Merge branch 'master' into feature_firmware_updater

This commit is contained in:
fieldOfView 2018-09-28 11:32:28 +02:00
commit 7c23a4e187
337 changed files with 88130 additions and 18157 deletions

2
.gitignore vendored
View file

@ -15,6 +15,7 @@ LC_MESSAGES
.cache .cache
*.qmlc *.qmlc
.mypy_cache .mypy_cache
.pytest_cache
#MacOS #MacOS
.DS_Store .DS_Store
@ -25,6 +26,7 @@ LC_MESSAGES
*.lprof *.lprof
*~ *~
*.qm *.qm
.directory
.idea .idea
cura.desktop cura.desktop

75
Jenkinsfile vendored
View file

@ -12,6 +12,30 @@ parallel_nodes(['linux && cura', 'windows && cura']) {
// If any error occurs during building, we want to catch it and continue with the "finale" stage. // If any error occurs during building, we want to catch it and continue with the "finale" stage.
catchError { catchError {
stage('Pre Checks') {
if (isUnix()) {
// Check shortcut keys
try {
sh """
echo 'Check for duplicate shortcut keys in all translation files.'
${env.CURA_ENVIRONMENT_PATH}/master/bin/python3 scripts/check_shortcut_keys.py
"""
} catch(e) {
currentBuild.result = "UNSTABLE"
}
// Check setting visibilities
try {
sh """
echo 'Check for duplicate shortcut keys in all translation files.'
${env.CURA_ENVIRONMENT_PATH}/master/bin/python3 scripts/check_setting_visibility.py
"""
} catch(e) {
currentBuild.result = "UNSTABLE"
}
}
}
// Building and testing should happen in a subdirectory. // Building and testing should happen in a subdirectory.
dir('build') { dir('build') {
// Perform the "build". Since Uranium is Python code, this basically only ensures CMake is setup. // Perform the "build". Since Uranium is Python code, this basically only ensures CMake is setup.
@ -28,10 +52,53 @@ parallel_nodes(['linux && cura', 'windows && cura']) {
// Try and run the unit tests. If this stage fails, we consider the build to be "unstable". // Try and run the unit tests. If this stage fails, we consider the build to be "unstable".
stage('Unit Test') { stage('Unit Test') {
try { if (isUnix()) {
make('test') // For Linux to show everything
} catch(e) { def branch = env.BRANCH_NAME
currentBuild.result = "UNSTABLE" if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}")) {
branch = "master"
}
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
try {
sh """
cd ..
export PYTHONPATH=.:"${uranium_dir}"
${env.CURA_ENVIRONMENT_PATH}/${branch}/bin/pytest -x --verbose --full-trace --capture=no ./tests
"""
} catch(e) {
currentBuild.result = "UNSTABLE"
}
}
else {
// For Windows
try {
// This also does code style checks.
bat 'ctest -V'
} catch(e) {
currentBuild.result = "UNSTABLE"
}
}
}
stage('Code Style') {
if (isUnix()) {
// For Linux to show everything
def branch = env.BRANCH_NAME
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}")) {
branch = "master"
}
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
try {
sh """
cd ..
export PYTHONPATH=.:"${uranium_dir}"
${env.CURA_ENVIRONMENT_PATH}/${branch}/bin/python3 run_mypy.py
"""
} catch(e) {
currentBuild.result = "UNSTABLE"
}
} }
} }
} }

View file

@ -34,7 +34,7 @@ function(cura_add_test)
if (NOT ${test_exists}) if (NOT ${test_exists})
add_test( add_test(
NAME ${_NAME} NAME ${_NAME}
COMMAND ${PYTHON_EXECUTABLE} -m pytest --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY} COMMAND ${PYTHON_EXECUTABLE} -m pytest --verbose --full-trace --capture=no --no-print-log --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY}
) )
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT LANG=C) set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT LANG=C)
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${_PYTHONPATH}") set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${_PYTHONPATH}")

View file

@ -13,6 +13,6 @@ TryExec=@CMAKE_INSTALL_FULL_BINDIR@/cura
Icon=cura-icon Icon=cura-icon
Terminal=false Terminal=false
Type=Application Type=Application
MimeType=application/sla;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml; MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml;
Categories=Graphics; Categories=Graphics;
Keywords=3D;Printing;Slicer; Keywords=3D;Printing;Slicer;

View file

@ -6,7 +6,7 @@
<glob-deleteall/> <glob-deleteall/>
<glob pattern="*.3mf"/> <glob pattern="*.3mf"/>
</mime-type> </mime-type>
<mime-type type="application/sla"> <mime-type type="model/stl">
<comment>Computer-aided design and manufacturing format</comment> <comment>Computer-aided design and manufacturing format</comment>
<icon name="unknown"/> <icon name="unknown"/>
<glob-deleteall/> <glob-deleteall/>

View file

@ -28,7 +28,7 @@ import copy
from typing import List, Optional from typing import List, Optional
# Setting for clearance around the prime # Radius of disallowed area in mm around prime. I.e. how much distance to keep from prime position.
PRIME_CLEARANCE = 6.5 PRIME_CLEARANCE = 6.5
@ -479,8 +479,6 @@ class BuildVolume(SceneNode):
maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - disallowed_area_size + bed_adhesion_size - 1) maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - disallowed_area_size + bed_adhesion_size - 1)
) )
self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds
self.updateNodeBoundaryCheck() self.updateNodeBoundaryCheck()
def getBoundingBox(self) -> AxisAlignedBox: def getBoundingBox(self) -> AxisAlignedBox:
@ -528,7 +526,7 @@ class BuildVolume(SceneNode):
def _onStackChanged(self): def _onStackChanged(self):
if self._global_container_stack: if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged) self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
extruders = ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()) extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
for extruder in extruders: for extruder in extruders:
extruder.propertyChanged.disconnect(self._onSettingPropertyChanged) extruder.propertyChanged.disconnect(self._onSettingPropertyChanged)
@ -536,7 +534,7 @@ class BuildVolume(SceneNode):
if self._global_container_stack: if self._global_container_stack:
self._global_container_stack.propertyChanged.connect(self._onSettingPropertyChanged) self._global_container_stack.propertyChanged.connect(self._onSettingPropertyChanged)
extruders = ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()) extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
for extruder in extruders: for extruder in extruders:
extruder.propertyChanged.connect(self._onSettingPropertyChanged) extruder.propertyChanged.connect(self._onSettingPropertyChanged)

View file

@ -19,5 +19,11 @@ class CameraImageProvider(QQuickImageProvider):
return image, QSize(15, 15) return image, QSize(15, 15)
except AttributeError: except AttributeError:
pass try:
image = output_device.activeCamera.getImage()
return image, QSize(15, 15)
except AttributeError:
pass
return QImage(), QSize(15, 15) return QImage(), QSize(15, 15)

View file

@ -13,6 +13,7 @@ from PyQt5.QtGui import QColor, QIcon
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
from UM.PluginError import PluginNotFoundError
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.Scene.Camera import Camera from UM.Scene.Camera import Camera
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
@ -67,9 +68,9 @@ from cura.Machines.Models.NozzleModel import NozzleModel
from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfilesDropDownMenuModel from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfilesDropDownMenuModel
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
from cura.Machines.Models.MaterialManagementModel import MaterialManagementModel from cura.Machines.Models.FavoriteMaterialsModel import FavoriteMaterialsModel
from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
from cura.Machines.Models.BrandMaterialsModel import BrandMaterialsModel from cura.Machines.Models.MaterialBrandsModel import MaterialBrandsModel
from cura.Machines.Models.QualityManagementModel import QualityManagementModel from cura.Machines.Models.QualityManagementModel import QualityManagementModel
from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
from cura.Machines.Models.MachineManagementModel import MachineManagementModel from cura.Machines.Models.MachineManagementModel import MachineManagementModel
@ -93,6 +94,7 @@ from . import CuraActions
from cura.Scene import ZOffsetDecorator from cura.Scene import ZOffsetDecorator
from . import CuraSplashScreen from . import CuraSplashScreen
from . import CameraImageProvider from . import CameraImageProvider
from . import PrintJobPreviewImageProvider
from . import MachineActionManager from . import MachineActionManager
from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager
@ -112,7 +114,9 @@ from UM.FlameProfiler import pyqtSlot
if TYPE_CHECKING: if TYPE_CHECKING:
from plugins.SliceInfoPlugin.SliceInfo import SliceInfo from cura.Machines.MaterialManager import MaterialManager
from cura.Machines.QualityManager import QualityManager
from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer
numpy.seterr(all = "ignore") numpy.seterr(all = "ignore")
@ -174,12 +178,12 @@ class CuraApplication(QtApplication):
self._machine_action_manager = None self._machine_action_manager = None
self.empty_container = None self.empty_container = None # type: EmptyInstanceContainer
self.empty_definition_changes_container = None self.empty_definition_changes_container = None # type: EmptyInstanceContainer
self.empty_variant_container = None self.empty_variant_container = None # type: EmptyInstanceContainer
self.empty_material_container = None self.empty_material_container = None # type: EmptyInstanceContainer
self.empty_quality_container = None self.empty_quality_container = None # type: EmptyInstanceContainer
self.empty_quality_changes_container = None self.empty_quality_changes_container = None # type: EmptyInstanceContainer
self._variant_manager = None self._variant_manager = None
self._material_manager = None self._material_manager = None
@ -215,7 +219,6 @@ class CuraApplication(QtApplication):
self._message_box_callback = None self._message_box_callback = None
self._message_box_callback_arguments = [] self._message_box_callback_arguments = []
self._preferred_mimetype = ""
self._i18n_catalog = None self._i18n_catalog = None
self._currently_loading_files = [] self._currently_loading_files = []
@ -368,7 +371,7 @@ class CuraApplication(QtApplication):
# Add empty variant, material and quality containers. # Add empty variant, material and quality containers.
# Since they are empty, they should never be serialized and instead just programmatically created. # Since they are empty, they should never be serialized and instead just programmatically created.
# We need them to simplify the switching between materials. # We need them to simplify the switching between materials.
self.empty_container = cura.Settings.cura_empty_instance_containers.empty_container self.empty_container = cura.Settings.cura_empty_instance_containers.empty_container # type: EmptyInstanceContainer
self._container_registry.addContainer( self._container_registry.addContainer(
cura.Settings.cura_empty_instance_containers.empty_definition_changes_container) cura.Settings.cura_empty_instance_containers.empty_definition_changes_container)
@ -429,6 +432,7 @@ class CuraApplication(QtApplication):
# Readers & Writers: # Readers & Writers:
"GCodeWriter", "GCodeWriter",
"STLReader", "STLReader",
"3MFWriter",
# Tools: # Tools:
"CameraTool", "CameraTool",
@ -481,7 +485,11 @@ class CuraApplication(QtApplication):
preferences.addPreference("view/filter_current_build_plate", False) preferences.addPreference("view/filter_current_build_plate", False)
preferences.addPreference("cura/sidebar_collapsed", False) preferences.addPreference("cura/sidebar_collapsed", False)
self._need_to_show_user_agreement = not self.getPreferences().getValue("general/accepted_user_agreement") preferences.addPreference("cura/favorite_materials", "")
preferences.addPreference("cura/expanded_brands", "")
preferences.addPreference("cura/expanded_types", "")
self._need_to_show_user_agreement = not preferences.getValue("general/accepted_user_agreement")
for key in [ for key in [
"dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin "dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin
@ -495,15 +503,13 @@ class CuraApplication(QtApplication):
self.applicationShuttingDown.connect(self.saveSettings) self.applicationShuttingDown.connect(self.saveSettings)
self.engineCreatedSignal.connect(self._onEngineCreated) self.engineCreatedSignal.connect(self._onEngineCreated)
self.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
self._onGlobalContainerChanged()
self.getCuraSceneController().setActiveBuildPlate(0) # Initialize self.getCuraSceneController().setActiveBuildPlate(0) # Initialize
CuraApplication.Created = True CuraApplication.Created = True
def _onEngineCreated(self): def _onEngineCreated(self):
self._qml_engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider()) self._qml_engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
self._qml_engine.addImageProvider("print_job_preview", PrintJobPreviewImageProvider.PrintJobPreviewImageProvider())
@pyqtProperty(bool) @pyqtProperty(bool)
def needToShowUserAgreement(self): def needToShowUserAgreement(self):
@ -806,20 +812,20 @@ class CuraApplication(QtApplication):
self._machine_manager = MachineManager(self) self._machine_manager = MachineManager(self)
return self._machine_manager return self._machine_manager
def getExtruderManager(self, *args): def getExtruderManager(self, *args) -> ExtruderManager:
if self._extruder_manager is None: if self._extruder_manager is None:
self._extruder_manager = ExtruderManager() self._extruder_manager = ExtruderManager()
return self._extruder_manager return self._extruder_manager
def getVariantManager(self, *args): def getVariantManager(self, *args) -> VariantManager:
return self._variant_manager return self._variant_manager
@pyqtSlot(result = QObject) @pyqtSlot(result = QObject)
def getMaterialManager(self, *args): def getMaterialManager(self, *args) -> "MaterialManager":
return self._material_manager return self._material_manager
@pyqtSlot(result = QObject) @pyqtSlot(result = QObject)
def getQualityManager(self, *args): def getQualityManager(self, *args) -> "QualityManager":
return self._quality_manager return self._quality_manager
def getObjectsModel(self, *args): def getObjectsModel(self, *args):
@ -828,23 +834,23 @@ class CuraApplication(QtApplication):
return self._object_manager return self._object_manager
@pyqtSlot(result = QObject) @pyqtSlot(result = QObject)
def getMultiBuildPlateModel(self, *args): def getMultiBuildPlateModel(self, *args) -> MultiBuildPlateModel:
if self._multi_build_plate_model is None: if self._multi_build_plate_model is None:
self._multi_build_plate_model = MultiBuildPlateModel(self) self._multi_build_plate_model = MultiBuildPlateModel(self)
return self._multi_build_plate_model return self._multi_build_plate_model
@pyqtSlot(result = QObject) @pyqtSlot(result = QObject)
def getBuildPlateModel(self, *args): def getBuildPlateModel(self, *args) -> BuildPlateModel:
if self._build_plate_model is None: if self._build_plate_model is None:
self._build_plate_model = BuildPlateModel(self) self._build_plate_model = BuildPlateModel(self)
return self._build_plate_model return self._build_plate_model
def getCuraSceneController(self, *args): def getCuraSceneController(self, *args) -> CuraSceneController:
if self._cura_scene_controller is None: if self._cura_scene_controller is None:
self._cura_scene_controller = CuraSceneController.createCuraSceneController() self._cura_scene_controller = CuraSceneController.createCuraSceneController()
return self._cura_scene_controller return self._cura_scene_controller
def getSettingInheritanceManager(self, *args): def getSettingInheritanceManager(self, *args) -> SettingInheritanceManager:
if self._setting_inheritance_manager is None: if self._setting_inheritance_manager is None:
self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager() self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager()
return self._setting_inheritance_manager return self._setting_inheritance_manager
@ -915,9 +921,9 @@ class CuraApplication(QtApplication):
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(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel")
qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel") qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel")
qmlRegisterType(BrandMaterialsModel, "Cura", 1, 0, "BrandMaterialsModel") qmlRegisterType(MaterialBrandsModel, "Cura", 1, 0, "MaterialBrandsModel")
qmlRegisterType(MaterialManagementModel, "Cura", 1, 0, "MaterialManagementModel")
qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel") qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel")
qmlRegisterType(MachineManagementModel, "Cura", 1, 0, "MachineManagementModel") qmlRegisterType(MachineManagementModel, "Cura", 1, 0, "MachineManagementModel")
@ -981,30 +987,14 @@ class CuraApplication(QtApplication):
self._camera_animation.setTarget(Selection.getSelectedObject(0).getWorldPosition()) self._camera_animation.setTarget(Selection.getSelectedObject(0).getWorldPosition())
self._camera_animation.start() self._camera_animation.start()
def _onGlobalContainerChanged(self):
if self._global_container_stack is not None:
machine_file_formats = [file_type.strip() for file_type in self._global_container_stack.getMetaDataEntry("file_formats").split(";")]
new_preferred_mimetype = ""
if machine_file_formats:
new_preferred_mimetype = machine_file_formats[0]
if new_preferred_mimetype != self._preferred_mimetype:
self._preferred_mimetype = new_preferred_mimetype
self.preferredOutputMimetypeChanged.emit()
requestAddPrinter = pyqtSignal() requestAddPrinter = pyqtSignal()
activityChanged = pyqtSignal() activityChanged = pyqtSignal()
sceneBoundingBoxChanged = pyqtSignal() sceneBoundingBoxChanged = pyqtSignal()
preferredOutputMimetypeChanged = pyqtSignal()
@pyqtProperty(bool, notify = activityChanged) @pyqtProperty(bool, notify = activityChanged)
def platformActivity(self): def platformActivity(self):
return self._platform_activity return self._platform_activity
@pyqtProperty(str, notify=preferredOutputMimetypeChanged)
def preferredOutputMimetype(self):
return self._preferred_mimetype
@pyqtProperty(str, notify = sceneBoundingBoxChanged) @pyqtProperty(str, notify = sceneBoundingBoxChanged)
def getSceneBoundingBoxString(self): def getSceneBoundingBoxString(self):
return self._i18n_catalog.i18nc("@info 'width', 'depth' and 'height' are variable names that must NOT be translated; just translate the format of ##x##x## mm.", "%(width).1f x %(depth).1f x %(height).1f mm") % {'width' : self._scene_bounding_box.width.item(), 'depth': self._scene_bounding_box.depth.item(), 'height' : self._scene_bounding_box.height.item()} return self._i18n_catalog.i18nc("@info 'width', 'depth' and 'height' are variable names that must NOT be translated; just translate the format of ##x##x## mm.", "%(width).1f x %(depth).1f x %(height).1f mm") % {'width' : self._scene_bounding_box.width.item(), 'depth': self._scene_bounding_box.depth.item(), 'height' : self._scene_bounding_box.height.item()}
@ -1720,7 +1710,11 @@ class CuraApplication(QtApplication):
@pyqtSlot() @pyqtSlot()
def showMoreInformationDialogForAnonymousDataCollection(self): def showMoreInformationDialogForAnonymousDataCollection(self):
cast(SliceInfo, self._plugin_registry.getPluginObject("SliceInfoPlugin")).showMoreInfoDialog() try:
slice_info = self._plugin_registry.getPluginObject("SliceInfoPlugin")
slice_info.showMoreInfoDialog()
except PluginNotFoundError:
Logger.log("w", "Plugin SliceInfo was not found, so not able to show the info dialog.")
def addSidebarCustomMenuItem(self, menu_item: dict) -> None: def addSidebarCustomMenuItem(self, menu_item: dict) -> None:
self._sidebar_custom_menu_items.append(menu_item) self._sidebar_custom_menu_items.append(menu_item)

View file

@ -21,7 +21,7 @@ class LayerPolygon:
__number_of_types = 11 __number_of_types = 11
__jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType, numpy.arange(__number_of_types) == MoveCombingType), numpy.arange(__number_of_types) == MoveRetractionType) __jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType, numpy.arange(__number_of_types) == MoveCombingType), numpy.arange(__number_of_types) == MoveRetractionType)
## LayerPolygon, used in ProcessSlicedLayersJob ## LayerPolygon, used in ProcessSlicedLayersJob
# \param extruder # \param extruder
# \param line_types array with line_types # \param line_types array with line_types

View file

@ -9,9 +9,6 @@ from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
from UM.Logger import Logger from UM.Logger import Logger
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
if TYPE_CHECKING:
from cura.Machines.QualityGroup import QualityGroup
## ##
# A metadata / container combination. Use getContainer() to get the container corresponding to the metadata. # A metadata / container combination. Use getContainer() to get the container corresponding to the metadata.
@ -24,29 +21,34 @@ if TYPE_CHECKING:
# This is used in Variant, Material, and Quality Managers. # This is used in Variant, Material, and Quality Managers.
# #
class ContainerNode: class ContainerNode:
__slots__ = ("metadata", "container", "children_map") __slots__ = ("_metadata", "_container", "children_map")
def __init__(self, metadata: Optional[Dict[str, Any]] = None) -> None: def __init__(self, metadata: Optional[Dict[str, Any]] = None) -> None:
self.metadata = metadata self._metadata = metadata
self.container = None self._container = None # type: Optional[InstanceContainer]
self.children_map = OrderedDict() #type: OrderedDict[str, Union[QualityGroup, ContainerNode]] self.children_map = OrderedDict() # type: ignore # This is because it's children are supposed to override it.
## Get an entry value from the metadata ## Get an entry value from the metadata
def getMetaDataEntry(self, entry: str, default: Any = None) -> Any: def getMetaDataEntry(self, entry: str, default: Any = None) -> Any:
if self.metadata is None: if self._metadata is None:
return default return default
return self.metadata.get(entry, default) return self._metadata.get(entry, default)
def getMetadata(self) -> Dict[str, Any]:
if self._metadata is None:
return {}
return self._metadata
def getChildNode(self, child_key: str) -> Optional["ContainerNode"]: def getChildNode(self, child_key: str) -> Optional["ContainerNode"]:
return self.children_map.get(child_key) return self.children_map.get(child_key)
def getContainer(self) -> Optional["InstanceContainer"]: def getContainer(self) -> Optional["InstanceContainer"]:
if self.metadata is None: if self._metadata is None:
Logger.log("e", "Cannot get container for a ContainerNode without metadata.") Logger.log("e", "Cannot get container for a ContainerNode without metadata.")
return None return None
if self.container is None: if self._container is None:
container_id = self.metadata["id"] container_id = self._metadata["id"]
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
container_list = ContainerRegistry.getInstance().findInstanceContainers(id = container_id) container_list = ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
if not container_list: if not container_list:
@ -54,9 +56,9 @@ class ContainerNode:
error_message = ConfigurationErrorMessage.getInstance() error_message = ConfigurationErrorMessage.getInstance()
error_message.addFaultyContainers(container_id) error_message.addFaultyContainers(container_id)
return None return None
self.container = container_list[0] self._container = container_list[0]
return self.container return self._container
def __str__(self) -> str: def __str__(self) -> str:
return "%s[%s]" % (self.__class__.__name__, self.getMetaDataEntry("id")) return "%s[%s]" % (self.__class__.__name__, self.getMetaDataEntry("id"))

View file

@ -50,7 +50,7 @@ class MachineErrorChecker(QObject):
self._error_check_timer.setInterval(100) self._error_check_timer.setInterval(100)
self._error_check_timer.setSingleShot(True) self._error_check_timer.setSingleShot(True)
def initialize(self): def initialize(self) -> None:
self._error_check_timer.timeout.connect(self._rescheduleCheck) self._error_check_timer.timeout.connect(self._rescheduleCheck)
# Reconnect all signals when the active machine gets changed. # Reconnect all signals when the active machine gets changed.
@ -62,7 +62,7 @@ class MachineErrorChecker(QObject):
self._onMachineChanged() self._onMachineChanged()
def _onMachineChanged(self): def _onMachineChanged(self) -> None:
if self._global_stack: if self._global_stack:
self._global_stack.propertyChanged.disconnect(self.startErrorCheck) self._global_stack.propertyChanged.disconnect(self.startErrorCheck)
self._global_stack.containersChanged.disconnect(self.startErrorCheck) self._global_stack.containersChanged.disconnect(self.startErrorCheck)
@ -94,7 +94,7 @@ class MachineErrorChecker(QObject):
return self._need_to_check or self._check_in_progress return self._need_to_check or self._check_in_progress
# Starts the error check timer to schedule a new error check. # Starts the error check timer to schedule a new error check.
def startErrorCheck(self, *args): def startErrorCheck(self, *args) -> None:
if not self._check_in_progress: if not self._check_in_progress:
self._need_to_check = True self._need_to_check = True
self.needToWaitForResultChanged.emit() self.needToWaitForResultChanged.emit()
@ -103,7 +103,7 @@ class MachineErrorChecker(QObject):
# This function is called by the timer to reschedule a new error check. # This function is called by the timer to reschedule a new error check.
# If there is no check in progress, it will start a new one. If there is any, it sets the "_need_to_check" flag # If there is no check in progress, it will start a new one. If there is any, it sets the "_need_to_check" flag
# to notify the current check to stop and start a new one. # to notify the current check to stop and start a new one.
def _rescheduleCheck(self): def _rescheduleCheck(self) -> None:
if self._check_in_progress and not self._need_to_check: if self._check_in_progress and not self._need_to_check:
self._need_to_check = True self._need_to_check = True
self.needToWaitForResultChanged.emit() self.needToWaitForResultChanged.emit()
@ -128,7 +128,7 @@ class MachineErrorChecker(QObject):
self._start_time = time.time() self._start_time = time.time()
Logger.log("d", "New error check scheduled.") Logger.log("d", "New error check scheduled.")
def _checkStack(self): def _checkStack(self) -> None:
if self._need_to_check: if self._need_to_check:
Logger.log("d", "Need to check for errors again. Discard the current progress and reschedule a check.") Logger.log("d", "Need to check for errors again. Discard the current progress and reschedule a check.")
self._check_in_progress = False self._check_in_progress = False
@ -169,7 +169,7 @@ class MachineErrorChecker(QObject):
# Schedule the check for the next key # Schedule the check for the next key
self._application.callLater(self._checkStack) self._application.callLater(self._checkStack)
def _setResult(self, result: bool): def _setResult(self, result: bool) -> None:
if result != self._has_errors: if result != self._has_errors:
self._has_errors = result self._has_errors = result
self.hasErrorUpdated.emit() self.hasErrorUpdated.emit()

View file

@ -24,8 +24,8 @@ class MaterialGroup:
def __init__(self, name: str, root_material_node: "MaterialNode") -> None: def __init__(self, name: str, root_material_node: "MaterialNode") -> None:
self.name = name self.name = name
self.is_read_only = False self.is_read_only = False
self.root_material_node = root_material_node # type: MaterialNode self.root_material_node = root_material_node # type: MaterialNode
self.derived_material_node_list = [] # type: List[MaterialNode] self.derived_material_node_list = [] # type: List[MaterialNode]
def __str__(self) -> str: def __str__(self) -> str:
return "%s[%s]" % (self.__class__.__name__, self.name) return "%s[%s]" % (self.__class__.__name__, self.name)

View file

@ -4,7 +4,7 @@
from collections import defaultdict, OrderedDict from collections import defaultdict, OrderedDict
import copy import copy
import uuid import uuid
from typing import Dict, Optional, TYPE_CHECKING from typing import Dict, Optional, TYPE_CHECKING, Any, Set, List, cast, Tuple
from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot
@ -21,7 +21,6 @@ from .VariantType import VariantType
if TYPE_CHECKING: if TYPE_CHECKING:
from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Settings.InstanceContainer import InstanceContainer
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from cura.Settings.ExtruderStack import ExtruderStack from cura.Settings.ExtruderStack import ExtruderStack
@ -39,24 +38,34 @@ if TYPE_CHECKING:
class MaterialManager(QObject): class MaterialManager(QObject):
materialsUpdated = pyqtSignal() # Emitted whenever the material lookup tables are updated. materialsUpdated = pyqtSignal() # Emitted whenever the material lookup tables are updated.
favoritesUpdated = pyqtSignal() # Emitted whenever the favorites are changed
def __init__(self, container_registry, parent = None): def __init__(self, container_registry, parent = None):
super().__init__(parent) super().__init__(parent)
self._application = Application.getInstance() self._application = Application.getInstance()
self._container_registry = container_registry # type: ContainerRegistry self._container_registry = container_registry # type: ContainerRegistry
self._fallback_materials_map = dict() # material_type -> generic material metadata # Material_type -> generic material metadata
self._material_group_map = dict() # root_material_id -> MaterialGroup self._fallback_materials_map = dict() # type: Dict[str, Dict[str, Any]]
self._diameter_machine_nozzle_buildplate_material_map = dict() # approximate diameter str -> dict(machine_definition_id -> MaterialNode)
# Root_material_id -> MaterialGroup
self._material_group_map = dict() # type: Dict[str, MaterialGroup]
# Approximate diameter str
self._diameter_machine_nozzle_buildplate_material_map = dict() # type: Dict[str, Dict[str, MaterialNode]]
# We're using these two maps to convert between the specific diameter material id and the generic material id # We're using these two maps to convert between the specific diameter material id and the generic material id
# because the generic material ids are used in qualities and definitions, while the specific diameter material is meant # because the generic material ids are used in qualities and definitions, while the specific diameter material is meant
# i.e. generic_pla -> generic_pla_175 # i.e. generic_pla -> generic_pla_175
self._material_diameter_map = defaultdict(dict) # root_material_id -> approximate diameter str -> root_material_id for that diameter # root_material_id -> approximate diameter str -> root_material_id for that diameter
self._diameter_material_map = dict() # material id including diameter (generic_pla_175) -> material root id (generic_pla) self._material_diameter_map = defaultdict(dict) # type: Dict[str, Dict[str, str]]
# Material id including diameter (generic_pla_175) -> material root id (generic_pla)
self._diameter_material_map = dict() # type: Dict[str, str]
# This is used in Legacy UM3 send material function and the material management page. # This is used in Legacy UM3 send material function and the material management page.
self._guid_material_groups_map = defaultdict(list) # GUID -> a list of material_groups # GUID -> a list of material_groups
self._guid_material_groups_map = defaultdict(list) # type: Dict[str, List[MaterialGroup]]
# The machine definition ID for the non-machine-specific materials. # The machine definition ID for the non-machine-specific materials.
# This is used as the last fallback option if the given machine-specific material(s) cannot be found. # This is used as the last fallback option if the given machine-specific material(s) cannot be found.
@ -75,13 +84,15 @@ class MaterialManager(QObject):
self._container_registry.containerAdded.connect(self._onContainerMetadataChanged) self._container_registry.containerAdded.connect(self._onContainerMetadataChanged)
self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged) self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged)
def initialize(self): self._favorites = set() # type: Set[str]
def initialize(self) -> None:
# Find all materials and put them in a matrix for quick search. # Find all materials and put them in a matrix for quick search.
material_metadatas = {metadata["id"]: metadata for metadata in material_metadatas = {metadata["id"]: metadata for metadata in
self._container_registry.findContainersMetadata(type = "material") if self._container_registry.findContainersMetadata(type = "material") if
metadata.get("GUID")} metadata.get("GUID")} # type: Dict[str, Dict[str, Any]]
self._material_group_map = dict() self._material_group_map = dict() # type: Dict[str, MaterialGroup]
# Map #1 # Map #1
# root_material_id -> MaterialGroup # root_material_id -> MaterialGroup
@ -90,7 +101,7 @@ class MaterialManager(QObject):
if material_id == "empty_material": if material_id == "empty_material":
continue continue
root_material_id = material_metadata.get("base_file") root_material_id = material_metadata.get("base_file", "")
if root_material_id not in self._material_group_map: if root_material_id not in self._material_group_map:
self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id])) self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id]))
self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id) self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id)
@ -106,26 +117,26 @@ class MaterialManager(QObject):
# Map #1.5 # Map #1.5
# GUID -> material group list # GUID -> material group list
self._guid_material_groups_map = defaultdict(list) self._guid_material_groups_map = defaultdict(list) # type: Dict[str, List[MaterialGroup]]
for root_material_id, material_group in self._material_group_map.items(): for root_material_id, material_group in self._material_group_map.items():
guid = material_group.root_material_node.metadata["GUID"] guid = material_group.root_material_node.getMetaDataEntry("GUID", "")
self._guid_material_groups_map[guid].append(material_group) self._guid_material_groups_map[guid].append(material_group)
# Map #2 # Map #2
# Lookup table for material type -> fallback material metadata, only for read-only materials # Lookup table for material type -> fallback material metadata, only for read-only materials
grouped_by_type_dict = dict() grouped_by_type_dict = dict() # type: Dict[str, Any]
material_types_without_fallback = set() material_types_without_fallback = set()
for root_material_id, material_node in self._material_group_map.items(): for root_material_id, material_node in self._material_group_map.items():
material_type = material_node.root_material_node.metadata["material"] material_type = material_node.root_material_node.getMetaDataEntry("material", "")
if material_type not in grouped_by_type_dict: if material_type not in grouped_by_type_dict:
grouped_by_type_dict[material_type] = {"generic": None, grouped_by_type_dict[material_type] = {"generic": None,
"others": []} "others": []}
material_types_without_fallback.add(material_type) material_types_without_fallback.add(material_type)
brand = material_node.root_material_node.metadata["brand"] brand = material_node.root_material_node.getMetaDataEntry("brand", "")
if brand.lower() == "generic": if brand.lower() == "generic":
to_add = True to_add = True
if material_type in grouped_by_type_dict: if material_type in grouped_by_type_dict:
diameter = material_node.root_material_node.metadata.get("approximate_diameter") diameter = material_node.root_material_node.getMetaDataEntry("approximate_diameter", "")
if diameter != self._default_approximate_diameter_for_quality_search: if diameter != self._default_approximate_diameter_for_quality_search:
to_add = False # don't add if it's not the default diameter to_add = False # don't add if it's not the default diameter
@ -134,7 +145,7 @@ class MaterialManager(QObject):
# - if it's in the list, it means that is a new material without fallback # - if it's in the list, it means that is a new material without fallback
# - if it is not, then it is a custom material with a fallback material (parent) # - if it is not, then it is a custom material with a fallback material (parent)
if material_type in material_types_without_fallback: if material_type in material_types_without_fallback:
grouped_by_type_dict[material_type] = material_node.root_material_node.metadata grouped_by_type_dict[material_type] = material_node.root_material_node._metadata
material_types_without_fallback.remove(material_type) material_types_without_fallback.remove(material_type)
# Remove the materials that have no fallback materials # Remove the materials that have no fallback materials
@ -151,15 +162,15 @@ class MaterialManager(QObject):
self._diameter_material_map = dict() self._diameter_material_map = dict()
# Group the material IDs by the same name, material, brand, and color but with different diameters. # Group the material IDs by the same name, material, brand, and color but with different diameters.
material_group_dict = dict() material_group_dict = dict() # type: Dict[Tuple[Any], Dict[str, str]]
keys_to_fetch = ("name", "material", "brand", "color") keys_to_fetch = ("name", "material", "brand", "color")
for root_material_id, machine_node in self._material_group_map.items(): for root_material_id, machine_node in self._material_group_map.items():
root_material_metadata = machine_node.root_material_node.metadata root_material_metadata = machine_node.root_material_node._metadata
key_data = [] key_data_list = [] # type: List[Any]
for key in keys_to_fetch: for key in keys_to_fetch:
key_data.append(root_material_metadata.get(key)) key_data_list.append(machine_node.root_material_node.getMetaDataEntry(key))
key_data = tuple(key_data) key_data = cast(Tuple[Any], tuple(key_data_list)) # type: Tuple[Any]
# If the key_data doesn't exist, it doesn't matter if the material is read only... # If the key_data doesn't exist, it doesn't matter if the material is read only...
if key_data not in material_group_dict: if key_data not in material_group_dict:
@ -168,8 +179,8 @@ class MaterialManager(QObject):
# ...but if key_data exists, we just overwrite it if the material is read only, otherwise we skip it # ...but if key_data exists, we just overwrite it if the material is read only, otherwise we skip it
if not machine_node.is_read_only: if not machine_node.is_read_only:
continue continue
approximate_diameter = root_material_metadata.get("approximate_diameter") approximate_diameter = machine_node.root_material_node.getMetaDataEntry("approximate_diameter", "")
material_group_dict[key_data][approximate_diameter] = root_material_metadata["id"] material_group_dict[key_data][approximate_diameter] = machine_node.root_material_node.getMetaDataEntry("id", "")
# Map [root_material_id][diameter] -> root_material_id for this diameter # Map [root_material_id][diameter] -> root_material_id for this diameter
for data_dict in material_group_dict.values(): for data_dict in material_group_dict.values():
@ -188,13 +199,17 @@ class MaterialManager(QObject):
# Map #4 # Map #4
# "machine" -> "nozzle name" -> "buildplate name" -> "root material ID" -> specific material InstanceContainer # "machine" -> "nozzle name" -> "buildplate name" -> "root material ID" -> specific material InstanceContainer
self._diameter_machine_nozzle_buildplate_material_map = dict() self._diameter_machine_nozzle_buildplate_material_map = dict() # type: Dict[str, Dict[str, MaterialNode]]
for material_metadata in material_metadatas.values(): for material_metadata in material_metadatas.values():
self.__addMaterialMetadataIntoLookupTree(material_metadata) self.__addMaterialMetadataIntoLookupTree(material_metadata)
favorites = self._application.getPreferences().getValue("cura/favorite_materials")
for item in favorites.split(";"):
self._favorites.add(item)
self.materialsUpdated.emit() self.materialsUpdated.emit()
def __addMaterialMetadataIntoLookupTree(self, material_metadata: dict) -> None: def __addMaterialMetadataIntoLookupTree(self, material_metadata: Dict[str, Any]) -> None:
material_id = material_metadata["id"] material_id = material_metadata["id"]
# We don't store empty material in the lookup tables # We don't store empty material in the lookup tables
@ -281,7 +296,7 @@ class MaterialManager(QObject):
return self._material_diameter_map.get(root_material_id, {}).get(approximate_diameter, root_material_id) return self._material_diameter_map.get(root_material_id, {}).get(approximate_diameter, root_material_id)
def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str: def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str:
return self._diameter_material_map.get(root_material_id) return self._diameter_material_map.get(root_material_id, "")
def getMaterialGroupListByGUID(self, guid: str) -> Optional[list]: def getMaterialGroupListByGUID(self, guid: str) -> Optional[list]:
return self._guid_material_groups_map.get(guid) return self._guid_material_groups_map.get(guid)
@ -321,6 +336,7 @@ class MaterialManager(QObject):
machine_exclude_materials = machine_definition.getMetaDataEntry("exclude_materials", []) machine_exclude_materials = machine_definition.getMetaDataEntry("exclude_materials", [])
material_id_metadata_dict = dict() # type: Dict[str, MaterialNode] material_id_metadata_dict = dict() # type: Dict[str, MaterialNode]
excluded_materials = set()
for current_node in nodes_to_check: for current_node in nodes_to_check:
if current_node is None: if current_node is None:
continue continue
@ -329,20 +345,22 @@ class MaterialManager(QObject):
# Do not exclude other materials that are of the same type. # Do not exclude other materials that are of the same type.
for material_id, node in current_node.material_map.items(): for material_id, node in current_node.material_map.items():
if material_id in machine_exclude_materials: if material_id in machine_exclude_materials:
Logger.log("d", "Exclude material [%s] for machine [%s]", excluded_materials.add(material_id)
material_id, machine_definition.getId())
continue continue
if material_id not in material_id_metadata_dict: if material_id not in material_id_metadata_dict:
material_id_metadata_dict[material_id] = node material_id_metadata_dict[material_id] = node
if excluded_materials:
Logger.log("d", "Exclude materials {excluded_materials} for machine {machine_definition_id}".format(excluded_materials = ", ".join(excluded_materials), machine_definition_id = machine_definition_id))
return material_id_metadata_dict return material_id_metadata_dict
# #
# A convenience function to get available materials for the given machine with the extruder position. # A convenience function to get available materials for the given machine with the extruder position.
# #
def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack", def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack",
extruder_stack: "ExtruderStack") -> Optional[dict]: extruder_stack: "ExtruderStack") -> Optional[Dict[str, MaterialNode]]:
buildplate_name = machine.getBuildplateName() buildplate_name = machine.getBuildplateName()
nozzle_name = None nozzle_name = None
if extruder_stack.variant.getId() != "empty_variant": if extruder_stack.variant.getId() != "empty_variant":
@ -359,7 +377,7 @@ class MaterialManager(QObject):
# 2. cannot find any material InstanceContainers with the given settings. # 2. cannot find any material InstanceContainers with the given settings.
# #
def getMaterialNode(self, machine_definition_id: str, nozzle_name: Optional[str], def getMaterialNode(self, machine_definition_id: str, nozzle_name: Optional[str],
buildplate_name: Optional[str], diameter: float, root_material_id: str) -> Optional["InstanceContainer"]: buildplate_name: Optional[str], diameter: float, root_material_id: str) -> Optional["MaterialNode"]:
# round the diameter to get the approximate diameter # round the diameter to get the approximate diameter
rounded_diameter = str(round(diameter)) rounded_diameter = str(round(diameter))
if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map: if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
@ -368,7 +386,7 @@ class MaterialManager(QObject):
return None return None
# If there are nozzle materials, get the nozzle-specific material # If there are nozzle materials, get the nozzle-specific material
machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter] machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter] # type: Dict[str, MaterialNode]
machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id) machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id)
nozzle_node = None nozzle_node = None
buildplate_node = None buildplate_node = None
@ -417,7 +435,7 @@ class MaterialManager(QObject):
# Look at the guid to material dictionary # Look at the guid to material dictionary
root_material_id = None root_material_id = None
for material_group in self._guid_material_groups_map[material_guid]: for material_group in self._guid_material_groups_map[material_guid]:
root_material_id = material_group.root_material_node.metadata["id"] root_material_id = cast(str, material_group.root_material_node.getMetaDataEntry("id", ""))
break break
if not root_material_id: if not root_material_id:
@ -493,7 +511,7 @@ class MaterialManager(QObject):
# Sets the new name for the given material. # Sets the new name for the given material.
# #
@pyqtSlot("QVariant", str) @pyqtSlot("QVariant", str)
def setMaterialName(self, material_node: "MaterialNode", name: str): def setMaterialName(self, material_node: "MaterialNode", name: str) -> None:
root_material_id = material_node.getMetaDataEntry("base_file") root_material_id = material_node.getMetaDataEntry("base_file")
if root_material_id is None: if root_material_id is None:
return return
@ -511,7 +529,7 @@ class MaterialManager(QObject):
# Removes the given material. # Removes the given material.
# #
@pyqtSlot("QVariant") @pyqtSlot("QVariant")
def removeMaterial(self, material_node: "MaterialNode"): def removeMaterial(self, material_node: "MaterialNode") -> None:
root_material_id = material_node.getMetaDataEntry("base_file") root_material_id = material_node.getMetaDataEntry("base_file")
if root_material_id is not None: if root_material_id is not None:
self.removeMaterialByRootId(root_material_id) self.removeMaterialByRootId(root_material_id)
@ -521,8 +539,8 @@ class MaterialManager(QObject):
# Returns the root material ID of the duplicated material if successful. # Returns the root material ID of the duplicated material if successful.
# #
@pyqtSlot("QVariant", result = str) @pyqtSlot("QVariant", result = str)
def duplicateMaterial(self, material_node, new_base_id = None, new_metadata = None) -> Optional[str]: def duplicateMaterial(self, material_node: MaterialNode, new_base_id: Optional[str] = None, new_metadata: Dict[str, Any] = None) -> Optional[str]:
root_material_id = material_node.metadata["base_file"] root_material_id = cast(str, material_node.getMetaDataEntry("base_file", ""))
material_group = self.getMaterialGroup(root_material_id) material_group = self.getMaterialGroup(root_material_id)
if not material_group: if not material_group:
@ -573,11 +591,17 @@ class MaterialManager(QObject):
for container_to_add in new_containers: for container_to_add in new_containers:
container_to_add.setDirty(True) container_to_add.setDirty(True)
self._container_registry.addContainer(container_to_add) self._container_registry.addContainer(container_to_add)
# if the duplicated material was favorite then the new material should also be added to favorite.
if root_material_id in self.getFavorites():
self.addFavorite(new_base_id)
return new_base_id return new_base_id
# #
# Create a new material by cloning Generic PLA for the current material diameter and generate a new GUID. # Create a new material by cloning Generic PLA for the current material diameter and generate a new GUID.
# # Returns the ID of the newly created material.
@pyqtSlot(result = str) @pyqtSlot(result = str)
def createMaterial(self) -> str: def createMaterial(self) -> str:
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
@ -608,3 +632,25 @@ class MaterialManager(QObject):
new_base_id = new_id, new_base_id = new_id,
new_metadata = new_metadata) new_metadata = new_metadata)
return new_id return new_id
@pyqtSlot(str)
def addFavorite(self, root_material_id: str) -> None:
self._favorites.add(root_material_id)
self.materialsUpdated.emit()
# Ensure all settings are saved.
self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites)))
self._application.saveSettings()
@pyqtSlot(str)
def removeFavorite(self, root_material_id: str) -> None:
self._favorites.remove(root_material_id)
self.materialsUpdated.emit()
# Ensure all settings are saved.
self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites)))
self._application.saveSettings()
@pyqtSlot()
def getFavorites(self):
return self._favorites

View file

@ -1,7 +1,7 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 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 typing import Optional, Dict from typing import Optional, Dict, Any
from collections import OrderedDict
from .ContainerNode import ContainerNode from .ContainerNode import ContainerNode
@ -14,6 +14,12 @@ from .ContainerNode import ContainerNode
class MaterialNode(ContainerNode): class MaterialNode(ContainerNode):
__slots__ = ("material_map", "children_map") __slots__ = ("material_map", "children_map")
def __init__(self, metadata: Optional[dict] = None) -> None: def __init__(self, metadata: Optional[Dict[str, Any]] = None) -> None:
super().__init__(metadata = metadata) super().__init__(metadata = metadata)
self.material_map = {} # type: Dict[str, MaterialNode] # material_root_id -> material_node self.material_map = {} # type: Dict[str, MaterialNode] # material_root_id -> material_node
# We overide this as we want to indicate that MaterialNodes can only contain other material nodes.
self.children_map = OrderedDict() # type: OrderedDict[str, "MaterialNode"]
def getChildNode(self, child_key: str) -> Optional["MaterialNode"]:
return self.children_map.get(child_key)

View file

@ -2,45 +2,60 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
from UM.Application import Application
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
# ## This is the base model class for GenericMaterialsModel and MaterialBrandsModel.
# This is the base model class for GenericMaterialsModel and BrandMaterialsModel # Those 2 models are used by the material drop down menu to show generic materials and branded materials separately.
# Those 2 models are used by the material drop down menu to show generic materials and branded materials separately. # The extruder position defined here is being used to bound a menu to the correct extruder. This is used in the top
# The extruder position defined here is being used to bound a menu to the correct extruder. This is used in the top # bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu
# bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu
#
class BaseMaterialsModel(ListModel): class BaseMaterialsModel(ListModel):
RootMaterialIdRole = Qt.UserRole + 1
IdRole = Qt.UserRole + 2
NameRole = Qt.UserRole + 3
BrandRole = Qt.UserRole + 4
MaterialRole = Qt.UserRole + 5
ColorRole = Qt.UserRole + 6
ContainerNodeRole = Qt.UserRole + 7
extruderPositionChanged = pyqtSignal() extruderPositionChanged = pyqtSignal()
def __init__(self, parent = None): def __init__(self, parent = None):
super().__init__(parent) super().__init__(parent)
self._application = Application.getInstance()
self._machine_manager = self._application.getMachineManager()
self.addRoleName(self.RootMaterialIdRole, "root_material_id") from cura.CuraApplication import CuraApplication
self.addRoleName(self.IdRole, "id")
self.addRoleName(self.NameRole, "name") self._application = CuraApplication.getInstance()
self.addRoleName(self.BrandRole, "brand")
self.addRoleName(self.MaterialRole, "material") # Make these managers available to all material models
self.addRoleName(self.ColorRole, "color_name") self._container_registry = self._application.getInstance().getContainerRegistry()
self.addRoleName(self.ContainerNodeRole, "container_node") self._machine_manager = self._application.getMachineManager()
self._material_manager = self._application.getMaterialManager()
# Update the stack and the model data when the machine changes
self._machine_manager.globalContainerChanged.connect(self._updateExtruderStack)
# Update this model when switching machines
self._machine_manager.activeStackChanged.connect(self._update)
# Update this model when list of materials changes
self._material_manager.materialsUpdated.connect(self._update)
self.addRoleName(Qt.UserRole + 1, "root_material_id")
self.addRoleName(Qt.UserRole + 2, "id")
self.addRoleName(Qt.UserRole + 3, "GUID")
self.addRoleName(Qt.UserRole + 4, "name")
self.addRoleName(Qt.UserRole + 5, "brand")
self.addRoleName(Qt.UserRole + 6, "description")
self.addRoleName(Qt.UserRole + 7, "material")
self.addRoleName(Qt.UserRole + 8, "color_name")
self.addRoleName(Qt.UserRole + 9, "color_code")
self.addRoleName(Qt.UserRole + 10, "density")
self.addRoleName(Qt.UserRole + 11, "diameter")
self.addRoleName(Qt.UserRole + 12, "approximate_diameter")
self.addRoleName(Qt.UserRole + 13, "adhesion_info")
self.addRoleName(Qt.UserRole + 14, "is_read_only")
self.addRoleName(Qt.UserRole + 15, "container_node")
self.addRoleName(Qt.UserRole + 16, "is_favorite")
self._extruder_position = 0 self._extruder_position = 0
self._extruder_stack = None self._extruder_stack = None
# Update the stack and the model data when the machine changes
self._machine_manager.globalContainerChanged.connect(self._updateExtruderStack) self._available_materials = None
self._favorite_ids = None
def _updateExtruderStack(self): def _updateExtruderStack(self):
global_stack = self._machine_manager.activeMachine global_stack = self._machine_manager.activeMachine
@ -65,8 +80,55 @@ class BaseMaterialsModel(ListModel):
def extruderPosition(self) -> int: def extruderPosition(self) -> int:
return self._extruder_position return self._extruder_position
# ## This is an abstract method that needs to be implemented by the specific
# This is an abstract method that needs to be implemented by # models themselves.
#
def _update(self): def _update(self):
pass pass
## This method is used by all material models in the beginning of the
# _update() method in order to prevent errors. It's the same in all models
# so it's placed here for easy access.
def _canUpdate(self):
global_stack = self._machine_manager.activeMachine
if global_stack is None:
return False
extruder_position = str(self._extruder_position)
if extruder_position not in global_stack.extruders:
return False
extruder_stack = global_stack.extruders[extruder_position]
self._available_materials = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack, extruder_stack)
if self._available_materials is None:
return False
return True
## This is another convenience function which is shared by all material
# models so it's put here to avoid having so much duplicated code.
def _createMaterialItem(self, root_material_id, container_node):
metadata = container_node.getMetadata()
item = {
"root_material_id": root_material_id,
"id": metadata["id"],
"container_id": metadata["id"], # TODO: Remove duplicate in material manager qml
"GUID": metadata["GUID"],
"name": metadata["name"],
"brand": metadata["brand"],
"description": metadata["description"],
"material": metadata["material"],
"color_name": metadata["color_name"],
"color_code": metadata.get("color_code", ""),
"density": metadata.get("properties", {}).get("density", ""),
"diameter": metadata.get("properties", {}).get("diameter", ""),
"approximate_diameter": metadata["approximate_diameter"],
"adhesion_info": metadata["adhesion_info"],
"is_read_only": self._container_registry.isReadOnly(metadata["id"]),
"container_node": container_node,
"is_favorite": root_material_id in self._favorite_ids
}
return item

View file

@ -1,157 +0,0 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
from UM.Qt.ListModel import ListModel
from UM.Logger import Logger
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
#
# This is an intermediate model to group materials with different colours for a same brand and type.
#
class MaterialsModelGroupedByType(ListModel):
NameRole = Qt.UserRole + 1
ColorsRole = Qt.UserRole + 2
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.ColorsRole, "colors")
#
# This model is used to show branded materials in the material drop down menu.
# The structure of the menu looks like this:
# Brand -> Material Type -> list of materials
#
# To illustrate, a branded material menu may look like this:
# Ultimaker -> PLA -> Yellow PLA
# -> Black PLA
# -> ...
# -> ABS -> White ABS
# ...
#
class BrandMaterialsModel(ListModel):
NameRole = Qt.UserRole + 1
MaterialsRole = Qt.UserRole + 2
extruderPositionChanged = pyqtSignal()
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.MaterialsRole, "materials")
self._extruder_position = 0
self._extruder_stack = None
from cura.CuraApplication import CuraApplication
self._machine_manager = CuraApplication.getInstance().getMachineManager()
self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
self._material_manager = CuraApplication.getInstance().getMaterialManager()
self._machine_manager.globalContainerChanged.connect(self._updateExtruderStack)
self._machine_manager.activeStackChanged.connect(self._update) #Update when switching machines.
self._material_manager.materialsUpdated.connect(self._update) #Update when the list of materials changes.
self._update()
def _updateExtruderStack(self):
global_stack = self._machine_manager.activeMachine
if global_stack is None:
return
if self._extruder_stack is not None:
self._extruder_stack.pyqtContainersChanged.disconnect(self._update)
self._extruder_stack = global_stack.extruders.get(str(self._extruder_position))
if self._extruder_stack is not None:
self._extruder_stack.pyqtContainersChanged.connect(self._update)
# Force update the model when the extruder stack changes
self._update()
def setExtruderPosition(self, position: int):
if self._extruder_stack is None or self._extruder_position != position:
self._extruder_position = position
self._updateExtruderStack()
self.extruderPositionChanged.emit()
@pyqtProperty(int, fset=setExtruderPosition, notify=extruderPositionChanged)
def extruderPosition(self) -> int:
return self._extruder_position
def _update(self):
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
global_stack = self._machine_manager.activeMachine
if global_stack is None:
self.setItems([])
return
extruder_position = str(self._extruder_position)
if extruder_position not in global_stack.extruders:
self.setItems([])
return
extruder_stack = global_stack.extruders[str(self._extruder_position)]
available_material_dict = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack,
extruder_stack)
if available_material_dict is None:
self.setItems([])
return
brand_item_list = []
brand_group_dict = {}
for root_material_id, container_node in available_material_dict.items():
metadata = container_node.metadata
brand = metadata["brand"]
# Only add results for generic materials
if brand.lower() == "generic":
continue
# Do not include the materials from a to-be-removed package
if bool(metadata.get("removed", False)):
continue
if brand not in brand_group_dict:
brand_group_dict[brand] = {}
material_type = metadata["material"]
if material_type not in brand_group_dict[brand]:
brand_group_dict[brand][material_type] = []
item = {"root_material_id": root_material_id,
"id": metadata["id"],
"name": metadata["name"],
"brand": metadata["brand"],
"material": metadata["material"],
"color_name": metadata["color_name"],
"container_node": container_node
}
brand_group_dict[brand][material_type].append(item)
for brand, material_dict in brand_group_dict.items():
brand_item = {"name": brand,
"materials": MaterialsModelGroupedByType(self)}
material_type_item_list = []
for material_type, material_list in material_dict.items():
material_type_item = {"name": material_type,
"colors": BaseMaterialsModel(self)}
material_type_item["colors"].clear()
# Sort materials by name
material_list = sorted(material_list, key = lambda x: x["name"].upper())
material_type_item["colors"].setItems(material_list)
material_type_item_list.append(material_type_item)
# Sort material type by name
material_type_item_list = sorted(material_type_item_list, key = lambda x: x["name"].upper())
brand_item["materials"].setItems(material_type_item_list)
brand_item_list.append(brand_item)
# Sort brand by name
brand_item_list = sorted(brand_item_list, key = lambda x: x["name"].upper())
self.setItems(brand_item_list)

View file

@ -0,0 +1,42 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
class FavoriteMaterialsModel(BaseMaterialsModel):
def __init__(self, parent = None):
super().__init__(parent)
self._update()
def _update(self):
# Perform standard check and reset if the check fails
if not self._canUpdate():
self.setItems([])
return
# Get updated list of favorites
self._favorite_ids = self._material_manager.getFavorites()
item_list = []
for root_material_id, container_node in self._available_materials.items():
metadata = container_node.getMetadata()
# Do not include the materials from a to-be-removed package
if bool(metadata.get("removed", False)):
continue
# Only add results for favorite materials
if root_material_id not in self._favorite_ids:
continue
item = self._createMaterialItem(root_material_id, container_node)
item_list.append(item)
# Sort the item list alphabetically by name
item_list = sorted(item_list, key = lambda d: d["brand"].upper())
self.setItems(item_list)

View file

@ -4,63 +4,39 @@
from UM.Logger import Logger from UM.Logger import Logger
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
class GenericMaterialsModel(BaseMaterialsModel): class GenericMaterialsModel(BaseMaterialsModel):
def __init__(self, parent = None): def __init__(self, parent = None):
super().__init__(parent) super().__init__(parent)
from cura.CuraApplication import CuraApplication
self._machine_manager = CuraApplication.getInstance().getMachineManager()
self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
self._material_manager = CuraApplication.getInstance().getMaterialManager()
self._machine_manager.activeStackChanged.connect(self._update) #Update when switching machines.
self._material_manager.materialsUpdated.connect(self._update) #Update when the list of materials changes.
self._update() self._update()
def _update(self): def _update(self):
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
global_stack = self._machine_manager.activeMachine # Perform standard check and reset if the check fails
if global_stack is None: if not self._canUpdate():
self.setItems([]) self.setItems([])
return return
extruder_position = str(self._extruder_position)
if extruder_position not in global_stack.extruders:
self.setItems([])
return
extruder_stack = global_stack.extruders[extruder_position]
available_material_dict = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack, # Get updated list of favorites
extruder_stack) self._favorite_ids = self._material_manager.getFavorites()
if available_material_dict is None:
self.setItems([])
return
item_list = [] item_list = []
for root_material_id, container_node in available_material_dict.items():
metadata = container_node.metadata
# Only add results for generic materials for root_material_id, container_node in self._available_materials.items():
if metadata["brand"].lower() != "generic": metadata = container_node.getMetadata()
continue
# Do not include the materials from a to-be-removed package # Do not include the materials from a to-be-removed package
if bool(metadata.get("removed", False)): if bool(metadata.get("removed", False)):
continue continue
item = {"root_material_id": root_material_id, # Only add results for generic materials
"id": metadata["id"], if metadata["brand"].lower() != "generic":
"name": metadata["name"], continue
"brand": metadata["brand"],
"material": metadata["material"], item = self._createMaterialItem(root_material_id, container_node)
"color_name": metadata["color_name"],
"container_node": container_node
}
item_list.append(item) item_list.append(item)
# Sort the item list by material name alphabetically # Sort the item list alphabetically by name
item_list = sorted(item_list, key = lambda d: d["name"].upper()) item_list = sorted(item_list, key = lambda d: d["name"].upper())
self.setItems(item_list) self.setItems(item_list)

View file

@ -0,0 +1,107 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
from UM.Qt.ListModel import ListModel
from UM.Logger import Logger
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
class MaterialTypesModel(ListModel):
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(Qt.UserRole + 1, "name")
self.addRoleName(Qt.UserRole + 2, "brand")
self.addRoleName(Qt.UserRole + 3, "colors")
class MaterialBrandsModel(BaseMaterialsModel):
extruderPositionChanged = pyqtSignal()
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(Qt.UserRole + 1, "name")
self.addRoleName(Qt.UserRole + 2, "material_types")
self._update()
def _update(self):
# Perform standard check and reset if the check fails
if not self._canUpdate():
self.setItems([])
return
# Get updated list of favorites
self._favorite_ids = self._material_manager.getFavorites()
brand_item_list = []
brand_group_dict = {}
# Part 1: Generate the entire tree of brands -> material types -> spcific materials
for root_material_id, container_node in self._available_materials.items():
# Do not include the materials from a to-be-removed package
if bool(container_node.getMetaDataEntry("removed", False)):
continue
# Add brands we haven't seen yet to the dict, skipping generics
brand = container_node.getMetaDataEntry("brand", "")
if brand.lower() == "generic":
continue
if brand not in brand_group_dict:
brand_group_dict[brand] = {}
# Add material types we haven't seen yet to the dict
material_type = container_node.getMetaDataEntry("material", "")
if material_type not in brand_group_dict[brand]:
brand_group_dict[brand][material_type] = []
# Now handle the individual materials
item = self._createMaterialItem(root_material_id, container_node)
brand_group_dict[brand][material_type].append(item)
# Part 2: Organize the tree into models
#
# Normally, the structure of the menu looks like this:
# Brand -> Material Type -> Specific Material
#
# To illustrate, a branded material menu may look like this:
# Ultimaker ┳ PLA ┳ Yellow PLA
# ┃ ┣ Black PLA
# ┃ ┗ ...
# ┃
# ┗ ABS ┳ White ABS
# ┗ ...
for brand, material_dict in brand_group_dict.items():
material_type_item_list = []
brand_item = {
"name": brand,
"material_types": MaterialTypesModel(self)
}
for material_type, material_list in material_dict.items():
material_type_item = {
"name": material_type,
"brand": brand,
"colors": BaseMaterialsModel(self)
}
material_type_item["colors"].clear()
# Sort materials by name
material_list = sorted(material_list, key = lambda x: x["name"].upper())
material_type_item["colors"].setItems(material_list)
material_type_item_list.append(material_type_item)
# Sort material type by name
material_type_item_list = sorted(material_type_item_list, key = lambda x: x["name"].upper())
brand_item["material_types"].setItems(material_type_item_list)
brand_item_list.append(brand_item)
# Sort brand by name
brand_item_list = sorted(brand_item_list, key = lambda x: x["name"].upper())
self.setItems(brand_item_list)

View file

@ -1,104 +0,0 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
#
# This model is for the Material management page.
#
class MaterialManagementModel(ListModel):
RootMaterialIdRole = Qt.UserRole + 1
DisplayNameRole = Qt.UserRole + 2
BrandRole = Qt.UserRole + 3
MaterialTypeRole = Qt.UserRole + 4
ColorNameRole = Qt.UserRole + 5
ColorCodeRole = Qt.UserRole + 6
ContainerNodeRole = Qt.UserRole + 7
ContainerIdRole = Qt.UserRole + 8
DescriptionRole = Qt.UserRole + 9
AdhesionInfoRole = Qt.UserRole + 10
ApproximateDiameterRole = Qt.UserRole + 11
GuidRole = Qt.UserRole + 12
DensityRole = Qt.UserRole + 13
DiameterRole = Qt.UserRole + 14
IsReadOnlyRole = Qt.UserRole + 15
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(self.RootMaterialIdRole, "root_material_id")
self.addRoleName(self.DisplayNameRole, "name")
self.addRoleName(self.BrandRole, "brand")
self.addRoleName(self.MaterialTypeRole, "material")
self.addRoleName(self.ColorNameRole, "color_name")
self.addRoleName(self.ColorCodeRole, "color_code")
self.addRoleName(self.ContainerNodeRole, "container_node")
self.addRoleName(self.ContainerIdRole, "container_id")
self.addRoleName(self.DescriptionRole, "description")
self.addRoleName(self.AdhesionInfoRole, "adhesion_info")
self.addRoleName(self.ApproximateDiameterRole, "approximate_diameter")
self.addRoleName(self.GuidRole, "guid")
self.addRoleName(self.DensityRole, "density")
self.addRoleName(self.DiameterRole, "diameter")
self.addRoleName(self.IsReadOnlyRole, "is_read_only")
from cura.CuraApplication import CuraApplication
self._container_registry = CuraApplication.getInstance().getContainerRegistry()
self._machine_manager = CuraApplication.getInstance().getMachineManager()
self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
self._material_manager = CuraApplication.getInstance().getMaterialManager()
self._machine_manager.globalContainerChanged.connect(self._update)
self._extruder_manager.activeExtruderChanged.connect(self._update)
self._material_manager.materialsUpdated.connect(self._update)
self._update()
def _update(self):
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
global_stack = self._machine_manager.activeMachine
if global_stack is None:
self.setItems([])
return
active_extruder_stack = self._machine_manager.activeStack
available_material_dict = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack,
active_extruder_stack)
if available_material_dict is None:
self.setItems([])
return
material_list = []
for root_material_id, container_node in available_material_dict.items():
keys_to_fetch = ("name",
"brand",
"material",
"color_name",
"color_code",
"description",
"adhesion_info",
"approximate_diameter",)
item = {"root_material_id": container_node.metadata["base_file"],
"container_node": container_node,
"guid": container_node.metadata["GUID"],
"container_id": container_node.metadata["id"],
"density": container_node.metadata.get("properties", {}).get("density", ""),
"diameter": container_node.metadata.get("properties", {}).get("diameter", ""),
"is_read_only": self._container_registry.isReadOnly(container_node.metadata["id"]),
}
for key in keys_to_fetch:
item[key] = container_node.metadata.get(key, "")
material_list.append(item)
material_list = sorted(material_list, key = lambda k: (k["brand"].upper(), k["name"].upper()))
self.setItems(material_list)

View file

@ -6,10 +6,10 @@ from PyQt5.QtCore import Qt
from UM.Application import Application from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
from UM.Settings.SettingFunction import SettingFunction
from cura.Machines.QualityManager import QualityGroup from cura.Machines.QualityManager import QualityGroup
# #
# QML Model for all built-in quality profiles. This model is used for the drop-down quality menu. # QML Model for all built-in quality profiles. This model is used for the drop-down quality menu.
# #
@ -106,4 +106,8 @@ class QualityProfilesDropDownMenuModel(ListModel):
container = global_stack.definition container = global_stack.definition
if container and container.hasProperty("layer_height", "value"): if container and container.hasProperty("layer_height", "value"):
layer_height = container.getProperty("layer_height", "value") layer_height = container.getProperty("layer_height", "value")
if isinstance(layer_height, SettingFunction):
layer_height = layer_height(global_stack)
return float(layer_height) return float(layer_height)

View file

@ -58,7 +58,7 @@ class SettingVisibilityPresetsModel(ListModel):
break break
return result return result
def _populate(self): def _populate(self) -> None:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
items = [] items = []
for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset): for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
@ -147,7 +147,7 @@ class SettingVisibilityPresetsModel(ListModel):
def activePreset(self) -> str: def activePreset(self) -> str:
return self._active_preset_item["id"] return self._active_preset_item["id"]
def _onPreferencesChanged(self, name: str): def _onPreferencesChanged(self, name: str) -> None:
if name != "general/visible_settings": if name != "general/visible_settings":
return return

View file

@ -17,16 +17,16 @@ class QualityChangesGroup(QualityGroup):
super().__init__(name, quality_type, parent) super().__init__(name, quality_type, parent)
self._container_registry = Application.getInstance().getContainerRegistry() self._container_registry = Application.getInstance().getContainerRegistry()
def addNode(self, node: "QualityNode"): def addNode(self, node: "QualityNode") -> None:
extruder_position = node.getMetaDataEntry("position") extruder_position = node.getMetaDataEntry("position")
if extruder_position is None and self.node_for_global is not None or extruder_position in self.nodes_for_extruders: #We would be overwriting another node. if extruder_position is None and self.node_for_global is not None or extruder_position in self.nodes_for_extruders: #We would be overwriting another node.
ConfigurationErrorMessage.getInstance().addFaultyContainers(node.getMetaDataEntry("id")) ConfigurationErrorMessage.getInstance().addFaultyContainers(node.getMetaDataEntry("id"))
return return
if extruder_position is None: #Then we're a global quality changes profile. if extruder_position is None: # Then we're a global quality changes profile.
self.node_for_global = node self.node_for_global = node
else: #This is an extruder's quality changes profile. else: # This is an extruder's quality changes profile.
self.nodes_for_extruders[extruder_position] = node self.nodes_for_extruders[extruder_position] = node
def __str__(self) -> str: def __str__(self) -> str:

View file

@ -6,6 +6,7 @@ from typing import Dict, Optional, List, Set
from PyQt5.QtCore import QObject, pyqtSlot from PyQt5.QtCore import QObject, pyqtSlot
from cura.Machines.ContainerNode import ContainerNode from cura.Machines.ContainerNode import ContainerNode
# #
# A QualityGroup represents a group of containers that must be applied to each ContainerStack when it's used. # A QualityGroup represents a group of containers that must be applied to each ContainerStack when it's used.
# Some concrete examples are Quality and QualityChanges: when we select quality type "normal", this quality type # Some concrete examples are Quality and QualityChanges: when we select quality type "normal", this quality type
@ -34,7 +35,7 @@ class QualityGroup(QObject):
return self.name return self.name
def getAllKeys(self) -> Set[str]: def getAllKeys(self) -> Set[str]:
result = set() #type: Set[str] result = set() # type: Set[str]
for node in [self.node_for_global] + list(self.nodes_for_extruders.values()): for node in [self.node_for_global] + list(self.nodes_for_extruders.values()):
if node is None: if node is None:
continue continue

View file

@ -1,7 +1,7 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 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 typing import TYPE_CHECKING, Optional, cast from typing import TYPE_CHECKING, Optional, cast, Dict, List
from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot
@ -20,6 +20,8 @@ if TYPE_CHECKING:
from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.DefinitionContainer import DefinitionContainer
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from .QualityChangesGroup import QualityChangesGroup from .QualityChangesGroup import QualityChangesGroup
from cura.CuraApplication import CuraApplication
from UM.Settings.ContainerRegistry import ContainerRegistry
# #
@ -36,17 +38,20 @@ class QualityManager(QObject):
qualitiesUpdated = pyqtSignal() qualitiesUpdated = pyqtSignal()
def __init__(self, container_registry, parent = None): def __init__(self, container_registry: "ContainerRegistry", parent = None) -> None:
super().__init__(parent) super().__init__(parent)
self._application = Application.getInstance() self._application = Application.getInstance() # type: CuraApplication
self._material_manager = self._application.getMaterialManager() self._material_manager = self._application.getMaterialManager()
self._container_registry = container_registry self._container_registry = container_registry
self._empty_quality_container = self._application.empty_quality_container self._empty_quality_container = self._application.empty_quality_container
self._empty_quality_changes_container = self._application.empty_quality_changes_container self._empty_quality_changes_container = self._application.empty_quality_changes_container
self._machine_nozzle_buildplate_material_quality_type_to_quality_dict = {} # for quality lookup # For quality lookup
self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup self._machine_nozzle_buildplate_material_quality_type_to_quality_dict = {} # type: Dict[str, QualityNode]
# For quality_changes lookup
self._machine_quality_type_to_quality_changes_dict = {} # type: Dict[str, QualityNode]
self._default_machine_definition_id = "fdmprinter" self._default_machine_definition_id = "fdmprinter"
@ -62,7 +67,7 @@ class QualityManager(QObject):
self._update_timer.setSingleShot(True) self._update_timer.setSingleShot(True)
self._update_timer.timeout.connect(self._updateMaps) self._update_timer.timeout.connect(self._updateMaps)
def initialize(self): def initialize(self) -> None:
# Initialize the lookup tree for quality profiles with following structure: # Initialize the lookup tree for quality profiles with following structure:
# <machine> -> <nozzle> -> <buildplate> -> <material> # <machine> -> <nozzle> -> <buildplate> -> <material>
# <machine> -> <material> # <machine> -> <material>
@ -133,13 +138,13 @@ class QualityManager(QObject):
Logger.log("d", "Lookup tables updated.") Logger.log("d", "Lookup tables updated.")
self.qualitiesUpdated.emit() self.qualitiesUpdated.emit()
def _updateMaps(self): def _updateMaps(self) -> None:
self.initialize() self.initialize()
def _onContainerMetadataChanged(self, container): def _onContainerMetadataChanged(self, container: InstanceContainer) -> None:
self._onContainerChanged(container) self._onContainerChanged(container)
def _onContainerChanged(self, container): def _onContainerChanged(self, container: InstanceContainer) -> None:
container_type = container.getMetaDataEntry("type") container_type = container.getMetaDataEntry("type")
if container_type not in ("quality", "quality_changes"): if container_type not in ("quality", "quality_changes"):
return return
@ -148,7 +153,7 @@ class QualityManager(QObject):
self._update_timer.start() self._update_timer.start()
# Updates the given quality groups' availabilities according to which extruders are being used/ enabled. # Updates the given quality groups' availabilities according to which extruders are being used/ enabled.
def _updateQualityGroupsAvailability(self, machine: "GlobalStack", quality_group_list): def _updateQualityGroupsAvailability(self, machine: "GlobalStack", quality_group_list) -> None:
used_extruders = set() used_extruders = set()
for i in range(machine.getProperty("machine_extruder_count", "value")): for i in range(machine.getProperty("machine_extruder_count", "value")):
if str(i) in machine.extruders and machine.extruders[str(i)].isEnabled: if str(i) in machine.extruders and machine.extruders[str(i)].isEnabled:
@ -196,32 +201,42 @@ class QualityManager(QObject):
# Whether a QualityGroup is available can be unknown via the field QualityGroup.is_available. # Whether a QualityGroup is available can be unknown via the field QualityGroup.is_available.
# For more details, see QualityGroup. # For more details, see QualityGroup.
# #
def getQualityGroups(self, machine: "GlobalStack") -> dict: def getQualityGroups(self, machine: "GlobalStack") -> Dict[str, QualityGroup]:
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition) machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
# This determines if we should only get the global qualities for the global stack and skip the global qualities for the extruder stacks # This determines if we should only get the global qualities for the global stack and skip the global qualities for the extruder stacks
has_variant_materials = parseBool(machine.getMetaDataEntry("has_variant_materials", False)) has_machine_specific_qualities = machine.getHasMachineQuality()
# To find the quality container for the GlobalStack, check in the following fall-back manner: # To find the quality container for the GlobalStack, check in the following fall-back manner:
# (1) the machine-specific node # (1) the machine-specific node
# (2) the generic node # (2) the generic node
machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id) machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id)
# Check if this machine has specific quality profiles for its extruders, if so, when looking up extruder
# qualities, we should not fall back to use the global qualities.
has_extruder_specific_qualities = False
if machine_node:
if machine_node.children_map:
has_extruder_specific_qualities = True
default_machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(self._default_machine_definition_id) default_machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(self._default_machine_definition_id)
nodes_to_check = [machine_node, default_machine_node]
nodes_to_check = [] # type: List[QualityNode]
if machine_node is not None:
nodes_to_check.append(machine_node)
if default_machine_node is not None:
nodes_to_check.append(default_machine_node)
# Iterate over all quality_types in the machine node # Iterate over all quality_types in the machine node
quality_group_dict = {} quality_group_dict = {}
for node in nodes_to_check: for node in nodes_to_check:
if node and node.quality_type_map: if node and node.quality_type_map:
# Only include global qualities quality_node = list(node.quality_type_map.values())[0]
if has_variant_materials: is_global_quality = parseBool(quality_node.getMetaDataEntry("global_quality", False))
quality_node = list(node.quality_type_map.values())[0] if not is_global_quality:
is_global_quality = parseBool(quality_node.metadata.get("global_quality", False)) continue
if not is_global_quality:
continue
for quality_type, quality_node in node.quality_type_map.items(): for quality_type, quality_node in node.quality_type_map.items():
quality_group = QualityGroup(quality_node.metadata["name"], quality_type) quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
quality_group.node_for_global = quality_node quality_group.node_for_global = quality_node
quality_group_dict[quality_type] = quality_group quality_group_dict[quality_type] = quality_group
break break
@ -259,18 +274,25 @@ class QualityManager(QObject):
# 2. machine-nozzle-and-material-specific qualities if exist # 2. machine-nozzle-and-material-specific qualities if exist
# 3. machine-nozzle-specific qualities if exist # 3. machine-nozzle-specific qualities if exist
# 4. machine-material-specific qualities if exist # 4. machine-material-specific qualities if exist
# 5. machine-specific qualities if exist # 5. machine-specific global qualities if exist, otherwise generic global qualities
# 6. generic qualities if exist # NOTE: We DO NOT fail back to generic global qualities if machine-specific global qualities exist.
# This is because when a machine defines its own global qualities such as Normal, Fine, etc.,
# it is intended to maintain those specific qualities ONLY. If we still fail back to the generic
# global qualities, there can be unimplemented quality types e.g. "coarse", and this is not
# correct.
# Each points above can be represented as a node in the lookup tree, so here we simply put those nodes into # Each points above can be represented as a node in the lookup tree, so here we simply put those nodes into
# the list with priorities as the order. Later, we just need to loop over each node in this list and fetch # the list with priorities as the order. Later, we just need to loop over each node in this list and fetch
# qualities from there. # qualities from there.
node_info_list_0 = [nozzle_name, buildplate_name, root_material_id] node_info_list_0 = [nozzle_name, buildplate_name, root_material_id] # type: List[Optional[str]]
nodes_to_check = [] nodes_to_check = []
# This function tries to recursively find the deepest (the most specific) branch and add those nodes to # This function tries to recursively find the deepest (the most specific) branch and add those nodes to
# the search list in the order described above. So, by iterating over that search node list, we first look # the search list in the order described above. So, by iterating over that search node list, we first look
# in the more specific branches and then the less specific (generic) ones. # in the more specific branches and then the less specific (generic) ones.
def addNodesToCheck(node, nodes_to_check_list, node_info_list, node_info_idx): def addNodesToCheck(node: Optional[QualityNode], nodes_to_check_list: List[QualityNode], node_info_list, node_info_idx: int) -> None:
if node is None:
return
if node_info_idx < len(node_info_list): if node_info_idx < len(node_info_list):
node_name = node_info_list[node_info_idx] node_name = node_info_list[node_info_idx]
if node_name is not None: if node_name is not None:
@ -289,31 +311,44 @@ class QualityManager(QObject):
addNodesToCheck(machine_node, nodes_to_check, node_info_list_0, 0) addNodesToCheck(machine_node, nodes_to_check, node_info_list_0, 0)
nodes_to_check += [machine_node, default_machine_node] # The last fall back will be the global qualities (either from the machine-specific node or the generic
for node in nodes_to_check: # node), but we only use one. For details see the overview comments above.
if machine_node is not None and machine_node.quality_type_map:
nodes_to_check += [machine_node]
elif default_machine_node is not None:
nodes_to_check += [default_machine_node]
for node_idx, node in enumerate(nodes_to_check):
if node and node.quality_type_map: if node and node.quality_type_map:
if has_variant_materials: if has_extruder_specific_qualities:
# Only include variant qualities; skip non global qualities # Only include variant qualities; skip non global qualities
quality_node = list(node.quality_type_map.values())[0] quality_node = list(node.quality_type_map.values())[0]
is_global_quality = parseBool(quality_node.metadata.get("global_quality", False)) is_global_quality = parseBool(quality_node.getMetaDataEntry("global_quality", False))
if is_global_quality: if is_global_quality:
continue continue
for quality_type, quality_node in node.quality_type_map.items(): for quality_type, quality_node in node.quality_type_map.items():
if quality_type not in quality_group_dict: if quality_type not in quality_group_dict:
quality_group = QualityGroup(quality_node.metadata["name"], quality_type) quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
quality_group_dict[quality_type] = quality_group quality_group_dict[quality_type] = quality_group
quality_group = quality_group_dict[quality_type] quality_group = quality_group_dict[quality_type]
if position not in quality_group.nodes_for_extruders: if position not in quality_group.nodes_for_extruders:
quality_group.nodes_for_extruders[position] = quality_node quality_group.nodes_for_extruders[position] = quality_node
# If the machine has its own specific qualities, for extruders, it should skip the global qualities
# and use the material/variant specific qualities.
if has_extruder_specific_qualities:
if node_idx == len(nodes_to_check) - 1:
break
# Update availabilities for each quality group # Update availabilities for each quality group
self._updateQualityGroupsAvailability(machine, quality_group_dict.values()) self._updateQualityGroupsAvailability(machine, quality_group_dict.values())
return quality_group_dict return quality_group_dict
def getQualityGroupsForMachineDefinition(self, machine: "GlobalStack") -> dict: def getQualityGroupsForMachineDefinition(self, machine: "GlobalStack") -> Dict[str, QualityGroup]:
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition) machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
# To find the quality container for the GlobalStack, check in the following fall-back manner: # To find the quality container for the GlobalStack, check in the following fall-back manner:
@ -329,7 +364,7 @@ class QualityManager(QObject):
for node in nodes_to_check: for node in nodes_to_check:
if node and node.quality_type_map: if node and node.quality_type_map:
for quality_type, quality_node in node.quality_type_map.items(): for quality_type, quality_node in node.quality_type_map.items():
quality_group = QualityGroup(quality_node.metadata["name"], quality_type) quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
quality_group.node_for_global = quality_node quality_group.node_for_global = quality_node
quality_group_dict[quality_type] = quality_group quality_group_dict[quality_type] = quality_group
break break
@ -351,10 +386,21 @@ class QualityManager(QObject):
# Remove the given quality changes group. # Remove the given quality changes group.
# #
@pyqtSlot(QObject) @pyqtSlot(QObject)
def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup"): def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None:
Logger.log("i", "Removing quality changes group [%s]", quality_changes_group.name) Logger.log("i", "Removing quality changes group [%s]", quality_changes_group.name)
removed_quality_changes_ids = set()
for node in quality_changes_group.getAllNodes(): for node in quality_changes_group.getAllNodes():
self._container_registry.removeContainer(node.getMetaDataEntry("id")) container_id = node.getMetaDataEntry("id")
self._container_registry.removeContainer(container_id)
removed_quality_changes_ids.add(container_id)
# Reset all machines that have activated this quality changes to empty.
for global_stack in self._container_registry.findContainerStacks(type = "machine"):
if global_stack.qualityChanges.getId() in removed_quality_changes_ids:
global_stack.qualityChanges = self._empty_quality_changes_container
for extruder_stack in self._container_registry.findContainerStacks(type = "extruder_train"):
if extruder_stack.qualityChanges.getId() in removed_quality_changes_ids:
extruder_stack.qualityChanges = self._empty_quality_changes_container
# #
# Rename a set of quality changes containers. Returns the new name. # Rename a set of quality changes containers. Returns the new name.
@ -383,7 +429,7 @@ class QualityManager(QObject):
# Duplicates the given quality. # Duplicates the given quality.
# #
@pyqtSlot(str, "QVariantMap") @pyqtSlot(str, "QVariantMap")
def duplicateQualityChanges(self, quality_changes_name, quality_model_item): def duplicateQualityChanges(self, quality_changes_name: str, quality_model_item) -> None:
global_stack = self._application.getGlobalContainerStack() global_stack = self._application.getGlobalContainerStack()
if not global_stack: if not global_stack:
Logger.log("i", "No active global stack, cannot duplicate quality changes.") Logger.log("i", "No active global stack, cannot duplicate quality changes.")
@ -411,7 +457,7 @@ class QualityManager(QObject):
# the user containers in each stack. These then replace the quality_changes containers in the # the user containers in each stack. These then replace the quality_changes containers in the
# stack and clear the user settings. # stack and clear the user settings.
@pyqtSlot(str) @pyqtSlot(str)
def createQualityChanges(self, base_name): def createQualityChanges(self, base_name: str) -> None:
machine_manager = Application.getInstance().getMachineManager() machine_manager = Application.getInstance().getMachineManager()
global_stack = machine_manager.activeMachine global_stack = machine_manager.activeMachine

View file

@ -1,7 +1,7 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 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 typing import Optional, Dict, cast from typing import Optional, Dict, cast, Any
from .ContainerNode import ContainerNode from .ContainerNode import ContainerNode
from .QualityChangesGroup import QualityChangesGroup from .QualityChangesGroup import QualityChangesGroup
@ -12,18 +12,21 @@ from .QualityChangesGroup import QualityChangesGroup
# #
class QualityNode(ContainerNode): class QualityNode(ContainerNode):
def __init__(self, metadata: Optional[dict] = None) -> None: def __init__(self, metadata: Optional[Dict[str, Any]] = None) -> None:
super().__init__(metadata = metadata) super().__init__(metadata = metadata)
self.quality_type_map = {} # type: Dict[str, QualityNode] # quality_type -> QualityNode for InstanceContainer self.quality_type_map = {} # type: Dict[str, QualityNode] # quality_type -> QualityNode for InstanceContainer
def addQualityMetadata(self, quality_type: str, metadata: dict): def getChildNode(self, child_key: str) -> Optional["QualityNode"]:
return self.children_map.get(child_key)
def addQualityMetadata(self, quality_type: str, metadata: Dict[str, Any]):
if quality_type not in self.quality_type_map: if quality_type not in self.quality_type_map:
self.quality_type_map[quality_type] = QualityNode(metadata) self.quality_type_map[quality_type] = QualityNode(metadata)
def getQualityNode(self, quality_type: str) -> Optional["QualityNode"]: def getQualityNode(self, quality_type: str) -> Optional["QualityNode"]:
return self.quality_type_map.get(quality_type) return self.quality_type_map.get(quality_type)
def addQualityChangesMetadata(self, quality_type: str, metadata: dict): def addQualityChangesMetadata(self, quality_type: str, metadata: Dict[str, Any]):
if quality_type not in self.quality_type_map: if quality_type not in self.quality_type_map:
self.quality_type_map[quality_type] = QualityNode() self.quality_type_map[quality_type] = QualityNode()
quality_type_node = self.quality_type_map[quality_type] quality_type_node = self.quality_type_map[quality_type]

View file

@ -2,7 +2,7 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from collections import OrderedDict from collections import OrderedDict
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING, Dict
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
from UM.Logger import Logger from UM.Logger import Logger
@ -36,11 +36,11 @@ if TYPE_CHECKING:
# #
class VariantManager: class VariantManager:
def __init__(self, container_registry): def __init__(self, container_registry: ContainerRegistry) -> None:
self._container_registry = container_registry # type: ContainerRegistry self._container_registry = container_registry
self._machine_to_variant_dict_map = dict() # <machine_type> -> <variant_dict> self._machine_to_variant_dict_map = dict() # type: Dict[str, Dict["VariantType", Dict[str, ContainerNode]]]
self._machine_to_buildplate_dict_map = dict() self._machine_to_buildplate_dict_map = dict() # type: Dict[str, Dict[str, ContainerNode]]
self._exclude_variant_id_list = ["empty_variant"] self._exclude_variant_id_list = ["empty_variant"]
@ -48,7 +48,7 @@ class VariantManager:
# Initializes the VariantManager including: # Initializes the VariantManager including:
# - initializing the variant lookup table based on the metadata in ContainerRegistry. # - initializing the variant lookup table based on the metadata in ContainerRegistry.
# #
def initialize(self): def initialize(self) -> None:
self._machine_to_variant_dict_map = OrderedDict() self._machine_to_variant_dict_map = OrderedDict()
self._machine_to_buildplate_dict_map = OrderedDict() self._machine_to_buildplate_dict_map = OrderedDict()
@ -106,10 +106,10 @@ class VariantManager:
variant_node = variant_dict[variant_name] variant_node = variant_dict[variant_name]
break break
return variant_node return variant_node
return self._machine_to_variant_dict_map[machine_definition_id].get(variant_type, {}).get(variant_name) return self._machine_to_variant_dict_map[machine_definition_id].get(variant_type, {}).get(variant_name)
def getVariantNodes(self, machine: "GlobalStack", def getVariantNodes(self, machine: "GlobalStack", variant_type: "VariantType") -> Dict[str, ContainerNode]:
variant_type: Optional["VariantType"] = None) -> dict:
machine_definition_id = machine.definition.getId() machine_definition_id = machine.definition.getId()
return self._machine_to_variant_dict_map.get(machine_definition_id, {}).get(variant_type, {}) return self._machine_to_variant_dict_map.get(machine_definition_id, {}).get(variant_type, {})

View file

@ -300,7 +300,7 @@ class PrintInformation(QObject):
def _updateJobName(self): def _updateJobName(self):
if self._base_name == "": if self._base_name == "":
self._job_name = "unnamed" self._job_name = "Untitled"
self._is_user_specified_job_name = False self._is_user_specified_job_name = False
self.jobNameChanged.emit() self.jobNameChanged.emit()
return return

View file

@ -0,0 +1,27 @@
from PyQt5.QtGui import QImage
from PyQt5.QtQuick import QQuickImageProvider
from PyQt5.QtCore import QSize
from UM.Application import Application
class PrintJobPreviewImageProvider(QQuickImageProvider):
def __init__(self):
super().__init__(QQuickImageProvider.Image)
## Request a new image.
def requestImage(self, id: str, size: QSize) -> QImage:
# The id will have an uuid and an increment separated by a slash. As we don't care about the value of the
# increment, we need to strip that first.
uuid = id[id.find("/") + 1:]
for output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices():
if not hasattr(output_device, "printJobs"):
continue
for print_job in output_device.printJobs:
if print_job.key == uuid:
if print_job.getPreviewImage():
return print_job.getPreviewImage(), QSize(15, 15)
else:
return QImage(), QSize(15, 15)
return QImage(), QSize(15,15)

View file

@ -13,36 +13,42 @@ class ConfigurationModel(QObject):
configurationChanged = pyqtSignal() configurationChanged = pyqtSignal()
def __init__(self): def __init__(self) -> None:
super().__init__() super().__init__()
self._printer_type = None self._printer_type = ""
self._extruder_configurations = [] # type: List[ExtruderConfigurationModel] self._extruder_configurations = [] # type: List[ExtruderConfigurationModel]
self._buildplate_configuration = None self._buildplate_configuration = ""
def setPrinterType(self, printer_type): def setPrinterType(self, printer_type):
self._printer_type = printer_type self._printer_type = printer_type
@pyqtProperty(str, fset = setPrinterType, notify = configurationChanged) @pyqtProperty(str, fset = setPrinterType, notify = configurationChanged)
def printerType(self): def printerType(self) -> str:
return self._printer_type return self._printer_type
def setExtruderConfigurations(self, extruder_configurations): def setExtruderConfigurations(self, extruder_configurations: List["ExtruderConfigurationModel"]):
self._extruder_configurations = extruder_configurations if self._extruder_configurations != extruder_configurations:
self._extruder_configurations = extruder_configurations
for extruder_configuration in self._extruder_configurations:
extruder_configuration.extruderConfigurationChanged.connect(self.configurationChanged)
self.configurationChanged.emit()
@pyqtProperty("QVariantList", fset = setExtruderConfigurations, notify = configurationChanged) @pyqtProperty("QVariantList", fset = setExtruderConfigurations, notify = configurationChanged)
def extruderConfigurations(self): def extruderConfigurations(self):
return self._extruder_configurations return self._extruder_configurations
def setBuildplateConfiguration(self, buildplate_configuration): def setBuildplateConfiguration(self, buildplate_configuration: str) -> None:
self._buildplate_configuration = buildplate_configuration self._buildplate_configuration = buildplate_configuration
@pyqtProperty(str, fset = setBuildplateConfiguration, notify = configurationChanged) @pyqtProperty(str, fset = setBuildplateConfiguration, notify = configurationChanged)
def buildplateConfiguration(self): def buildplateConfiguration(self) -> str:
return self._buildplate_configuration return self._buildplate_configuration
## This method is intended to indicate whether the configuration is valid or not. ## This method is intended to indicate whether the configuration is valid or not.
# The method checks if the mandatory fields are or not set # The method checks if the mandatory fields are or not set
def isValid(self): def isValid(self) -> bool:
if not self._extruder_configurations: if not self._extruder_configurations:
return False return False
for configuration in self._extruder_configurations: for configuration in self._extruder_configurations:

View file

@ -1,56 +1,67 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 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 typing import Optional
from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
class ExtruderConfigurationModel(QObject): class ExtruderConfigurationModel(QObject):
extruderConfigurationChanged = pyqtSignal() extruderConfigurationChanged = pyqtSignal()
def __init__(self): def __init__(self, position: int = -1) -> None:
super().__init__() super().__init__()
self._position = -1 self._position = position # type: int
self._material = None self._material = None # type: Optional[MaterialOutputModel]
self._hotend_id = None self._hotend_id = None # type: Optional[str]
def setPosition(self, position): def setPosition(self, position: int) -> None:
self._position = position self._position = position
@pyqtProperty(int, fset = setPosition, notify = extruderConfigurationChanged) @pyqtProperty(int, fset = setPosition, notify = extruderConfigurationChanged)
def position(self): def position(self) -> int:
return self._position return self._position
def setMaterial(self, material): def setMaterial(self, material: Optional[MaterialOutputModel]) -> None:
self._material = material if self._hotend_id != material:
self._material = material
self.extruderConfigurationChanged.emit()
@pyqtProperty(QObject, fset = setMaterial, notify = extruderConfigurationChanged) @pyqtProperty(QObject, fset = setMaterial, notify = extruderConfigurationChanged)
def material(self): def activeMaterial(self) -> Optional[MaterialOutputModel]:
return self._material return self._material
def setHotendID(self, hotend_id): @pyqtProperty(QObject, fset=setMaterial, notify=extruderConfigurationChanged)
self._hotend_id = hotend_id def material(self) -> Optional[MaterialOutputModel]:
return self._material
def setHotendID(self, hotend_id: Optional[str]) -> None:
if self._hotend_id != hotend_id:
self._hotend_id = hotend_id
self.extruderConfigurationChanged.emit()
@pyqtProperty(str, fset = setHotendID, notify = extruderConfigurationChanged) @pyqtProperty(str, fset = setHotendID, notify = extruderConfigurationChanged)
def hotendID(self): def hotendID(self) -> Optional[str]:
return self._hotend_id return self._hotend_id
## This method is intended to indicate whether the configuration is valid or not. ## This method is intended to indicate whether the configuration is valid or not.
# The method checks if the mandatory fields are or not set # The method checks if the mandatory fields are or not set
# At this moment is always valid since we allow to have empty material and variants. # At this moment is always valid since we allow to have empty material and variants.
def isValid(self): def isValid(self) -> bool:
return True return True
def __str__(self): def __str__(self) -> str:
message_chunks = [] message_chunks = []
message_chunks.append("Position: " + str(self._position)) message_chunks.append("Position: " + str(self._position))
message_chunks.append("-") message_chunks.append("-")
message_chunks.append("Material: " + self.material.type if self.material else "empty") message_chunks.append("Material: " + self.activeMaterial.type if self.activeMaterial else "empty")
message_chunks.append("-") message_chunks.append("-")
message_chunks.append("HotendID: " + self.hotendID if self.hotendID else "empty") message_chunks.append("HotendID: " + self.hotendID if self.hotendID else "empty")
return " ".join(message_chunks) return " ".join(message_chunks)
def __eq__(self, other): def __eq__(self, other) -> bool:
return hash(self) == hash(other) return hash(self) == hash(other)
# Calculating a hash function using the position of the extruder, the material GUID and the hotend id to check if is # Calculating a hash function using the position of the extruder, the material GUID and the hotend id to check if is

View file

@ -12,64 +12,61 @@ if TYPE_CHECKING:
class ExtruderOutputModel(QObject): class ExtruderOutputModel(QObject):
hotendIDChanged = pyqtSignal()
targetHotendTemperatureChanged = pyqtSignal() targetHotendTemperatureChanged = pyqtSignal()
hotendTemperatureChanged = pyqtSignal() hotendTemperatureChanged = pyqtSignal()
activeMaterialChanged = pyqtSignal()
extruderConfigurationChanged = pyqtSignal() extruderConfigurationChanged = pyqtSignal()
isPreheatingChanged = pyqtSignal() isPreheatingChanged = pyqtSignal()
def __init__(self, printer: "PrinterOutputModel", position, parent=None) -> None: def __init__(self, printer: "PrinterOutputModel", position: int, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self._printer = printer self._printer = printer # type: PrinterOutputModel
self._position = position self._position = position
self._target_hotend_temperature = 0 # type: float self._target_hotend_temperature = 0.0 # type: float
self._hotend_temperature = 0 # type: float self._hotend_temperature = 0.0 # type: float
self._hotend_id = ""
self._active_material = None # type: Optional[MaterialOutputModel]
self._extruder_configuration = ExtruderConfigurationModel()
self._extruder_configuration.position = self._position
self._is_preheating = False self._is_preheating = False
def getPrinter(self): # The extruder output model wraps the configuration model. This way we can use the same config model for jobs
# and extruders alike.
self._extruder_configuration = ExtruderConfigurationModel()
self._extruder_configuration.position = self._position
self._extruder_configuration.extruderConfigurationChanged.connect(self.extruderConfigurationChanged)
def getPrinter(self) -> "PrinterOutputModel":
return self._printer return self._printer
def getPosition(self): def getPosition(self) -> int:
return self._position return self._position
# Does the printer support pre-heating the bed at all # Does the printer support pre-heating the bed at all
@pyqtProperty(bool, constant=True) @pyqtProperty(bool, constant=True)
def canPreHeatHotends(self): def canPreHeatHotends(self) -> bool:
if self._printer: if self._printer:
return self._printer.canPreHeatHotends return self._printer.canPreHeatHotends
return False return False
@pyqtProperty(QObject, notify = activeMaterialChanged) @pyqtProperty(QObject, notify = extruderConfigurationChanged)
def activeMaterial(self) -> Optional["MaterialOutputModel"]: def activeMaterial(self) -> Optional["MaterialOutputModel"]:
return self._active_material return self._extruder_configuration.activeMaterial
def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]): def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]) -> None:
if self._active_material != material: self._extruder_configuration.setMaterial(material)
self._active_material = material
self._extruder_configuration.material = self._active_material
self.activeMaterialChanged.emit()
self.extruderConfigurationChanged.emit()
## Update the hotend temperature. This only changes it locally. ## Update the hotend temperature. This only changes it locally.
def updateHotendTemperature(self, temperature: float): def updateHotendTemperature(self, temperature: float) -> None:
if self._hotend_temperature != temperature: if self._hotend_temperature != temperature:
self._hotend_temperature = temperature self._hotend_temperature = temperature
self.hotendTemperatureChanged.emit() self.hotendTemperatureChanged.emit()
def updateTargetHotendTemperature(self, temperature: float): def updateTargetHotendTemperature(self, temperature: float) -> None:
if self._target_hotend_temperature != temperature: if self._target_hotend_temperature != temperature:
self._target_hotend_temperature = temperature self._target_hotend_temperature = temperature
self.targetHotendTemperatureChanged.emit() self.targetHotendTemperatureChanged.emit()
## Set the target hotend temperature. This ensures that it's actually sent to the remote. ## Set the target hotend temperature. This ensures that it's actually sent to the remote.
@pyqtSlot(float) @pyqtSlot(float)
def setTargetHotendTemperature(self, temperature: float): def setTargetHotendTemperature(self, temperature: float) -> None:
self._printer.getController().setTargetHotendTemperature(self._printer, self, temperature) self._printer.getController().setTargetHotendTemperature(self._printer, self, temperature)
self.updateTargetHotendTemperature(temperature) self.updateTargetHotendTemperature(temperature)
@ -81,30 +78,26 @@ class ExtruderOutputModel(QObject):
def hotendTemperature(self) -> float: def hotendTemperature(self) -> float:
return self._hotend_temperature return self._hotend_temperature
@pyqtProperty(str, notify = hotendIDChanged) @pyqtProperty(str, notify = extruderConfigurationChanged)
def hotendID(self) -> str: def hotendID(self) -> str:
return self._hotend_id return self._extruder_configuration.hotendID
def updateHotendID(self, id: str): def updateHotendID(self, hotend_id: str) -> None:
if self._hotend_id != id: self._extruder_configuration.setHotendID(hotend_id)
self._hotend_id = id
self._extruder_configuration.hotendID = self._hotend_id
self.hotendIDChanged.emit()
self.extruderConfigurationChanged.emit()
@pyqtProperty(QObject, notify = extruderConfigurationChanged) @pyqtProperty(QObject, notify = extruderConfigurationChanged)
def extruderConfiguration(self): def extruderConfiguration(self) -> Optional[ExtruderConfigurationModel]:
if self._extruder_configuration.isValid(): if self._extruder_configuration.isValid():
return self._extruder_configuration return self._extruder_configuration
return None return None
def updateIsPreheating(self, pre_heating): def updateIsPreheating(self, pre_heating: bool) -> None:
if self._is_preheating != pre_heating: if self._is_preheating != pre_heating:
self._is_preheating = pre_heating self._is_preheating = pre_heating
self.isPreheatingChanged.emit() self.isPreheatingChanged.emit()
@pyqtProperty(bool, notify=isPreheatingChanged) @pyqtProperty(bool, notify=isPreheatingChanged)
def isPreheating(self): def isPreheating(self) -> bool:
return self._is_preheating return self._is_preheating
## Pre-heats the extruder before printer. ## Pre-heats the extruder before printer.
@ -113,9 +106,9 @@ class ExtruderOutputModel(QObject):
# Celsius. # Celsius.
# \param duration How long the bed should stay warm, in seconds. # \param duration How long the bed should stay warm, in seconds.
@pyqtSlot(float, float) @pyqtSlot(float, float)
def preheatHotend(self, temperature, duration): def preheatHotend(self, temperature: float, duration: float) -> None:
self._printer._controller.preheatHotend(self, temperature, duration) self._printer._controller.preheatHotend(self, temperature, duration)
@pyqtSlot() @pyqtSlot()
def cancelPreheatHotend(self): def cancelPreheatHotend(self) -> None:
self._printer._controller.cancelPreheatHotend(self) self._printer._controller.cancelPreheatHotend(self)

View file

@ -53,21 +53,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
self._sending_gcode = False self._sending_gcode = False
self._compressing_gcode = False self._compressing_gcode = False
self._gcode = [] # type: List[str] self._gcode = [] # type: List[str]
self._connection_state_before_timeout = None # type: Optional[ConnectionState] self._connection_state_before_timeout = None # type: Optional[ConnectionState]
printer_type = self._properties.get(b"machine", b"").decode("utf-8")
printer_type_identifiers = {
"9066": "ultimaker3",
"9511": "ultimaker3_extended",
"9051": "ultimaker_s5"
}
self._printer_type = "Unknown"
for key, value in printer_type_identifiers.items():
if printer_type.startswith(key):
self._printer_type = value
break
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None: def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
raise NotImplementedError("requestWrite needs to be implemented") raise NotImplementedError("requestWrite needs to be implemented")
@ -188,40 +175,55 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
if reply in self._kept_alive_multiparts: if reply in self._kept_alive_multiparts:
del self._kept_alive_multiparts[reply] del self._kept_alive_multiparts[reply]
def put(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: def _validateManager(self) -> None:
if self._manager is None: if self._manager is None:
self._createNetworkManager() self._createNetworkManager()
assert (self._manager is not None) assert (self._manager is not None)
def put(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
self._validateManager()
request = self._createEmptyRequest(target) request = self._createEmptyRequest(target)
self._last_request_time = time() self._last_request_time = time()
reply = self._manager.put(request, data.encode()) if self._manager is not None:
self._registerOnFinishedCallback(reply, on_finished) reply = self._manager.put(request, data.encode())
self._registerOnFinishedCallback(reply, on_finished)
else:
Logger.log("e", "Could not find manager.")
def delete(self, target: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
self._validateManager()
request = self._createEmptyRequest(target)
self._last_request_time = time()
if self._manager is not None:
reply = self._manager.deleteResource(request)
self._registerOnFinishedCallback(reply, on_finished)
else:
Logger.log("e", "Could not find manager.")
def get(self, target: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: def get(self, target: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
if self._manager is None: self._validateManager()
self._createNetworkManager()
assert (self._manager is not None)
request = self._createEmptyRequest(target) request = self._createEmptyRequest(target)
self._last_request_time = time() self._last_request_time = time()
reply = self._manager.get(request) if self._manager is not None:
self._registerOnFinishedCallback(reply, on_finished) reply = self._manager.get(request)
self._registerOnFinishedCallback(reply, on_finished)
else:
Logger.log("e", "Could not find manager.")
def post(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> None: def post(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> None:
if self._manager is None: self._validateManager()
self._createNetworkManager()
assert (self._manager is not None)
request = self._createEmptyRequest(target) request = self._createEmptyRequest(target)
self._last_request_time = time() self._last_request_time = time()
reply = self._manager.post(request, data) if self._manager is not None:
if on_progress is not None: reply = self._manager.post(request, data)
reply.uploadProgress.connect(on_progress) if on_progress is not None:
self._registerOnFinishedCallback(reply, on_finished) reply.uploadProgress.connect(on_progress)
self._registerOnFinishedCallback(reply, on_finished)
else:
Logger.log("e", "Could not find manager.")
def postFormWithParts(self, target: str, parts: List[QHttpPart], on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> QNetworkReply: def postFormWithParts(self, target: str, parts: List[QHttpPart], on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> QNetworkReply:
self._validateManager()
if self._manager is None:
self._createNetworkManager()
assert (self._manager is not None)
request = self._createEmptyRequest(target, content_type=None) request = self._createEmptyRequest(target, content_type=None)
multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType) multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType)
for part in parts: for part in parts:
@ -229,15 +231,18 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
self._last_request_time = time() self._last_request_time = time()
reply = self._manager.post(request, multi_post_part) if self._manager is not None:
reply = self._manager.post(request, multi_post_part)
self._kept_alive_multiparts[reply] = multi_post_part self._kept_alive_multiparts[reply] = multi_post_part
if on_progress is not None: if on_progress is not None:
reply.uploadProgress.connect(on_progress) reply.uploadProgress.connect(on_progress)
self._registerOnFinishedCallback(reply, on_finished) self._registerOnFinishedCallback(reply, on_finished)
return reply return reply
else:
Logger.log("e", "Could not find manager.")
def postForm(self, target: str, header_data: str, body_data: bytes, on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> None: def postForm(self, target: str, header_data: str, body_data: bytes, on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> None:
post_part = QHttpPart() post_part = QHttpPart()
@ -323,7 +328,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
@pyqtProperty(str, constant = True) @pyqtProperty(str, constant = True)
def printerType(self) -> str: def printerType(self) -> str:
return self._printer_type return self._properties.get(b"printer_type", b"Unknown").decode("utf-8")
## IP adress of this printer ## IP adress of this printer
@pyqtProperty(str, constant = True) @pyqtProperty(str, constant = True)

View file

@ -2,11 +2,15 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING, List
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QImage
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
class PrintJobOutputModel(QObject): class PrintJobOutputModel(QObject):
@ -17,6 +21,9 @@ class PrintJobOutputModel(QObject):
keyChanged = pyqtSignal() keyChanged = pyqtSignal()
assignedPrinterChanged = pyqtSignal() assignedPrinterChanged = pyqtSignal()
ownerChanged = pyqtSignal() ownerChanged = pyqtSignal()
configurationChanged = pyqtSignal()
previewImageChanged = pyqtSignal()
compatibleMachineFamiliesChanged = pyqtSignal()
def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent=None) -> None: def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent=None) -> None:
super().__init__(parent) super().__init__(parent)
@ -29,6 +36,48 @@ class PrintJobOutputModel(QObject):
self._assigned_printer = None # type: Optional[PrinterOutputModel] self._assigned_printer = None # type: Optional[PrinterOutputModel]
self._owner = "" # Who started/owns the print job? self._owner = "" # Who started/owns the print job?
self._configuration = None # type: Optional[ConfigurationModel]
self._compatible_machine_families = [] # type: List[str]
self._preview_image_id = 0
self._preview_image = None # type: Optional[QImage]
@pyqtProperty("QStringList", notify=compatibleMachineFamiliesChanged)
def compatibleMachineFamilies(self):
# Hack; Some versions of cluster will return a family more than once...
return set(self._compatible_machine_families)
def setCompatibleMachineFamilies(self, compatible_machine_families: List[str]) -> None:
if self._compatible_machine_families != compatible_machine_families:
self._compatible_machine_families = compatible_machine_families
self.compatibleMachineFamiliesChanged.emit()
@pyqtProperty(QUrl, notify=previewImageChanged)
def previewImageUrl(self):
self._preview_image_id += 1
# There is an image provider that is called "camera". In order to ensure that the image qml object, that
# requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
# as new (instead of relying on cached version and thus forces an update.
temp = "image://print_job_preview/" + str(self._preview_image_id) + "/" + self._key
return QUrl(temp, QUrl.TolerantMode)
def getPreviewImage(self) -> Optional[QImage]:
return self._preview_image
def updatePreviewImage(self, preview_image: Optional[QImage]) -> None:
if self._preview_image != preview_image:
self._preview_image = preview_image
self.previewImageChanged.emit()
@pyqtProperty(QObject, notify=configurationChanged)
def configuration(self) -> Optional["ConfigurationModel"]:
return self._configuration
def updateConfiguration(self, configuration: Optional["ConfigurationModel"]) -> None:
if self._configuration != configuration:
self._configuration = configuration
self.configurationChanged.emit()
@pyqtProperty(str, notify=ownerChanged) @pyqtProperty(str, notify=ownerChanged)
def owner(self): def owner(self):
return self._owner return self._owner

View file

@ -37,7 +37,7 @@ class PrinterOutputModel(QObject):
self._controller = output_controller self._controller = output_controller
self._controller.canUpdateFirmwareChanged.connect(self._onControllerCanUpdateFirmwareChanged) self._controller.canUpdateFirmwareChanged.connect(self._onControllerCanUpdateFirmwareChanged)
self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)] self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)]
self._printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer self._printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer
self._head_position = Vector(0, 0, 0) self._head_position = Vector(0, 0, 0)
self._active_print_job = None # type: Optional[PrintJobOutputModel] self._active_print_job = None # type: Optional[PrintJobOutputModel]
self._firmware_version = firmware_version self._firmware_version = firmware_version
@ -45,9 +45,9 @@ class PrinterOutputModel(QObject):
self._is_preheating = False self._is_preheating = False
self._printer_type = "" self._printer_type = ""
self._buildplate_name = None self._buildplate_name = None
# Update the printer configuration every time any of the extruders changes its configuration
for extruder in self._extruders: self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in
extruder.extruderConfigurationChanged.connect(self._updateExtruderConfiguration) self._extruders]
self._camera = None self._camera = None
@ -295,8 +295,4 @@ class PrinterOutputModel(QObject):
def printerConfiguration(self): def printerConfiguration(self):
if self._printer_configuration.isValid(): if self._printer_configuration.isValid():
return self._printer_configuration return self._printer_configuration
return None return None
def _updateExtruderConfiguration(self):
self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in self._extruders]
self.configurationChanged.emit()

View file

@ -13,23 +13,31 @@ from cura.Scene import ConvexHullNode
import numpy import numpy
from typing import TYPE_CHECKING, Any, Optional
if TYPE_CHECKING:
from UM.Scene.SceneNode import SceneNode
from cura.Settings.GlobalStack import GlobalStack
## The convex hull decorator is a scene node decorator that adds the convex hull functionality to a scene node. ## The convex hull decorator is a scene node decorator that adds the convex hull functionality to a scene node.
# If a scene node has a convex hull decorator, it will have a shadow in which other objects can not be printed. # If a scene node has a convex hull decorator, it will have a shadow in which other objects can not be printed.
class ConvexHullDecorator(SceneNodeDecorator): class ConvexHullDecorator(SceneNodeDecorator):
def __init__(self): def __init__(self) -> None:
super().__init__() super().__init__()
self._convex_hull_node = None self._convex_hull_node = None # type: Optional["SceneNode"]
self._init2DConvexHullCache() self._init2DConvexHullCache()
self._global_stack = None self._global_stack = None # type: Optional[GlobalStack]
# Make sure the timer is created on the main thread # Make sure the timer is created on the main thread
self._recompute_convex_hull_timer = None self._recompute_convex_hull_timer = None # type: Optional[QTimer]
Application.getInstance().callLater(self.createRecomputeConvexHullTimer)
if Application.getInstance() is not None:
Application.getInstance().callLater(self.createRecomputeConvexHullTimer)
self._raft_thickness = 0.0 self._raft_thickness = 0.0
# For raft thickness, DRY
self._build_volume = Application.getInstance().getBuildVolume() self._build_volume = Application.getInstance().getBuildVolume()
self._build_volume.raftThicknessChanged.connect(self._onChanged) self._build_volume.raftThicknessChanged.connect(self._onChanged)
@ -39,13 +47,13 @@ class ConvexHullDecorator(SceneNodeDecorator):
self._onGlobalStackChanged() self._onGlobalStackChanged()
def createRecomputeConvexHullTimer(self): def createRecomputeConvexHullTimer(self) -> None:
self._recompute_convex_hull_timer = QTimer() self._recompute_convex_hull_timer = QTimer()
self._recompute_convex_hull_timer.setInterval(200) self._recompute_convex_hull_timer.setInterval(200)
self._recompute_convex_hull_timer.setSingleShot(True) self._recompute_convex_hull_timer.setSingleShot(True)
self._recompute_convex_hull_timer.timeout.connect(self.recomputeConvexHull) self._recompute_convex_hull_timer.timeout.connect(self.recomputeConvexHull)
def setNode(self, node): def setNode(self, node: "SceneNode") -> None:
previous_node = self._node previous_node = self._node
# Disconnect from previous node signals # Disconnect from previous node signals
if previous_node is not None and node is not previous_node: if previous_node is not None and node is not previous_node:
@ -63,14 +71,14 @@ class ConvexHullDecorator(SceneNodeDecorator):
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
return ConvexHullDecorator() return ConvexHullDecorator()
## Get the unmodified 2D projected convex hull of the node ## Get the unmodified 2D projected convex hull of the node (if any)
def getConvexHull(self): def getConvexHull(self) -> Optional[Polygon]:
if self._node is None: if self._node is None:
return None return None
hull = self._compute2DConvexHull() hull = self._compute2DConvexHull()
if self._global_stack and self._node: if self._global_stack and self._node and hull is not None:
# Parent can be None if node is just loaded. # Parent can be None if node is just loaded.
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and (self._node.getParent() is None or not self._node.getParent().callDecoration("isGroup")): if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and (self._node.getParent() is None or not self._node.getParent().callDecoration("isGroup")):
hull = hull.getMinkowskiHull(Polygon(numpy.array(self._global_stack.getProperty("machine_head_polygon", "value"), numpy.float32))) hull = hull.getMinkowskiHull(Polygon(numpy.array(self._global_stack.getProperty("machine_head_polygon", "value"), numpy.float32)))
@ -78,7 +86,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
return hull return hull
## Get the convex hull of the node with the full head size ## Get the convex hull of the node with the full head size
def getConvexHullHeadFull(self): def getConvexHullHeadFull(self) -> Optional[Polygon]:
if self._node is None: if self._node is None:
return None return None
@ -87,7 +95,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
## Get convex hull of the object + head size ## Get convex hull of the object + head size
# In case of printing all at once this is the same as the convex hull. # In case of printing all at once this is the same as the convex hull.
# For one at the time this is area with intersection of mirrored head # For one at the time this is area with intersection of mirrored head
def getConvexHullHead(self): def getConvexHullHead(self) -> Optional[Polygon]:
if self._node is None: if self._node is None:
return None return None
@ -101,7 +109,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
## Get convex hull of the node ## Get convex hull of the node
# In case of printing all at once this is the same as the convex hull. # In case of printing all at once this is the same as the convex hull.
# For one at the time this is the area without the head. # For one at the time this is the area without the head.
def getConvexHullBoundary(self): def getConvexHullBoundary(self) -> Optional[Polygon]:
if self._node is None: if self._node is None:
return None return None
@ -111,13 +119,14 @@ class ConvexHullDecorator(SceneNodeDecorator):
return self._compute2DConvexHull() return self._compute2DConvexHull()
return None return None
def recomputeConvexHullDelayed(self): ## The same as recomputeConvexHull, but using a timer if it was set.
def recomputeConvexHullDelayed(self) -> None:
if self._recompute_convex_hull_timer is not None: if self._recompute_convex_hull_timer is not None:
self._recompute_convex_hull_timer.start() self._recompute_convex_hull_timer.start()
else: else:
self.recomputeConvexHull() self.recomputeConvexHull()
def recomputeConvexHull(self): def recomputeConvexHull(self) -> None:
controller = Application.getInstance().getController() controller = Application.getInstance().getController()
root = controller.getScene().getRoot() root = controller.getScene().getRoot()
if self._node is None or controller.isToolOperationActive() or not self.__isDescendant(root, self._node): if self._node is None or controller.isToolOperationActive() or not self.__isDescendant(root, self._node):
@ -132,17 +141,17 @@ class ConvexHullDecorator(SceneNodeDecorator):
hull_node = ConvexHullNode.ConvexHullNode(self._node, convex_hull, self._raft_thickness, root) hull_node = ConvexHullNode.ConvexHullNode(self._node, convex_hull, self._raft_thickness, root)
self._convex_hull_node = hull_node self._convex_hull_node = hull_node
def _onSettingValueChanged(self, key, property_name): def _onSettingValueChanged(self, key: str, property_name: str) -> None:
if property_name != "value": #Not the value that was changed. if property_name != "value": # Not the value that was changed.
return return
if key in self._affected_settings: if key in self._affected_settings:
self._onChanged() self._onChanged()
if key in self._influencing_settings: if key in self._influencing_settings:
self._init2DConvexHullCache() #Invalidate the cache. self._init2DConvexHullCache() # Invalidate the cache.
self._onChanged() self._onChanged()
def _init2DConvexHullCache(self): def _init2DConvexHullCache(self) -> None:
# Cache for the group code path in _compute2DConvexHull() # Cache for the group code path in _compute2DConvexHull()
self._2d_convex_hull_group_child_polygon = None self._2d_convex_hull_group_child_polygon = None
self._2d_convex_hull_group_result = None self._2d_convex_hull_group_result = None
@ -152,7 +161,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
self._2d_convex_hull_mesh_world_transform = None self._2d_convex_hull_mesh_world_transform = None
self._2d_convex_hull_mesh_result = None self._2d_convex_hull_mesh_result = None
def _compute2DConvexHull(self): def _compute2DConvexHull(self) -> Optional[Polygon]:
if self._node.callDecoration("isGroup"): if self._node.callDecoration("isGroup"):
points = numpy.zeros((0, 2), dtype=numpy.int32) points = numpy.zeros((0, 2), dtype=numpy.int32)
for child in self._node.getChildren(): for child in self._node.getChildren():
@ -179,8 +188,6 @@ class ConvexHullDecorator(SceneNodeDecorator):
else: else:
offset_hull = None offset_hull = None
mesh = None
world_transform = None
if self._node.getMeshData(): if self._node.getMeshData():
mesh = self._node.getMeshData() mesh = self._node.getMeshData()
world_transform = self._node.getWorldTransformation() world_transform = self._node.getWorldTransformation()
@ -228,24 +235,33 @@ class ConvexHullDecorator(SceneNodeDecorator):
return offset_hull return offset_hull
def _getHeadAndFans(self): def _getHeadAndFans(self) -> Polygon:
return Polygon(numpy.array(self._global_stack.getHeadAndFansCoordinates(), numpy.float32)) if self._global_stack:
return Polygon(numpy.array(self._global_stack.getHeadAndFansCoordinates(), numpy.float32))
return Polygon()
def _compute2DConvexHeadFull(self): def _compute2DConvexHeadFull(self) -> Optional[Polygon]:
return self._compute2DConvexHull().getMinkowskiHull(self._getHeadAndFans()) convex_hull = self._compute2DConvexHeadFull()
if convex_hull:
return convex_hull.getMinkowskiHull(self._getHeadAndFans())
return None
def _compute2DConvexHeadMin(self): def _compute2DConvexHeadMin(self) -> Optional[Polygon]:
headAndFans = self._getHeadAndFans() head_and_fans = self._getHeadAndFans()
mirrored = headAndFans.mirror([0, 0], [0, 1]).mirror([0, 0], [1, 0]) # Mirror horizontally & vertically. mirrored = head_and_fans.mirror([0, 0], [0, 1]).mirror([0, 0], [1, 0]) # Mirror horizontally & vertically.
head_and_fans = self._getHeadAndFans().intersectionConvexHulls(mirrored) head_and_fans = self._getHeadAndFans().intersectionConvexHulls(mirrored)
# Min head hull is used for the push free # Min head hull is used for the push free
min_head_hull = self._compute2DConvexHull().getMinkowskiHull(head_and_fans) convex_hull = self._compute2DConvexHeadFull()
return min_head_hull if convex_hull:
return convex_hull.getMinkowskiHull(head_and_fans)
return None
## Compensate given 2D polygon with adhesion margin ## Compensate given 2D polygon with adhesion margin
# \return 2D polygon with added margin # \return 2D polygon with added margin
def _add2DAdhesionMargin(self, poly): def _add2DAdhesionMargin(self, poly: Polygon) -> Polygon:
if not self._global_stack:
return Polygon()
# Compensate for raft/skirt/brim # Compensate for raft/skirt/brim
# Add extra margin depending on adhesion type # Add extra margin depending on adhesion type
adhesion_type = self._global_stack.getProperty("adhesion_type", "value") adhesion_type = self._global_stack.getProperty("adhesion_type", "value")
@ -263,7 +279,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
else: else:
raise Exception("Unknown bed adhesion type. Did you forget to update the convex hull calculations for your new bed adhesion type?") raise Exception("Unknown bed adhesion type. Did you forget to update the convex hull calculations for your new bed adhesion type?")
# adjust head_and_fans with extra margin # Adjust head_and_fans with extra margin
if extra_margin > 0: if extra_margin > 0:
extra_margin_polygon = Polygon.approximatedCircle(extra_margin) extra_margin_polygon = Polygon.approximatedCircle(extra_margin)
poly = poly.getMinkowskiHull(extra_margin_polygon) poly = poly.getMinkowskiHull(extra_margin_polygon)
@ -274,7 +290,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
# \param convex_hull Polygon of the original convex hull. # \param convex_hull Polygon of the original convex hull.
# \return New Polygon instance that is offset with everything that # \return New Polygon instance that is offset with everything that
# influences the collision area. # influences the collision area.
def _offsetHull(self, convex_hull): def _offsetHull(self, convex_hull: Polygon) -> Polygon:
horizontal_expansion = max( horizontal_expansion = max(
self._getSettingProperty("xy_offset", "value"), self._getSettingProperty("xy_offset", "value"),
self._getSettingProperty("xy_offset_layer_0", "value") self._getSettingProperty("xy_offset_layer_0", "value")
@ -295,16 +311,16 @@ class ConvexHullDecorator(SceneNodeDecorator):
else: else:
return convex_hull return convex_hull
def _onChanged(self, *args): def _onChanged(self, *args) -> None:
self._raft_thickness = self._build_volume.getRaftThickness() self._raft_thickness = self._build_volume.getRaftThickness()
if not args or args[0] == self._node: if not args or args[0] == self._node:
self.recomputeConvexHullDelayed() self.recomputeConvexHullDelayed()
def _onGlobalStackChanged(self): def _onGlobalStackChanged(self) -> None:
if self._global_stack: if self._global_stack:
self._global_stack.propertyChanged.disconnect(self._onSettingValueChanged) self._global_stack.propertyChanged.disconnect(self._onSettingValueChanged)
self._global_stack.containersChanged.disconnect(self._onChanged) self._global_stack.containersChanged.disconnect(self._onChanged)
extruders = ExtruderManager.getInstance().getMachineExtruders(self._global_stack.getId()) extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
for extruder in extruders: for extruder in extruders:
extruder.propertyChanged.disconnect(self._onSettingValueChanged) extruder.propertyChanged.disconnect(self._onSettingValueChanged)
@ -314,14 +330,16 @@ class ConvexHullDecorator(SceneNodeDecorator):
self._global_stack.propertyChanged.connect(self._onSettingValueChanged) self._global_stack.propertyChanged.connect(self._onSettingValueChanged)
self._global_stack.containersChanged.connect(self._onChanged) self._global_stack.containersChanged.connect(self._onChanged)
extruders = ExtruderManager.getInstance().getMachineExtruders(self._global_stack.getId()) extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
for extruder in extruders: for extruder in extruders:
extruder.propertyChanged.connect(self._onSettingValueChanged) extruder.propertyChanged.connect(self._onSettingValueChanged)
self._onChanged() self._onChanged()
## Private convenience function to get a setting from the correct extruder (as defined by limit_to_extruder property). ## Private convenience function to get a setting from the correct extruder (as defined by limit_to_extruder property).
def _getSettingProperty(self, setting_key, prop = "value"): def _getSettingProperty(self, setting_key: str, prop: str = "value") -> Any:
if not self._global_stack:
return None
per_mesh_stack = self._node.callDecoration("getStack") per_mesh_stack = self._node.callDecoration("getStack")
if per_mesh_stack: if per_mesh_stack:
return per_mesh_stack.getProperty(setting_key, prop) return per_mesh_stack.getProperty(setting_key, prop)
@ -339,8 +357,8 @@ class ConvexHullDecorator(SceneNodeDecorator):
# Limit_to_extruder is set. The global stack handles this then # Limit_to_extruder is set. The global stack handles this then
return self._global_stack.getProperty(setting_key, prop) return self._global_stack.getProperty(setting_key, prop)
## Returns true if node is a descendant or the same as the root node. ## Returns True if node is a descendant or the same as the root node.
def __isDescendant(self, root, node): def __isDescendant(self, root: "SceneNode", node: "SceneNode") -> bool:
if node is None: if node is None:
return False return False
if root is node: if root is node:

View file

@ -1,13 +1,19 @@
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
from typing import List
class GCodeListDecorator(SceneNodeDecorator): class GCodeListDecorator(SceneNodeDecorator):
def __init__(self): def __init__(self) -> None:
super().__init__() super().__init__()
self._gcode_list = [] self._gcode_list = [] # type: List[str]
def getGCodeList(self): def getGCodeList(self) -> List[str]:
return self._gcode_list return self._gcode_list
def setGCodeList(self, list): def setGCodeList(self, list: List[str]):
self._gcode_list = list self._gcode_list = list
def __deepcopy__(self, memo) -> "GCodeListDecorator":
copied_decorator = GCodeListDecorator()
copied_decorator.setGCodeList(self.getGCodeList())
return copied_decorator

View file

@ -2,11 +2,11 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
class SliceableObjectDecorator(SceneNodeDecorator): class SliceableObjectDecorator(SceneNodeDecorator):
def __init__(self): def __init__(self) -> None:
super().__init__() super().__init__()
def isSliceable(self): def isSliceable(self) -> bool:
return True return True
def __deepcopy__(self, memo): def __deepcopy__(self, memo) -> "SliceableObjectDecorator":
return type(self)() return type(self)()

View file

@ -1,18 +1,19 @@
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
## A decorator that stores the amount an object has been moved below the platform. ## A decorator that stores the amount an object has been moved below the platform.
class ZOffsetDecorator(SceneNodeDecorator): class ZOffsetDecorator(SceneNodeDecorator):
def __init__(self): def __init__(self) -> None:
super().__init__() super().__init__()
self._z_offset = 0 self._z_offset = 0.
def setZOffset(self, offset): def setZOffset(self, offset: float) -> None:
self._z_offset = offset self._z_offset = offset
def getZOffset(self): def getZOffset(self) -> float:
return self._z_offset return self._z_offset
def __deepcopy__(self, memo): def __deepcopy__(self, memo) -> "ZOffsetDecorator":
copied_decorator = ZOffsetDecorator() copied_decorator = ZOffsetDecorator()
copied_decorator.setZOffset(self.getZOffset()) copied_decorator.setZOffset(self.getZOffset())
return copied_decorator return copied_decorator

View file

@ -4,12 +4,12 @@
import os import os
import urllib.parse import urllib.parse
import uuid import uuid
from typing import Any from typing import Dict, Union, Any, TYPE_CHECKING, List
from typing import Dict, Union, Optional
from PyQt5.QtCore import QObject, QUrl, QVariant from PyQt5.QtCore import QObject, QUrl
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.FlameProfiler import pyqtSlot from UM.FlameProfiler import pyqtSlot
from UM.Logger import Logger from UM.Logger import Logger
@ -21,6 +21,18 @@ from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
from cura.Machines.ContainerNode import ContainerNode
from cura.Machines.MaterialNode import MaterialNode
from cura.Machines.QualityChangesGroup import QualityChangesGroup
from UM.PluginRegistry import PluginRegistry
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.Settings.MachineManager import MachineManager
from cura.Machines.MaterialManager import MaterialManager
from cura.Machines.QualityManager import QualityManager
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
@ -31,20 +43,20 @@ catalog = i18nCatalog("cura")
# when a certain action happens. This can be done through this class. # when a certain action happens. This can be done through this class.
class ContainerManager(QObject): class ContainerManager(QObject):
def __init__(self, application): def __init__(self, application: "CuraApplication") -> None:
if ContainerManager.__instance is not None: if ContainerManager.__instance is not None:
raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__) raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__)
ContainerManager.__instance = self ContainerManager.__instance = self
super().__init__(parent = application) super().__init__(parent = application)
self._application = application self._application = application # type: CuraApplication
self._plugin_registry = self._application.getPluginRegistry() self._plugin_registry = self._application.getPluginRegistry() # type: PluginRegistry
self._container_registry = self._application.getContainerRegistry() self._container_registry = self._application.getContainerRegistry() # type: ContainerRegistry
self._machine_manager = self._application.getMachineManager() self._machine_manager = self._application.getMachineManager() # type: MachineManager
self._material_manager = self._application.getMaterialManager() self._material_manager = self._application.getMaterialManager() # type: MaterialManager
self._quality_manager = self._application.getQualityManager() self._quality_manager = self._application.getQualityManager() # type: QualityManager
self._container_name_filters = {} # type: Dict[str, Dict[str, Any]] self._container_name_filters = {} # type: Dict[str, Dict[str, Any]]
@pyqtSlot(str, str, result=str) @pyqtSlot(str, str, result=str)
def getContainerMetaDataEntry(self, container_id: str, entry_names: str) -> str: def getContainerMetaDataEntry(self, container_id: str, entry_names: str) -> str:
@ -69,21 +81,23 @@ class ContainerManager(QObject):
# by using "/" as a separator. For example, to change an entry "foo" in a # by using "/" as a separator. For example, to change an entry "foo" in a
# dictionary entry "bar", you can specify "bar/foo" as entry name. # dictionary entry "bar", you can specify "bar/foo" as entry name.
# #
# \param container_id \type{str} The ID of the container to change. # \param container_node \type{ContainerNode}
# \param entry_name \type{str} The name of the metadata entry to change. # \param entry_name \type{str} The name of the metadata entry to change.
# \param entry_value The new value of the entry. # \param entry_value The new value of the entry.
# #
# \return True if successful, False if not.
# TODO: This is ONLY used by MaterialView for material containers. Maybe refactor this. # TODO: This is ONLY used by MaterialView for material containers. Maybe refactor this.
# Update: In order for QML to use objects and sub objects, those (sub) objects must all be QObject. Is that what we want? # Update: In order for QML to use objects and sub objects, those (sub) objects must all be QObject. Is that what we want?
@pyqtSlot("QVariant", str, str) @pyqtSlot("QVariant", str, str)
def setContainerMetaDataEntry(self, container_node, entry_name, entry_value): def setContainerMetaDataEntry(self, container_node: "ContainerNode", entry_name: str, entry_value: str) -> bool:
root_material_id = container_node.metadata["base_file"] root_material_id = container_node.getMetaDataEntry("base_file", "")
if self._container_registry.isReadOnly(root_material_id): if self._container_registry.isReadOnly(root_material_id):
Logger.log("w", "Cannot set metadata of read-only container %s.", root_material_id) Logger.log("w", "Cannot set metadata of read-only container %s.", root_material_id)
return False return False
material_group = self._material_manager.getMaterialGroup(root_material_id) material_group = self._material_manager.getMaterialGroup(root_material_id)
if material_group is None:
Logger.log("w", "Unable to find material group for: %s.", root_material_id)
return False
entries = entry_name.split("/") entries = entry_name.split("/")
entry_name = entries.pop() entry_name = entries.pop()
@ -91,11 +105,11 @@ class ContainerManager(QObject):
sub_item_changed = False sub_item_changed = False
if entries: if entries:
root_name = entries.pop(0) root_name = entries.pop(0)
root = material_group.root_material_node.metadata.get(root_name) root = material_group.root_material_node.getMetaDataEntry(root_name)
item = root item = root
for _ in range(len(entries)): for _ in range(len(entries)):
item = item.get(entries.pop(0), { }) item = item.get(entries.pop(0), {})
if item[entry_name] != entry_value: if item[entry_name] != entry_value:
sub_item_changed = True sub_item_changed = True
@ -109,9 +123,10 @@ class ContainerManager(QObject):
container.setMetaDataEntry(entry_name, entry_value) container.setMetaDataEntry(entry_name, entry_value)
if sub_item_changed: #If it was only a sub-item that has changed then the setMetaDataEntry won't correctly notice that something changed, and we must manually signal that the metadata changed. if sub_item_changed: #If it was only a sub-item that has changed then the setMetaDataEntry won't correctly notice that something changed, and we must manually signal that the metadata changed.
container.metaDataChanged.emit(container) container.metaDataChanged.emit(container)
return True
@pyqtSlot(str, result = str) @pyqtSlot(str, result = str)
def makeUniqueName(self, original_name): def makeUniqueName(self, original_name: str) -> str:
return self._container_registry.uniqueName(original_name) return self._container_registry.uniqueName(original_name)
## Get a list of string that can be used as name filters for a Qt File Dialog ## Get a list of string that can be used as name filters for a Qt File Dialog
@ -125,7 +140,7 @@ class ContainerManager(QObject):
# #
# \return A string list with name filters. # \return A string list with name filters.
@pyqtSlot(str, result = "QStringList") @pyqtSlot(str, result = "QStringList")
def getContainerNameFilters(self, type_name): def getContainerNameFilters(self, type_name: str) -> List[str]:
if not self._container_name_filters: if not self._container_name_filters:
self._updateContainerNameFilters() self._updateContainerNameFilters()
@ -257,7 +272,7 @@ class ContainerManager(QObject):
# #
# \return \type{bool} True if successful, False if not. # \return \type{bool} True if successful, False if not.
@pyqtSlot(result = bool) @pyqtSlot(result = bool)
def updateQualityChanges(self): def updateQualityChanges(self) -> bool:
global_stack = self._machine_manager.activeMachine global_stack = self._machine_manager.activeMachine
if not global_stack: if not global_stack:
return False return False
@ -313,10 +328,10 @@ class ContainerManager(QObject):
# \param material_id \type{str} the id of the material for which to get the linked materials. # \param material_id \type{str} the id of the material for which to get the linked materials.
# \return \type{list} a list of names of materials with the same GUID # \return \type{list} a list of names of materials with the same GUID
@pyqtSlot("QVariant", bool, result = "QStringList") @pyqtSlot("QVariant", bool, result = "QStringList")
def getLinkedMaterials(self, material_node, exclude_self = False): def getLinkedMaterials(self, material_node: "MaterialNode", exclude_self: bool = False):
guid = material_node.metadata["GUID"] guid = material_node.getMetaDataEntry("GUID", "")
self_root_material_id = material_node.metadata["base_file"] self_root_material_id = material_node.getMetaDataEntry("base_file")
material_group_list = self._material_manager.getMaterialGroupListByGUID(guid) material_group_list = self._material_manager.getMaterialGroupListByGUID(guid)
linked_material_names = [] linked_material_names = []
@ -324,15 +339,19 @@ class ContainerManager(QObject):
for material_group in material_group_list: for material_group in material_group_list:
if exclude_self and material_group.name == self_root_material_id: if exclude_self and material_group.name == self_root_material_id:
continue continue
linked_material_names.append(material_group.root_material_node.metadata["name"]) linked_material_names.append(material_group.root_material_node.getMetaDataEntry("name", ""))
return linked_material_names return linked_material_names
## Unlink a material from all other materials by creating a new GUID ## Unlink a material from all other materials by creating a new GUID
# \param material_id \type{str} the id of the material to create a new GUID for. # \param material_id \type{str} the id of the material to create a new GUID for.
@pyqtSlot("QVariant") @pyqtSlot("QVariant")
def unlinkMaterial(self, material_node): def unlinkMaterial(self, material_node: "MaterialNode") -> None:
# Get the material group # Get the material group
material_group = self._material_manager.getMaterialGroup(material_node.metadata["base_file"]) material_group = self._material_manager.getMaterialGroup(material_node.getMetaDataEntry("base_file", ""))
if material_group is None:
Logger.log("w", "Unable to find material group for %s", material_node)
return
# Generate a new GUID # Generate a new GUID
new_guid = str(uuid.uuid4()) new_guid = str(uuid.uuid4())
@ -344,7 +363,7 @@ class ContainerManager(QObject):
if container is not None: if container is not None:
container.setMetaDataEntry("GUID", new_guid) container.setMetaDataEntry("GUID", new_guid)
def _performMerge(self, merge_into, merge, clear_settings = True): def _performMerge(self, merge_into: InstanceContainer, merge: InstanceContainer, clear_settings: bool = True) -> None:
if merge == merge_into: if merge == merge_into:
return return
@ -400,7 +419,7 @@ class ContainerManager(QObject):
## Import single profile, file_url does not have to end with curaprofile ## Import single profile, file_url does not have to end with curaprofile
@pyqtSlot(QUrl, result="QVariantMap") @pyqtSlot(QUrl, result="QVariantMap")
def importProfile(self, file_url): def importProfile(self, file_url: QUrl):
if not file_url.isValid(): if not file_url.isValid():
return return
path = file_url.toLocalFile() path = file_url.toLocalFile()
@ -409,7 +428,7 @@ class ContainerManager(QObject):
return self._container_registry.importProfile(path) return self._container_registry.importProfile(path)
@pyqtSlot(QObject, QUrl, str) @pyqtSlot(QObject, QUrl, str)
def exportQualityChangesGroup(self, quality_changes_group, file_url: QUrl, file_type: str): def exportQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", file_url: QUrl, file_type: str) -> None:
if not file_url.isValid(): if not file_url.isValid():
return return
path = file_url.toLocalFile() path = file_url.toLocalFile()

View file

@ -291,7 +291,7 @@ class CuraContainerStack(ContainerStack):
# Helper to make sure we emit a PyQt signal on container changes. # Helper to make sure we emit a PyQt signal on container changes.
def _onContainersChanged(self, container: Any) -> None: def _onContainersChanged(self, container: Any) -> None:
self.pyqtContainersChanged.emit() Application.getInstance().callLater(self.pyqtContainersChanged.emit)
# Helper that can be overridden to get the "machine" definition, that is, the definition that defines the machine # Helper that can be overridden to get the "machine" definition, that is, the definition that defines the machine
# and its properties rather than, for example, the extruder. Defaults to simply returning the definition property. # and its properties rather than, for example, the extruder. Defaults to simply returning the definition property.

View file

@ -15,6 +15,7 @@ from .ExtruderStack import ExtruderStack
## Contains helper functions to create new machines. ## Contains helper functions to create new machines.
class CuraStackBuilder: class CuraStackBuilder:
## Create a new instance of a machine. ## Create a new instance of a machine.
# #
# \param name The name of the new machine. # \param name The name of the new machine.
@ -26,7 +27,6 @@ class CuraStackBuilder:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
application = CuraApplication.getInstance() application = CuraApplication.getInstance()
variant_manager = application.getVariantManager() variant_manager = application.getVariantManager()
material_manager = application.getMaterialManager()
quality_manager = application.getQualityManager() quality_manager = application.getQualityManager()
registry = application.getContainerRegistry() registry = application.getContainerRegistry()
@ -46,16 +46,6 @@ class CuraStackBuilder:
if not global_variant_container: if not global_variant_container:
global_variant_container = application.empty_variant_container global_variant_container = application.empty_variant_container
# get variant container for extruders
extruder_variant_container = application.empty_variant_container
extruder_variant_node = variant_manager.getDefaultVariantNode(machine_definition, VariantType.NOZZLE)
extruder_variant_name = None
if extruder_variant_node:
extruder_variant_container = extruder_variant_node.getContainer()
if not extruder_variant_container:
extruder_variant_container = application.empty_variant_container
extruder_variant_name = extruder_variant_container.getName()
generated_name = registry.createUniqueName("machine", "", name, machine_definition.getName()) generated_name = registry.createUniqueName("machine", "", name, machine_definition.getName())
# Make sure the new name does not collide with any definition or (quality) profile # Make sure the new name does not collide with any definition or (quality) profile
# createUniqueName() only looks at other stacks, but not at definitions or quality profiles # createUniqueName() only looks at other stacks, but not at definitions or quality profiles
@ -74,34 +64,8 @@ class CuraStackBuilder:
# Create ExtruderStacks # Create ExtruderStacks
extruder_dict = machine_definition.getMetaDataEntry("machine_extruder_trains") extruder_dict = machine_definition.getMetaDataEntry("machine_extruder_trains")
for position in extruder_dict:
for position, extruder_definition_id in extruder_dict.items(): cls.createExtruderStackWithDefaultSetup(new_global_stack, position)
# Sanity check: make sure that the positions in the extruder definitions are same as in the machine
# definition
extruder_definition = registry.findDefinitionContainers(id = extruder_definition_id)[0]
position_in_extruder_def = extruder_definition.getMetaDataEntry("position")
if position_in_extruder_def != position:
ConfigurationErrorMessage.getInstance().addFaultyContainers(extruder_definition_id)
return None #Don't return any container stack then, not the rest of the extruders either.
# get material container for extruders
material_container = application.empty_material_container
material_node = material_manager.getDefaultMaterial(new_global_stack, position, extruder_variant_name, extruder_definition = extruder_definition)
if material_node and material_node.getContainer():
material_container = material_node.getContainer()
new_extruder_id = registry.uniqueName(extruder_definition_id)
new_extruder = cls.createExtruderStack(
new_extruder_id,
extruder_definition = extruder_definition,
machine_definition_id = definition_id,
position = position,
variant_container = extruder_variant_container,
material_container = material_container,
quality_container = application.empty_quality_container
)
new_extruder.setNextStack(new_global_stack)
new_global_stack.addExtruder(new_extruder)
for new_extruder in new_global_stack.extruders.values(): #Only register the extruders if we're sure that all of them are correct. for new_extruder in new_global_stack.extruders.values(): #Only register the extruders if we're sure that all of them are correct.
registry.addContainer(new_extruder) registry.addContainer(new_extruder)
@ -136,19 +100,73 @@ class CuraStackBuilder:
return new_global_stack return new_global_stack
## Create a default Extruder Stack
#
# \param global_stack The global stack this extruder refers to.
# \param extruder_position The position of the current extruder.
@classmethod
def createExtruderStackWithDefaultSetup(cls, global_stack: "GlobalStack", extruder_position: int) -> None:
from cura.CuraApplication import CuraApplication
application = CuraApplication.getInstance()
variant_manager = application.getVariantManager()
material_manager = application.getMaterialManager()
registry = application.getContainerRegistry()
# get variant container for extruders
extruder_variant_container = application.empty_variant_container
extruder_variant_node = variant_manager.getDefaultVariantNode(global_stack.definition, VariantType.NOZZLE)
extruder_variant_name = None
if extruder_variant_node:
extruder_variant_container = extruder_variant_node.getContainer()
if not extruder_variant_container:
extruder_variant_container = application.empty_variant_container
extruder_variant_name = extruder_variant_container.getName()
extruder_definition_dict = global_stack.getMetaDataEntry("machine_extruder_trains")
extruder_definition_id = extruder_definition_dict[str(extruder_position)]
extruder_definition = registry.findDefinitionContainers(id = extruder_definition_id)[0]
# get material container for extruders
material_container = application.empty_material_container
material_node = material_manager.getDefaultMaterial(global_stack, extruder_position, extruder_variant_name,
extruder_definition = extruder_definition)
if material_node and material_node.getContainer():
material_container = material_node.getContainer()
new_extruder_id = registry.uniqueName(extruder_definition_id)
new_extruder = cls.createExtruderStack(
new_extruder_id,
extruder_definition = extruder_definition,
machine_definition_id = global_stack.definition.getId(),
position = extruder_position,
variant_container = extruder_variant_container,
material_container = material_container,
quality_container = application.empty_quality_container
)
new_extruder.setNextStack(global_stack)
global_stack.addExtruder(new_extruder)
registry.addContainer(new_extruder)
## Create a new Extruder stack ## Create a new Extruder stack
# #
# \param new_stack_id The ID of the new stack. # \param new_stack_id The ID of the new stack.
# \param definition The definition to base the new stack on. # \param extruder_definition The definition to base the new stack on.
# \param machine_definition_id The ID of the machine definition to use for # \param machine_definition_id The ID of the machine definition to use for the user container.
# the user container. # \param position The position the extruder occupies in the machine.
# \param kwargs You can add keyword arguments to specify IDs of containers to use for a specific type, for example "variant": "0.4mm" # \param variant_container The variant selected for the current extruder.
# \param material_container The material selected for the current extruder.
# \param quality_container The quality selected for the current extruder.
# #
# \return A new Global stack instance with the specified parameters. # \return A new Extruder stack instance with the specified parameters.
@classmethod @classmethod
def createExtruderStack(cls, new_stack_id: str, extruder_definition: DefinitionContainerInterface, machine_definition_id: str, def createExtruderStack(cls, new_stack_id: str, extruder_definition: DefinitionContainerInterface,
machine_definition_id: str,
position: int, position: int,
variant_container, material_container, quality_container) -> ExtruderStack: variant_container: "InstanceContainer",
material_container: "InstanceContainer",
quality_container: "InstanceContainer") -> ExtruderStack:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
application = CuraApplication.getInstance() application = CuraApplication.getInstance()
registry = application.getContainerRegistry() registry = application.getContainerRegistry()
@ -157,7 +175,7 @@ class CuraStackBuilder:
stack.setName(extruder_definition.getName()) stack.setName(extruder_definition.getName())
stack.setDefinition(extruder_definition) stack.setDefinition(extruder_definition)
stack.setMetaDataEntry("position", position) stack.setMetaDataEntry("position", str(position))
user_container = cls.createUserChangesContainer(new_stack_id + "_user", machine_definition_id, new_stack_id, user_container = cls.createUserChangesContainer(new_stack_id + "_user", machine_definition_id, new_stack_id,
is_global_stack = False) is_global_stack = False)
@ -183,9 +201,22 @@ class CuraStackBuilder:
# \param kwargs You can add keyword arguments to specify IDs of containers to use for a specific type, for example "variant": "0.4mm" # \param kwargs You can add keyword arguments to specify IDs of containers to use for a specific type, for example "variant": "0.4mm"
# #
# \return A new Global stack instance with the specified parameters. # \return A new Global stack instance with the specified parameters.
## Create a new Global stack
#
# \param new_stack_id The ID of the new stack.
# \param definition The definition to base the new stack on.
# \param variant_container The variant selected for the current stack.
# \param material_container The material selected for the current stack.
# \param quality_container The quality selected for the current stack.
#
# \return A new Global stack instance with the specified parameters.
@classmethod @classmethod
def createGlobalStack(cls, new_stack_id: str, definition: DefinitionContainerInterface, def createGlobalStack(cls, new_stack_id: str, definition: DefinitionContainerInterface,
variant_container, material_container, quality_container) -> GlobalStack: variant_container: "InstanceContainer",
material_container: "InstanceContainer",
quality_container: "InstanceContainer") -> GlobalStack:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
application = CuraApplication.getInstance() application = CuraApplication.getInstance()
registry = application.getContainerRegistry() registry = application.getContainerRegistry()

View file

@ -4,7 +4,8 @@
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt. from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt.
from UM.FlameProfiler import pyqtSlot from UM.FlameProfiler import pyqtSlot
import cura.CuraApplication #To get the global container stack to find the current machine. import cura.CuraApplication # To get the global container stack to find the current machine.
from cura.Settings.GlobalStack import GlobalStack
from UM.Logger import Logger from UM.Logger import Logger
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
@ -12,15 +13,13 @@ from UM.Scene.Selection import Selection
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID. from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID.
from UM.Settings.SettingFunction import SettingFunction from UM.Settings.SettingFunction import SettingFunction
from UM.Settings.SettingInstance import SettingInstance
from UM.Settings.ContainerStack import ContainerStack from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
from typing import Optional, List, TYPE_CHECKING, Union, Dict from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.Settings.ExtruderStack import ExtruderStack from cura.Settings.ExtruderStack import ExtruderStack
from cura.Settings.GlobalStack import GlobalStack
## Manages all existing extruder stacks. ## Manages all existing extruder stacks.
@ -38,9 +37,13 @@ class ExtruderManager(QObject):
self._application = cura.CuraApplication.CuraApplication.getInstance() self._application = cura.CuraApplication.CuraApplication.getInstance()
self._extruder_trains = {} # Per machine, a dictionary of extruder container stack IDs. Only for separately defined extruders. # Per machine, a dictionary of extruder container stack IDs. Only for separately defined extruders.
self._extruder_trains = {} # type: Dict[str, Dict[str, "ExtruderStack"]]
self._active_extruder_index = -1 # Indicates the index of the active extruder stack. -1 means no active extruder stack self._active_extruder_index = -1 # Indicates the index of the active extruder stack. -1 means no active extruder stack
self._selected_object_extruders = []
# TODO; I have no idea why this is a union of ID's and extruder stacks. This needs to be fixed at some point.
self._selected_object_extruders = [] # type: List[Union[str, "ExtruderStack"]]
self._addCurrentMachineExtruders() self._addCurrentMachineExtruders()
Selection.selectionChanged.connect(self.resetSelectedObjectExtruders) Selection.selectionChanged.connect(self.resetSelectedObjectExtruders)
@ -68,7 +71,7 @@ class ExtruderManager(QObject):
## Return extruder count according to extruder trains. ## Return extruder count according to extruder trains.
@pyqtProperty(int, notify = extrudersChanged) @pyqtProperty(int, notify = extrudersChanged)
def extruderCount(self): def extruderCount(self) -> int:
if not self._application.getGlobalContainerStack(): if not self._application.getGlobalContainerStack():
return 0 # No active machine, so no extruders. return 0 # No active machine, so no extruders.
try: try:
@ -79,28 +82,14 @@ class ExtruderManager(QObject):
## Gets a dict with the extruder stack ids with the extruder number as the key. ## Gets a dict with the extruder stack ids with the extruder number as the key.
@pyqtProperty("QVariantMap", notify = extrudersChanged) @pyqtProperty("QVariantMap", notify = extrudersChanged)
def extruderIds(self) -> Dict[str, str]: def extruderIds(self) -> Dict[str, str]:
extruder_stack_ids = {} extruder_stack_ids = {} # type: Dict[str, str]
global_container_stack = self._application.getGlobalContainerStack() global_container_stack = self._application.getGlobalContainerStack()
if global_container_stack: if global_container_stack:
global_stack_id = global_container_stack.getId() extruder_stack_ids = {position: extruder.id for position, extruder in global_container_stack.extruders.items()}
if global_stack_id in self._extruder_trains:
for position in self._extruder_trains[global_stack_id]:
extruder_stack_ids[position] = self._extruder_trains[global_stack_id][position].getId()
return extruder_stack_ids return extruder_stack_ids
@pyqtSlot(str, result = str)
def getQualityChangesIdByExtruderStackId(self, extruder_stack_id: str) -> str:
global_container_stack = self._application.getGlobalContainerStack()
if global_container_stack is not None:
for position in self._extruder_trains[global_container_stack.getId()]:
extruder = self._extruder_trains[global_container_stack.getId()][position]
if extruder.getId() == extruder_stack_id:
return extruder.qualityChanges.getId()
return ""
## Changes the active extruder by index. ## Changes the active extruder by index.
# #
# \param index The index of the new active extruder. # \param index The index of the new active extruder.
@ -117,9 +106,9 @@ class ExtruderManager(QObject):
# #
# \param index The index of the extruder whose name to get. # \param index The index of the extruder whose name to get.
@pyqtSlot(int, result = str) @pyqtSlot(int, result = str)
def getExtruderName(self, index): def getExtruderName(self, index: int) -> str:
try: try:
return list(self.getActiveExtruderStacks())[index].getName() return self.getActiveExtruderStacks()[index].getName()
except IndexError: except IndexError:
return "" return ""
@ -128,12 +117,12 @@ class ExtruderManager(QObject):
## Provides a list of extruder IDs used by the current selected objects. ## Provides a list of extruder IDs used by the current selected objects.
@pyqtProperty("QVariantList", notify = selectedObjectExtrudersChanged) @pyqtProperty("QVariantList", notify = selectedObjectExtrudersChanged)
def selectedObjectExtruders(self) -> List[str]: def selectedObjectExtruders(self) -> List[Union[str, "ExtruderStack"]]:
if not self._selected_object_extruders: if not self._selected_object_extruders:
object_extruders = set() object_extruders = set()
# First, build a list of the actual selected objects (including children of groups, excluding group nodes) # First, build a list of the actual selected objects (including children of groups, excluding group nodes)
selected_nodes = [] selected_nodes = [] # type: List["SceneNode"]
for node in Selection.getAllSelectedObjects(): for node in Selection.getAllSelectedObjects():
if node.callDecoration("isGroup"): if node.callDecoration("isGroup"):
for grouped_node in BreadthFirstIterator(node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. for grouped_node in BreadthFirstIterator(node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
@ -145,16 +134,15 @@ class ExtruderManager(QObject):
selected_nodes.append(node) selected_nodes.append(node)
# Then, figure out which nodes are used by those selected nodes. # Then, figure out which nodes are used by those selected nodes.
global_stack = self._application.getGlobalContainerStack() current_extruder_trains = self.getActiveExtruderStacks()
current_extruder_trains = self._extruder_trains.get(global_stack.getId())
for node in selected_nodes: for node in selected_nodes:
extruder = node.callDecoration("getActiveExtruder") extruder = node.callDecoration("getActiveExtruder")
if extruder: if extruder:
object_extruders.add(extruder) object_extruders.add(extruder)
elif current_extruder_trains: elif current_extruder_trains:
object_extruders.add(current_extruder_trains["0"].getId()) object_extruders.add(current_extruder_trains[0].getId())
self._selected_object_extruders = list(object_extruders) self._selected_object_extruders = list(object_extruders) # type: List[Union[str, "ExtruderStack"]]
return self._selected_object_extruders return self._selected_object_extruders
@ -163,19 +151,12 @@ class ExtruderManager(QObject):
# This will trigger a recalculation of the extruders used for the # This will trigger a recalculation of the extruders used for the
# selection. # selection.
def resetSelectedObjectExtruders(self) -> None: def resetSelectedObjectExtruders(self) -> None:
self._selected_object_extruders = [] self._selected_object_extruders = [] # type: List[Union[str, "ExtruderStack"]]
self.selectedObjectExtrudersChanged.emit() self.selectedObjectExtrudersChanged.emit()
@pyqtSlot(result = QObject) @pyqtSlot(result = QObject)
def getActiveExtruderStack(self) -> Optional["ExtruderStack"]: def getActiveExtruderStack(self) -> Optional["ExtruderStack"]:
global_container_stack = self._application.getGlobalContainerStack() return self.getExtruderStack(self._active_extruder_index)
if global_container_stack:
if global_container_stack.getId() in self._extruder_trains:
if str(self._active_extruder_index) in self._extruder_trains[global_container_stack.getId()]:
return self._extruder_trains[global_container_stack.getId()][str(self._active_extruder_index)]
return None
## Get an extruder stack by index ## Get an extruder stack by index
def getExtruderStack(self, index) -> Optional["ExtruderStack"]: def getExtruderStack(self, index) -> Optional["ExtruderStack"]:
@ -186,16 +167,7 @@ class ExtruderManager(QObject):
return self._extruder_trains[global_container_stack.getId()][str(index)] return self._extruder_trains[global_container_stack.getId()][str(index)]
return None return None
## Get all extruder stacks def registerExtruder(self, extruder_train: "ExtruderStack", machine_id: str) -> None:
def getExtruderStacks(self) -> List["ExtruderStack"]:
result = []
for i in range(self.extruderCount):
stack = self.getExtruderStack(i)
if stack:
result.append(stack)
return result
def registerExtruder(self, extruder_train, machine_id):
changed = False changed = False
if machine_id not in self._extruder_trains: if machine_id not in self._extruder_trains:
@ -214,23 +186,20 @@ class ExtruderManager(QObject):
if changed: if changed:
self.extrudersChanged.emit(machine_id) self.extrudersChanged.emit(machine_id)
def getAllExtruderValues(self, setting_key):
return self.getAllExtruderSettings(setting_key, "value")
## Gets a property of a setting for all extruders. ## Gets a property of a setting for all extruders.
# #
# \param setting_key \type{str} The setting to get the property of. # \param setting_key \type{str} The setting to get the property of.
# \param property \type{str} The property to get. # \param property \type{str} The property to get.
# \return \type{List} the list of results # \return \type{List} the list of results
def getAllExtruderSettings(self, setting_key: str, prop: str): def getAllExtruderSettings(self, setting_key: str, prop: str) -> List:
result = [] result = []
for index in self.extruderIds:
extruder_stack_id = self.extruderIds[str(index)] for extruder_stack in self.getActiveExtruderStacks():
extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
result.append(extruder_stack.getProperty(setting_key, prop)) result.append(extruder_stack.getProperty(setting_key, prop))
return result return result
def extruderValueWithDefault(self, value): def extruderValueWithDefault(self, value: str) -> str:
machine_manager = self._application.getMachineManager() machine_manager = self._application.getMachineManager()
if value == "-1": if value == "-1":
return machine_manager.defaultExtruderPosition return machine_manager.defaultExtruderPosition
@ -321,7 +290,7 @@ class ExtruderManager(QObject):
## Removes the container stack and user profile for the extruders for a specific machine. ## Removes the container stack and user profile for the extruders for a specific machine.
# #
# \param machine_id The machine to remove the extruders for. # \param machine_id The machine to remove the extruders for.
def removeMachineExtruders(self, machine_id: str): def removeMachineExtruders(self, machine_id: str) -> None:
for extruder in self.getMachineExtruders(machine_id): for extruder in self.getMachineExtruders(machine_id):
ContainerRegistry.getInstance().removeContainer(extruder.userChanges.getId()) ContainerRegistry.getInstance().removeContainer(extruder.userChanges.getId())
ContainerRegistry.getInstance().removeContainer(extruder.getId()) ContainerRegistry.getInstance().removeContainer(extruder.getId())
@ -331,24 +300,11 @@ class ExtruderManager(QObject):
## Returns extruders for a specific machine. ## Returns extruders for a specific machine.
# #
# \param machine_id The machine to get the extruders of. # \param machine_id The machine to get the extruders of.
def getMachineExtruders(self, machine_id: str): def getMachineExtruders(self, machine_id: str) -> List["ExtruderStack"]:
if machine_id not in self._extruder_trains: if machine_id not in self._extruder_trains:
return [] return []
return [self._extruder_trains[machine_id][name] for name in self._extruder_trains[machine_id]] return [self._extruder_trains[machine_id][name] for name in self._extruder_trains[machine_id]]
## Returns a list containing the global stack and active extruder stacks.
#
# The first element is the global container stack, followed by any extruder stacks.
# \return \type{List[ContainerStack]}
def getActiveGlobalAndExtruderStacks(self) -> Optional[List[Union["ExtruderStack", "GlobalStack"]]]:
global_stack = self._application.getGlobalContainerStack()
if not global_stack:
return None
result = [global_stack]
result.extend(self.getActiveExtruderStacks())
return result
## Returns the list of active extruder stacks, taking into account the machine extruder count. ## Returns the list of active extruder stacks, taking into account the machine extruder count.
# #
# \return \type{List[ContainerStack]} a list of # \return \type{List[ContainerStack]} a list of
@ -357,14 +313,11 @@ class ExtruderManager(QObject):
if not global_stack: if not global_stack:
return [] return []
result = [] result_tuple_list = sorted(list(global_stack.extruders.items()), key = lambda x: int(x[0]))
if global_stack.getId() in self._extruder_trains: result_list = [item[1] for item in result_tuple_list]
for extruder in sorted(self._extruder_trains[global_stack.getId()]):
result.append(self._extruder_trains[global_stack.getId()][extruder])
machine_extruder_count = global_stack.getProperty("machine_extruder_count", "value") machine_extruder_count = global_stack.getProperty("machine_extruder_count", "value")
return result_list[:machine_extruder_count]
return result[:machine_extruder_count]
def _globalContainerStackChanged(self) -> None: def _globalContainerStackChanged(self) -> None:
# If the global container changed, the machine changed and might have extruders that were not registered yet # If the global container changed, the machine changed and might have extruders that were not registered yet
@ -406,10 +359,17 @@ class ExtruderManager(QObject):
# After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing # After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing
# "fdmextruder". We need to check a machine here so its extruder definition is correct according to this. # "fdmextruder". We need to check a machine here so its extruder definition is correct according to this.
def _fixSingleExtrusionMachineExtruderDefinition(self, global_stack): def _fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None:
expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["0"] expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["0"]
extruder_stack_0 = global_stack.extruders["0"] extruder_stack_0 = global_stack.extruders.get("0")
if extruder_stack_0.definition.getId() != expected_extruder_definition_0_id:
if extruder_stack_0 is None:
Logger.log("i", "No extruder stack for global stack [%s], create one", global_stack.getId())
# Single extrusion machine without an ExtruderStack, create it
from cura.Settings.CuraStackBuilder import CuraStackBuilder
CuraStackBuilder.createExtruderStackWithDefaultSetup(global_stack, 0)
elif extruder_stack_0.definition.getId() != expected_extruder_definition_0_id:
Logger.log("e", "Single extruder printer [{printer}] expected extruder [{expected}], but got [{got}]. I'm making it [{expected}].".format( Logger.log("e", "Single extruder printer [{printer}] expected extruder [{expected}], but got [{got}]. I'm making it [{expected}].".format(
printer = global_stack.getId(), expected = expected_extruder_definition_0_id, got = extruder_stack_0.definition.getId())) printer = global_stack.getId(), expected = expected_extruder_definition_0_id, got = extruder_stack_0.definition.getId()))
container_registry = ContainerRegistry.getInstance() container_registry = ContainerRegistry.getInstance()
@ -425,11 +385,11 @@ class ExtruderManager(QObject):
# \return A list of values for all extruders. If an extruder does not have a value, it will not be in the list. # \return A list of values for all extruders. If an extruder does not have a value, it will not be in the list.
# If no extruder has the value, the list will contain the global value. # If no extruder has the value, the list will contain the global value.
@staticmethod @staticmethod
def getExtruderValues(key): def getExtruderValues(key: str) -> List[Any]:
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() global_stack = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()) #We know that there must be a global stack by the time you're requesting setting values.
result = [] result = []
for extruder in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()): for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
if not extruder.isEnabled: if not extruder.isEnabled:
continue continue
# only include values from extruders that are "active" for the current machine instance # only include values from extruders that are "active" for the current machine instance
@ -460,8 +420,8 @@ class ExtruderManager(QObject):
# \return A list of values for all extruders. If an extruder does not have a value, it will not be in the list. # \return A list of values for all extruders. If an extruder does not have a value, it will not be in the list.
# If no extruder has the value, the list will contain the global value. # If no extruder has the value, the list will contain the global value.
@staticmethod @staticmethod
def getDefaultExtruderValues(key): def getDefaultExtruderValues(key: str) -> List[Any]:
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() global_stack = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()) #We know that there must be a global stack by the time you're requesting setting values.
context = PropertyEvaluationContext(global_stack) context = PropertyEvaluationContext(global_stack)
context.context["evaluate_from_container_index"] = 1 # skip the user settings container context.context["evaluate_from_container_index"] = 1 # skip the user settings container
context.context["override_operators"] = { context.context["override_operators"] = {
@ -471,7 +431,7 @@ class ExtruderManager(QObject):
} }
result = [] result = []
for extruder in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()): for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
# only include values from extruders that are "active" for the current machine instance # only include values from extruders that are "active" for the current machine instance
if int(extruder.getMetaDataEntry("position")) >= global_stack.getProperty("machine_extruder_count", "value", context = context): if int(extruder.getMetaDataEntry("position")) >= global_stack.getProperty("machine_extruder_count", "value", context = context):
continue continue
@ -504,7 +464,7 @@ class ExtruderManager(QObject):
# #
# \return String representing the extruder values # \return String representing the extruder values
@pyqtSlot(str, result="QVariant") @pyqtSlot(str, result="QVariant")
def getInstanceExtruderValues(self, key): def getInstanceExtruderValues(self, key) -> List:
return ExtruderManager.getExtruderValues(key) return ExtruderManager.getExtruderValues(key)
## Get the value for a setting from a specific extruder. ## Get the value for a setting from a specific extruder.
@ -517,7 +477,7 @@ class ExtruderManager(QObject):
# \return The value of the setting for the specified extruder or for the # \return The value of the setting for the specified extruder or for the
# global stack if not found. # global stack if not found.
@staticmethod @staticmethod
def getExtruderValue(extruder_index, key): def getExtruderValue(extruder_index: int, key: str) -> Any:
if extruder_index == -1: if extruder_index == -1:
extruder_index = int(cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition) extruder_index = int(cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition)
extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index) extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index)
@ -528,7 +488,7 @@ class ExtruderManager(QObject):
value = value(extruder) value = value(extruder)
else: else:
# Just a value from global. # Just a value from global.
value = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack().getProperty(key, "value") value = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()).getProperty(key, "value")
return value return value
@ -542,7 +502,7 @@ class ExtruderManager(QObject):
# \return The value of the setting for the specified extruder or for the # \return The value of the setting for the specified extruder or for the
# global stack if not found. # global stack if not found.
@staticmethod @staticmethod
def getDefaultExtruderValue(extruder_index, key): def getDefaultExtruderValue(extruder_index: int, key: str) -> Any:
extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index) extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index)
context = PropertyEvaluationContext(extruder) context = PropertyEvaluationContext(extruder)
context.context["evaluate_from_container_index"] = 1 # skip the user settings container context.context["evaluate_from_container_index"] = 1 # skip the user settings container
@ -557,7 +517,7 @@ class ExtruderManager(QObject):
if isinstance(value, SettingFunction): if isinstance(value, SettingFunction):
value = value(extruder, context = context) value = value(extruder, context = context)
else: # Just a value from global. else: # Just a value from global.
value = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack().getProperty(key, "value", context = context) value = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()).getProperty(key, "value", context = context)
return value return value
@ -569,8 +529,8 @@ class ExtruderManager(QObject):
# #
# \return The effective value # \return The effective value
@staticmethod @staticmethod
def getResolveOrValue(key): def getResolveOrValue(key: str) -> Any:
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() global_stack = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack())
resolved_value = global_stack.getProperty(key, "value") resolved_value = global_stack.getProperty(key, "value")
return resolved_value return resolved_value
@ -583,8 +543,8 @@ class ExtruderManager(QObject):
# #
# \return The effective value # \return The effective value
@staticmethod @staticmethod
def getDefaultResolveOrValue(key): def getDefaultResolveOrValue(key: str) -> Any:
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() global_stack = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack())
context = PropertyEvaluationContext(global_stack) context = PropertyEvaluationContext(global_stack)
context.context["evaluate_from_container_index"] = 1 # skip the user settings container context.context["evaluate_from_container_index"] = 1 # skip the user settings container
context.context["override_operators"] = { context.context["override_operators"] = {

View file

@ -134,7 +134,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
# Link to new extruders # Link to new extruders
self._active_machine_extruders = [] self._active_machine_extruders = []
extruder_manager = Application.getInstance().getExtruderManager() extruder_manager = Application.getInstance().getExtruderManager()
for extruder in extruder_manager.getExtruderStacks(): for extruder in extruder_manager.getActiveExtruderStacks():
if extruder is None: #This extruder wasn't loaded yet. This happens asynchronously while this model is constructed from QML. if extruder is None: #This extruder wasn't loaded yet. This happens asynchronously while this model is constructed from QML.
continue continue
extruder.containersChanged.connect(self._onExtruderStackContainersChanged) extruder.containersChanged.connect(self._onExtruderStackContainersChanged)
@ -171,7 +171,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
# get machine extruder count for verification # get machine extruder count for verification
machine_extruder_count = global_container_stack.getProperty("machine_extruder_count", "value") machine_extruder_count = global_container_stack.getProperty("machine_extruder_count", "value")
for extruder in Application.getInstance().getExtruderManager().getMachineExtruders(global_container_stack.getId()): for extruder in Application.getInstance().getExtruderManager().getActiveExtruderStacks():
position = extruder.getMetaDataEntry("position", default = "0") # Get the position position = extruder.getMetaDataEntry("position", default = "0") # Get the position
try: try:
position = int(position) position = int(position)

View file

@ -13,6 +13,8 @@ from UM.Settings.SettingInstance import InstanceState
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.Interfaces import PropertyEvaluationContext from UM.Settings.Interfaces import PropertyEvaluationContext
from UM.Logger import Logger from UM.Logger import Logger
from UM.Util import parseBool
import cura.CuraApplication import cura.CuraApplication
from . import Exceptions from . import Exceptions
@ -21,6 +23,7 @@ from .CuraContainerStack import CuraContainerStack
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.Settings.ExtruderStack import ExtruderStack from cura.Settings.ExtruderStack import ExtruderStack
## Represents the Global or Machine stack and its related containers. ## Represents the Global or Machine stack and its related containers.
# #
class GlobalStack(CuraContainerStack): class GlobalStack(CuraContainerStack):
@ -61,6 +64,10 @@ class GlobalStack(CuraContainerStack):
name = self.variant.getName() name = self.variant.getName()
return name return name
@pyqtProperty(str, constant = True)
def preferred_output_file_formats(self) -> str:
return self.getMetaDataEntry("file_formats")
## Add an extruder to the list of extruders of this stack. ## Add an extruder to the list of extruders of this stack.
# #
# \param extruder The extruder to add. # \param extruder The extruder to add.
@ -184,6 +191,15 @@ class GlobalStack(CuraContainerStack):
def getHeadAndFansCoordinates(self): def getHeadAndFansCoordinates(self):
return self.getProperty("machine_head_with_fans_polygon", "value") return self.getProperty("machine_head_with_fans_polygon", "value")
def getHasMaterials(self) -> bool:
return parseBool(self.getMetaDataEntry("has_materials", False))
def getHasVariants(self) -> bool:
return parseBool(self.getMetaDataEntry("has_variants", False))
def getHasMachineQuality(self) -> bool:
return parseBool(self.getMetaDataEntry("has_machine_quality", False))
## private: ## private:
global_stack_mime = MimeType( global_stack_mime = MimeType(

View file

@ -369,6 +369,7 @@ class MachineManager(QObject):
return return
global_stack = containers[0] global_stack = containers[0]
ExtruderManager.getInstance()._fixSingleExtrusionMachineExtruderDefinition(global_stack)
if not global_stack.isValid(): if not global_stack.isValid():
# Mark global stack as invalid # Mark global stack as invalid
ConfigurationErrorMessage.getInstance().addFaultyContainers(global_stack.getId()) ConfigurationErrorMessage.getInstance().addFaultyContainers(global_stack.getId())
@ -377,7 +378,7 @@ class MachineManager(QObject):
self._global_container_stack = global_stack self._global_container_stack = global_stack
self._application.setGlobalContainerStack(global_stack) self._application.setGlobalContainerStack(global_stack)
ExtruderManager.getInstance()._globalContainerStackChanged() ExtruderManager.getInstance()._globalContainerStackChanged()
self._initMachineState(containers[0]) self._initMachineState(global_stack)
self._onGlobalContainerChanged() self._onGlobalContainerChanged()
self.__emitChangedSignals() self.__emitChangedSignals()
@ -387,7 +388,9 @@ class MachineManager(QObject):
# \param definition_id \type{str} definition id that needs to look for # \param definition_id \type{str} definition id that needs to look for
# \param metadata_filter \type{dict} list of metadata keys and values used for filtering # \param metadata_filter \type{dict} list of metadata keys and values used for filtering
@staticmethod @staticmethod
def getMachine(definition_id: str, metadata_filter: Dict[str, str] = None) -> Optional["GlobalStack"]: def getMachine(definition_id: str, metadata_filter: Optional[Dict[str, str]] = None) -> Optional["GlobalStack"]:
if metadata_filter is None:
metadata_filter = {}
machines = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) machines = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter)
for machine in machines: for machine in machines:
if machine.definition.getId() == definition_id: if machine.definition.getId() == definition_id:
@ -414,7 +417,7 @@ class MachineManager(QObject):
# Not a very pretty solution, but the extruder manager doesn't really know how many extruders there are # Not a very pretty solution, but the extruder manager doesn't really know how many extruders there are
machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value") machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
extruder_stacks = ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()) extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
count = 1 # we start with the global stack count = 1 # we start with the global stack
for stack in extruder_stacks: for stack in extruder_stacks:
md = stack.getMetaData() md = stack.getMetaData()
@ -437,7 +440,7 @@ class MachineManager(QObject):
if self._global_container_stack.getTop().findInstances(): if self._global_container_stack.getTop().findInstances():
return True return True
stacks = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())) stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
for stack in stacks: for stack in stacks:
if stack.getTop().findInstances(): if stack.getTop().findInstances():
return True return True
@ -450,7 +453,7 @@ class MachineManager(QObject):
return 0 return 0
num_user_settings = 0 num_user_settings = 0
num_user_settings += len(self._global_container_stack.getTop().findInstances()) num_user_settings += len(self._global_container_stack.getTop().findInstances())
stacks = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())) stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
for stack in stacks: for stack in stacks:
num_user_settings += len(stack.getTop().findInstances()) num_user_settings += len(stack.getTop().findInstances())
return num_user_settings return num_user_settings
@ -475,7 +478,7 @@ class MachineManager(QObject):
stack = ExtruderManager.getInstance().getActiveExtruderStack() stack = ExtruderManager.getInstance().getActiveExtruderStack()
stacks = [stack] stacks = [stack]
else: else:
stacks = ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()) stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
for stack in stacks: for stack in stacks:
if stack is not None: if stack is not None:
@ -640,7 +643,7 @@ class MachineManager(QObject):
if self._active_container_stack is None or self._global_container_stack is None: if self._active_container_stack is None or self._global_container_stack is None:
return return
new_value = self._active_container_stack.getProperty(key, "value") new_value = self._active_container_stack.getProperty(key, "value")
extruder_stacks = [stack for stack in ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())] extruder_stacks = [stack for stack in ExtruderManager.getInstance().getActiveExtruderStacks()]
# check in which stack the value has to be replaced # check in which stack the value has to be replaced
for extruder_stack in extruder_stacks: for extruder_stack in extruder_stacks:
@ -892,7 +895,11 @@ class MachineManager(QObject):
extruder_nr = node.callDecoration("getActiveExtruderPosition") extruder_nr = node.callDecoration("getActiveExtruderPosition")
if extruder_nr is not None and int(extruder_nr) > extruder_count - 1: if extruder_nr is not None and int(extruder_nr) > extruder_count - 1:
node.callDecoration("setActiveExtruder", extruder_manager.getExtruderStack(extruder_count - 1).getId()) extruder = extruder_manager.getExtruderStack(extruder_count - 1)
if extruder is not None:
node.callDecoration("setActiveExtruder", extruder.getId())
else:
Logger.log("w", "Could not find extruder to set active.")
# Make sure one of the extruder stacks is active # Make sure one of the extruder stacks is active
extruder_manager.setActiveExtruderIndex(0) extruder_manager.setActiveExtruderIndex(0)

View file

@ -35,10 +35,9 @@ class MachineNameValidator(QObject):
## Check if a specified machine name is allowed. ## Check if a specified machine name is allowed.
# #
# \param name The machine name to check. # \param name The machine name to check.
# \param position The current position of the cursor in the text box.
# \return ``QValidator.Invalid`` if it's disallowed, or # \return ``QValidator.Invalid`` if it's disallowed, or
# ``QValidator.Acceptable`` if it's allowed. # ``QValidator.Acceptable`` if it's allowed.
def validate(self, name, position): def validate(self, name):
#Check for file name length of the current settings container (which is the longest file we're saving with the name). #Check for file name length of the current settings container (which is the longest file we're saving with the name).
try: try:
filename_max_length = os.statvfs(Resources.getDataStoragePath()).f_namemax filename_max_length = os.statvfs(Resources.getDataStoragePath()).f_namemax
@ -54,7 +53,7 @@ class MachineNameValidator(QObject):
## Updates the validation state of a machine name text field. ## Updates the validation state of a machine name text field.
@pyqtSlot(str) @pyqtSlot(str)
def updateValidation(self, new_name): def updateValidation(self, new_name):
is_valid = self.validate(new_name, 0) is_valid = self.validate(new_name)
if is_valid == QValidator.Acceptable: if is_valid == QValidator.Acceptable:
self.validation_regex = "^.*$" #Matches anything. self.validation_regex = "^.*$" #Matches anything.
else: else:

View file

@ -18,7 +18,7 @@ class SidebarCustomMenuItemsModel(ListModel):
self.addRoleName(self.name_role, "name") self.addRoleName(self.name_role, "name")
self.addRoleName(self.actions_role, "actions") self.addRoleName(self.actions_role, "actions")
self.addRoleName(self.menu_item_role, "menu_item") self.addRoleName(self.menu_item_role, "menu_item")
self.addRoleName(self.menu_item_icon_name_role, "iconName") self.addRoleName(self.menu_item_icon_name_role, "icon_name")
self._updateExtensionList() self._updateExtensionList()
def _updateExtensionList(self)-> None: def _updateExtensionList(self)-> None:

View file

@ -43,7 +43,9 @@ class UserChangesModel(ListModel):
global_stack = Application.getInstance().getGlobalContainerStack() global_stack = Application.getInstance().getGlobalContainerStack()
if not global_stack: if not global_stack:
return return
stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
stacks = [global_stack]
stacks.extend(global_stack.extruders.values())
# Check if the definition container has a translation file and ensure it's loaded. # Check if the definition container has a translation file and ensure it's loaded.
definition = global_stack.getBottom() definition = global_stack.getBottom()

View file

@ -225,7 +225,7 @@ class ThreeMFReader(MeshReader):
except Exception: except Exception:
Logger.logException("e", "An exception occurred in 3mf reader.") Logger.logException("e", "An exception occurred in 3mf reader.")
return [] return None
return result return result

View file

@ -85,14 +85,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
MimeTypeDatabase.addMimeType(
MimeType(
name="application/x-curaproject+xml",
comment="Cura Project File",
suffixes=["curaproject.3mf"]
)
)
self._supported_extensions = [".3mf"] self._supported_extensions = [".3mf"]
self._dialog = WorkspaceDialog() self._dialog = WorkspaceDialog()
self._3mf_mesh_reader = None self._3mf_mesh_reader = None
@ -726,8 +718,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
nodes = [] nodes = []
base_file_name = os.path.basename(file_name) base_file_name = os.path.basename(file_name)
if base_file_name.endswith(".curaproject.3mf"):
base_file_name = base_file_name[:base_file_name.rfind(".curaproject.3mf")]
self.setWorkspaceName(base_file_name) self.setWorkspaceName(base_file_name)
return nodes return nodes
@ -944,7 +934,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
root_material_id) root_material_id)
if material_node is not None and material_node.getContainer() is not None: if material_node is not None and material_node.getContainer() is not None:
extruder_stack.material = material_node.getContainer() extruder_stack.material = material_node.getContainer() # type: InstanceContainer
def _applyChangesToMachine(self, global_stack, extruder_stack_dict): def _applyChangesToMachine(self, global_stack, extruder_stack_dict):
# Clear all first # Clear all first

View file

@ -18,11 +18,7 @@ catalog = i18nCatalog("cura")
def getMetaData() -> Dict: def getMetaData() -> Dict:
# Workaround for osx not supporting double file extensions correctly. workspace_extension = "3mf"
if Platform.isOSX():
workspace_extension = "3mf"
else:
workspace_extension = "curaproject.3mf"
metaData = {} metaData = {}
if "3MFReader.ThreeMFReader" in sys.modules: if "3MFReader.ThreeMFReader" in sys.modules:

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides support for reading 3MF files.", "description": "Provides support for reading 3MF files.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -15,11 +15,7 @@ from UM.Platform import Platform
i18n_catalog = i18nCatalog("uranium") i18n_catalog = i18nCatalog("uranium")
def getMetaData(): def getMetaData():
# Workarround for osx not supporting double file extensions correctly. workspace_extension = "3mf"
if Platform.isOSX():
workspace_extension = "3mf"
else:
workspace_extension = "curaproject.3mf"
metaData = {} metaData = {}
@ -36,7 +32,7 @@ def getMetaData():
"output": [{ "output": [{
"extension": workspace_extension, "extension": workspace_extension,
"description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"), "description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"),
"mime_type": "application/x-curaproject+xml", "mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
"mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode "mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode
}] }]
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides support for writing 3MF files.", "description": "Provides support for writing 3MF files.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -1,3 +1,103 @@
[3.5.0]
*Monitor page
The monitor page of Ultimaker Cura has been remodeled for better consistency with the Cura Connect Print jobs interface. This means less switching between interfaces, and more control from within Ultimaker Cura.
*Open recent projects
Project files can now be found in the Open Recent menu.
*New tool hotkeys
New hotkeys have been assigned for quick toggling between the translate (T), scale (S), rotate (R) and mirror (M) tools.
*Project files use 3MF only
A 3MF extension is now used for project files. The .curaproject extension is no longer used.
*Camera maximum zoom
The maximum zoom has been adjusted to scale with the size of the selected printer. This fixes third-party printers with huge build volumes to be correctly visible.
*Corrected width of layer number box
The layer number indicator in the layer view now displays numbers above 999 correctly.
*Materials preferences
This screen has been redesigned to improve user experience. Materials can now be set as a favorites, so they can be easily accessed in the material selection panel at the top-right of the screen.
*Installed packages checkmark
Packages that are already installed in the Toolbox are now have a checkmark for easy reference.
*Mac OSX save dialog
The save dialog has been restored to its native behavior and bugs have been fixed.
*Removed .gz extension
Saving compressed g-code files from the save dialog has been removed because of incompatibility with MacOS. If sending jobs over Wi-Fi, g-code is still compressed.
*Updates to Chinese translations
Improved and updated Chinese translations. Contributed by MarmaladeForMeat.
*Save project
Saving the project no longer triggers the project to reslice.
*File menu
The Save option in the file menu now saves project files. The export option now saves other types of files, such as STL.
*Improved processing of overhang walls
Overhang walls are detected and printed with different speeds. It will not start a perimeter on an overhanging wall. The quality of overhanging walls may be improved by printing those at a different speed. Contributed by smartavionics.
*Prime tower reliability
The prime tower has been improved for better reliability. This is especially useful when printing with two materials that do not adhere well.
*Support infill line direction
The support infill lines can now be rotated to increase the supporting capabilities and reduce artifacts on the model. This setting rotates existing patterns, like triangle support infill. Contributed by fieldOfView.
*Minimum polygon circumference
Polygons in sliced layers that have a circumference smaller than the setting value will be filtered out. Lower values lead to higher resolution meshes at the cost of increased slicing time. This setting is ideal for very tiny prints with a lot of detail, or for SLA printers. Contributed by cubiq.
*Initial layer support line distance
This setting enables the user to reduce or increase the density of the support initial layer in order to increase or reduce adhesion to the build plate and the overall strength.
*Extra infill wall line count
Adds extra walls around infill. Contributed by BagelOrb.
*Multiply infill
Creates multiple infill lines on the same pattern for sturdier infill. Contributed by BagelOrb.
*Connected infill polygons
Connecting infill lines now also works with concentric and cross infill patterns. The benefit would be stronger infill and more consistent material flow/saving retractions. Contributed by BagelOrb.
*Fan speed override
New setting to modify the fan speed of supported areas. This setting can be found in Support settings > Fan Speed Override when support is enabled. Contributed by smartavionics.
*Minimum wall flow
New setting to define a minimum flow for thin printed walls. Contributed by smartavionics.
*Custom support plugin
A tool downloadable from the toolbox, similar to the support blocker, that adds cubes of support to the model manually by clicking parts of it. Contributed by Lokster.
*Quickly toggle autoslicing
Adds a pause/play button to the progress bar to quickly toggle autoslicing. Contributed by fieldOfview.
*Cura-DuetRRFPlugin
Adds output devices for a Duet RepRapFirmware printer: "Print", "Simulate", and "Upload". Contributed by Kriechi.
*Dremel 3D20
This plugin adds the Dremel printer to Ultimaker Cura. Contributed by Kriechi.
*Bug fixes
- Removed extra M109 commands. Older versions would generate superfluous M109 commands. This has been fixed for better temperature stability when printing.
- Fixed minor mesh handling bugs. A few combinations of modifier meshes now lead to expected behavior.
- Removed unnecessary travels. Connected infill lines are now always printed completely connected, without unnecessary travel moves.
- Removed concentric 3D infill. This infill type has been removed due to lack of reliability.
- Extra skin wall count. Fixed an issue that caused extra print moves with this setting enabled.
- Concentric skin. Small gaps in concentric skin are now filled correctly.
- Order of printed models. The order of a large batch of printed models is now more consistent, instead of random.
*Third party printers
- TiZYX
- Winbo
- Tevo Tornado
- Creality CR-10S
- Wanhao Duplicator
- Deltacomb (update)
- Dacoma (update)
[3.4.1] [3.4.1]
*Bug fixes *Bug fixes
- Fixed an issue that would occasionally cause an unnecessary extra skin wall to be printed, which increased print time. - Fixed an issue that would occasionally cause an unnecessary extra skin wall to be printed, which increased print time.

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Shows changes since latest checked version.", "description": "Shows changes since latest checked version.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -179,8 +179,7 @@ class CuraEngineBackend(QObject, Backend):
# This is useful for debugging and used to actually start the engine. # This is useful for debugging and used to actually start the engine.
# \return list of commands and args / parameters. # \return list of commands and args / parameters.
def getEngineCommand(self) -> List[str]: def getEngineCommand(self) -> List[str]:
json_path = Resources.getPath(Resources.DefinitionContainers, "fdmprinter.def.json") command = [self._application.getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), ""]
command = [self._application.getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", json_path, ""]
parser = argparse.ArgumentParser(prog = "cura", add_help = False) parser = argparse.ArgumentParser(prog = "cura", add_help = False)
parser.add_argument("--debug", action = "store_true", default = False, help = "Turn on the debug mode by setting this option.") parser.add_argument("--debug", action = "store_true", default = False, help = "Turn on the debug mode by setting this option.")
@ -343,7 +342,7 @@ class CuraEngineBackend(QObject, Backend):
if not self._global_container_stack: if not self._global_container_stack:
Logger.log("w", "Global container stack not assigned to CuraEngineBackend!") Logger.log("w", "Global container stack not assigned to CuraEngineBackend!")
return return
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())) extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
error_keys = [] #type: List[str] error_keys = [] #type: List[str]
for extruder in extruders: for extruder in extruders:
error_keys.extend(extruder.getErrorKeys()) error_keys.extend(extruder.getErrorKeys())

View file

@ -178,7 +178,7 @@ class ProcessSlicedLayersJob(Job):
# Find out colors per extruder # Find out colors per extruder
global_container_stack = Application.getInstance().getGlobalContainerStack() global_container_stack = Application.getInstance().getGlobalContainerStack()
manager = ExtruderManager.getInstance() manager = ExtruderManager.getInstance()
extruders = list(manager.getMachineExtruders(global_container_stack.getId())) extruders = manager.getActiveExtruderStacks()
if extruders: if extruders:
material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32) material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32)
for extruder in extruders: for extruder in extruders:

View file

@ -220,8 +220,10 @@ class StartSliceJob(Job):
stack = global_stack stack = global_stack
skip_group = False skip_group = False
for node in group: for node in group:
# Only check if the printing extruder is enabled for printing meshes
is_non_printing_mesh = node.callDecoration("evaluateIsNonPrintingMesh")
extruder_position = node.callDecoration("getActiveExtruderPosition") extruder_position = node.callDecoration("getActiveExtruderPosition")
if not extruders_enabled[extruder_position]: if not is_non_printing_mesh and not extruders_enabled[extruder_position]:
skip_group = True skip_group = True
has_model_with_disabled_extruders = True has_model_with_disabled_extruders = True
associated_disabled_extruders.add(extruder_position) associated_disabled_extruders.add(extruder_position)
@ -331,7 +333,7 @@ class StartSliceJob(Job):
"-1": self._buildReplacementTokens(global_stack) "-1": self._buildReplacementTokens(global_stack)
} }
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()): for extruder_stack in ExtruderManager.getInstance().getActiveExtruderStacks():
extruder_nr = extruder_stack.getProperty("extruder_nr", "value") extruder_nr = extruder_stack.getProperty("extruder_nr", "value")
self._all_extruders_settings[str(extruder_nr)] = self._buildReplacementTokens(extruder_stack) self._all_extruders_settings[str(extruder_nr)] = self._buildReplacementTokens(extruder_stack)
@ -438,8 +440,7 @@ class StartSliceJob(Job):
Job.yieldThread() Job.yieldThread()
# Ensure that the engine is aware what the build extruder is. # Ensure that the engine is aware what the build extruder is.
if stack.getProperty("machine_extruder_count", "value") > 1: changed_setting_keys.add("extruder_nr")
changed_setting_keys.add("extruder_nr")
# Get values for all changed settings # Get values for all changed settings
for key in changed_setting_keys: for key in changed_setting_keys:

View file

@ -2,7 +2,7 @@
"name": "CuraEngine Backend", "name": "CuraEngine Backend",
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"description": "Provides the link to the CuraEngine slicing backend.", "description": "Provides the link to the CuraEngine slicing backend.",
"api": 4, "api": 5,
"version": "1.0.0", "version": "1.0.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides support for importing Cura profiles.", "description": "Provides support for importing Cura profiles.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides support for exporting Cura profiles.", "description": "Provides support for exporting Cura profiles.",
"api": 4, "api": 5,
"i18n-catalog":"cura" "i18n-catalog":"cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Checks for firmware updates.", "description": "Checks for firmware updates.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -4,15 +4,22 @@
import gzip import gzip
from UM.Mesh.MeshReader import MeshReader #The class we're extending/implementing. from UM.Mesh.MeshReader import MeshReader #The class we're extending/implementing.
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType #To add the .gcode.gz files to the MIME type database.
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
## A file reader that reads gzipped g-code. ## A file reader that reads gzipped g-code.
# #
# If you're zipping g-code, you might as well use gzip! # If you're zipping g-code, you might as well use gzip!
class GCodeGzReader(MeshReader): class GCodeGzReader(MeshReader):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
MimeTypeDatabase.addMimeType(
MimeType(
name = "application/x-cura-compressed-gcode-file",
comment = "Cura Compressed GCode File",
suffixes = ["gcode.gz"]
)
)
self._supported_extensions = [".gcode.gz"] self._supported_extensions = [".gcode.gz"]
def _read(self, file_name): def _read(self, file_name):

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Reads g-code from a compressed archive.", "description": "Reads g-code from a compressed archive.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -17,6 +17,10 @@ catalog = i18nCatalog("cura")
# #
# If you're zipping g-code, you might as well use gzip! # If you're zipping g-code, you might as well use gzip!
class GCodeGzWriter(MeshWriter): class GCodeGzWriter(MeshWriter):
def __init__(self) -> None:
super().__init__(add_to_recent_files = False)
## Writes the gzipped g-code to a stream. ## Writes the gzipped g-code to a stream.
# #
# Note that even though the function accepts a collection of nodes, the # Note that even though the function accepts a collection of nodes, the

View file

@ -16,7 +16,8 @@ def getMetaData():
"extension": file_extension, "extension": file_extension,
"description": catalog.i18nc("@item:inlistbox", "Compressed G-code File"), "description": catalog.i18nc("@item:inlistbox", "Compressed G-code File"),
"mime_type": "application/gzip", "mime_type": "application/gzip",
"mode": GCodeGzWriter.GCodeGzWriter.OutputMode.BinaryMode "mode": GCodeGzWriter.GCodeGzWriter.OutputMode.BinaryMode,
"hide_in_file_dialog": True,
}] }]
} }
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Writes g-code to a compressed archive.", "description": "Writes g-code to a compressed archive.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides support for importing profiles from g-code files.", "description": "Provides support for importing profiles from g-code files.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -275,7 +275,7 @@ class FlavorParser:
## For showing correct x, y offsets for each extruder ## For showing correct x, y offsets for each extruder
def _extruderOffsets(self) -> Dict[int, List[float]]: def _extruderOffsets(self) -> Dict[int, List[float]]:
result = {} result = {}
for extruder in ExtruderManager.getInstance().getExtruderStacks(): for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
result[int(extruder.getMetaData().get("position", "0"))] = [ result[int(extruder.getMetaData().get("position", "0"))] = [
extruder.getProperty("machine_nozzle_offset_x", "value"), extruder.getProperty("machine_nozzle_offset_x", "value"),
extruder.getProperty("machine_nozzle_offset_y", "value")] extruder.getProperty("machine_nozzle_offset_y", "value")]

View file

@ -1,4 +1,5 @@
# Copyright (c) 2017 Aleph Objects, Inc. # Copyright (c) 2017 Aleph Objects, Inc.
# Copyright (c) 2018 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 UM.FileHandler.FileReader import FileReader from UM.FileHandler.FileReader import FileReader
@ -11,13 +12,7 @@ catalog = i18nCatalog("cura")
from . import MarlinFlavorParser, RepRapFlavorParser from . import MarlinFlavorParser, RepRapFlavorParser
MimeTypeDatabase.addMimeType(
MimeType(
name = "application/x-cura-gcode-file",
comment = "Cura GCode File",
suffixes = ["gcode", "gcode.gz"]
)
)
# Class for loading and parsing G-code files # Class for loading and parsing G-code files
@ -29,7 +24,15 @@ class GCodeReader(MeshReader):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
MimeTypeDatabase.addMimeType(
MimeType(
name = "application/x-cura-gcode-file",
comment = "Cura GCode File",
suffixes = ["gcode"]
)
)
self._supported_extensions = [".gcode", ".g"] self._supported_extensions = [".gcode", ".g"]
self._flavor_reader = None self._flavor_reader = None
Application.getInstance().getPreferences().addPreference("gcodereader/show_caution", True) Application.getInstance().getPreferences().addPreference("gcodereader/show_caution", True)

View file

@ -3,6 +3,6 @@
"author": "Victor Larchenko", "author": "Victor Larchenko",
"version": "1.0.0", "version": "1.0.0",
"description": "Allows loading and displaying G-code files.", "description": "Allows loading and displaying G-code files.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -47,7 +47,7 @@ class GCodeWriter(MeshWriter):
_setting_keyword = ";SETTING_" _setting_keyword = ";SETTING_"
def __init__(self): def __init__(self):
super().__init__() super().__init__(add_to_recent_files = False)
self._application = Application.getInstance() self._application = Application.getInstance()

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Writes g-code to a file.", "description": "Writes g-code to a file.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Enables ability to generate printable geometry from 2D image files.", "description": "Enables ability to generate printable geometry from 2D image files.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides support for importing profiles from legacy Cura versions.", "description": "Provides support for importing profiles from legacy Cura versions.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -16,6 +16,8 @@ Cura.MachineAction
property var extrudersModel: Cura.ExtrudersModel{} property var extrudersModel: Cura.ExtrudersModel{}
property int extruderTabsCount: 0 property int extruderTabsCount: 0
property var activeMachineId: Cura.MachineManager.activeMachine != null ? Cura.MachineManager.activeMachine.id : ""
Connections Connections
{ {
target: base.extrudersModel target: base.extrudersModel
@ -511,7 +513,7 @@ Cura.MachineAction
} }
return ""; return "";
} }
return Cura.MachineManager.activeMachineId; return base.activeMachineId
} }
key: settingKey key: settingKey
watchedProperties: [ "value", "description" ] watchedProperties: [ "value", "description" ]
@ -564,7 +566,7 @@ Cura.MachineAction
} }
return ""; return "";
} }
return Cura.MachineManager.activeMachineId; return base.activeMachineId
} }
key: settingKey key: settingKey
watchedProperties: [ "value", "description" ] watchedProperties: [ "value", "description" ]
@ -655,7 +657,7 @@ Cura.MachineAction
} }
return ""; return "";
} }
return Cura.MachineManager.activeMachineId; return base.activeMachineId
} }
key: settingKey key: settingKey
watchedProperties: [ "value", "options", "description" ] watchedProperties: [ "value", "options", "description" ]
@ -754,7 +756,7 @@ Cura.MachineAction
} }
return ""; return "";
} }
return Cura.MachineManager.activeMachineId; return base.activeMachineId
} }
key: settingKey key: settingKey
watchedProperties: [ "value", "description" ] watchedProperties: [ "value", "description" ]
@ -879,7 +881,7 @@ Cura.MachineAction
{ {
id: machineExtruderCountProvider id: machineExtruderCountProvider
containerStackId: Cura.MachineManager.activeMachineId containerStackId: base.activeMachineId
key: "machine_extruder_count" key: "machine_extruder_count"
watchedProperties: [ "value", "description" ] watchedProperties: [ "value", "description" ]
storeIndex: manager.containerIndex storeIndex: manager.containerIndex
@ -889,7 +891,7 @@ Cura.MachineAction
{ {
id: machineHeadPolygonProvider id: machineHeadPolygonProvider
containerStackId: Cura.MachineManager.activeMachineId containerStackId: base.activeMachineId
key: "machine_head_with_fans_polygon" key: "machine_head_with_fans_polygon"
watchedProperties: [ "value" ] watchedProperties: [ "value" ]
storeIndex: manager.containerIndex storeIndex: manager.containerIndex

View file

@ -3,6 +3,6 @@
"author": "fieldOfView", "author": "fieldOfView",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).", "description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -2,7 +2,7 @@
"name": "Model Checker", "name": "Model Checker",
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "0.1", "version": "0.1",
"api": 4, "api": 5,
"description": "Checks models and print configuration for possible printing issues and give suggestions.", "description": "Checks models and print configuration for possible printing issues and give suggestions.",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides a monitor stage in Cura.", "description": "Provides a monitor stage in Cura.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -17,7 +17,6 @@ Item {
width: childrenRect.width; width: childrenRect.width;
height: childrenRect.height; height: childrenRect.height;
property var all_categories_except_support: [ "machine_settings", "resolution", "shell", "infill", "material", "speed", property var all_categories_except_support: [ "machine_settings", "resolution", "shell", "infill", "material", "speed",
"travel", "cooling", "platform_adhesion", "dual", "meshfix", "blackmagic", "experimental"] "travel", "cooling", "platform_adhesion", "dual", "meshfix", "blackmagic", "experimental"]
@ -45,7 +44,7 @@ Item {
UM.SettingPropertyProvider UM.SettingPropertyProvider
{ {
id: meshTypePropertyProvider id: meshTypePropertyProvider
containerStackId: Cura.MachineManager.activeMachineId containerStack: Cura.MachineManager.activeMachine
watchedProperties: [ "enabled" ] watchedProperties: [ "enabled" ]
} }
@ -518,7 +517,7 @@ Item {
{ {
id: machineExtruderCount id: machineExtruderCount
containerStackId: Cura.MachineManager.activeMachineId containerStack: Cura.MachineManager.activeMachine
key: "machine_extruder_count" key: "machine_extruder_count"
watchedProperties: [ "value" ] watchedProperties: [ "value" ]
storeIndex: 0 storeIndex: 0
@ -528,7 +527,7 @@ Item {
{ {
id: printSequencePropertyProvider id: printSequencePropertyProvider
containerStackId: Cura.MachineManager.activeMachineId containerStack: Cura.MachineManager.activeMachine
key: "print_sequence" key: "print_sequence"
watchedProperties: [ "value" ] watchedProperties: [ "value" ]
storeIndex: 0 storeIndex: 0

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides the Per Model Settings.", "description": "Provides the Per Model Settings.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -1,5 +1,6 @@
# Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V. # Copyright (c) 2018 Jaime van Kessel, Ultimaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. # The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
@ -260,6 +261,9 @@ class PostProcessingPlugin(QObject, Extension):
# Create the plugin dialog component # Create the plugin dialog component
path = os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml") path = os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml")
self._view = Application.getInstance().createQmlComponent(path, {"manager": self}) self._view = Application.getInstance().createQmlComponent(path, {"manager": self})
if self._view is None:
Logger.log("e", "Not creating PostProcessing button near save button because the QML component failed to be created.")
return
Logger.log("d", "Post processing view created.") Logger.log("d", "Post processing view created.")
# Create the save button component # Create the save button component
@ -269,6 +273,9 @@ class PostProcessingPlugin(QObject, Extension):
def showPopup(self): def showPopup(self):
if self._view is None: if self._view is None:
self._createView() self._createView()
if self._view is None:
Logger.log("e", "Not creating PostProcessing window since the QML component failed to be created.")
return
self._view.show() self._view.show()
## Property changed: trigger re-slice ## Property changed: trigger re-slice

View file

@ -384,7 +384,7 @@ UM.Dialog
UM.SettingPropertyProvider UM.SettingPropertyProvider
{ {
id: inheritStackProvider id: inheritStackProvider
containerStackId: Cura.MachineManager.activeMachineId containerStack: Cura.MachineManager.activeMachine
key: model.key ? model.key : "None" key: model.key ? model.key : "None"
watchedProperties: [ "limit_to_extruder" ] watchedProperties: [ "limit_to_extruder" ]
} }

View file

@ -2,7 +2,7 @@
"name": "Post Processing", "name": "Post Processing",
"author": "Ultimaker", "author": "Ultimaker",
"version": "2.2", "version": "2.2",
"api": 4, "api": 5,
"description": "Extension that allows for user created scripts for post processing", "description": "Extension that allows for user created scripts for post processing",
"catalog": "cura" "catalog": "cura"
} }

View file

@ -0,0 +1,51 @@
from ..Script import Script
class PauseAtHeightRepRapFirmwareDuet(Script):
def getSettingDataString(self):
return """{
"name": "Pause at height for RepRapFirmware DuetWifi / Duet Ethernet / Duet Maestro",
"key": "PauseAtHeightRepRapFirmwareDuet",
"metadata": {},
"version": 2,
"settings":
{
"pause_height":
{
"label": "Pause height",
"description": "At what height should the pause occur",
"unit": "mm",
"type": "float",
"default_value": 5.0
}
}
}"""
def execute(self, data):
current_z = 0.
pause_z = self.getSettingValueByKey("pause_height")
layers_started = False
for layer_number, layer in enumerate(data):
lines = layer.split("\n")
for line in lines:
if ";LAYER:0" in line:
layers_started = True
continue
if not layers_started:
continue
if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0:
current_z = self.getValue(line, 'Z')
if current_z != None:
if current_z >= pause_z:
prepend_gcode = ";TYPE:CUSTOM\n"
prepend_gcode += "; -- Pause at height (%.2f mm) --\n" % pause_z
prepend_gcode += self.putValue(M = 226) + "\n"
layer = prepend_gcode + layer
data[layer_number] = layer # Override the data of this layer with the modified data
return data
break
return data

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides a prepare stage in Cura.", "description": "Provides a prepare stage in Cura.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"description": "Provides removable drive hotplugging and writing support.", "description": "Provides removable drive hotplugging and writing support.",
"version": "1.0.0", "version": "1.0.0",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -9,7 +9,8 @@ import QtQuick.Controls.Styles 1.1
import UM 1.0 as UM import UM 1.0 as UM
import Cura 1.0 as Cura import Cura 1.0 as Cura
Item { Item
{
id: sliderRoot id: sliderRoot
// handle properties // handle properties
@ -39,40 +40,49 @@ Item {
property real lowerValue: minimumValue property real lowerValue: minimumValue
property bool layersVisible: true property bool layersVisible: true
property bool manuallyChanged: true // Indicates whether the value was changed manually or during simulation
function getUpperValueFromSliderHandle() { function getUpperValueFromSliderHandle()
{
return upperHandle.getValue() return upperHandle.getValue()
} }
function setUpperValue(value) { function setUpperValue(value)
{
upperHandle.setValue(value) upperHandle.setValue(value)
updateRangeHandle() updateRangeHandle()
} }
function getLowerValueFromSliderHandle() { function getLowerValueFromSliderHandle()
{
return lowerHandle.getValue() return lowerHandle.getValue()
} }
function setLowerValue(value) { function setLowerValue(value)
{
lowerHandle.setValue(value) lowerHandle.setValue(value)
updateRangeHandle() updateRangeHandle()
} }
function updateRangeHandle() { function updateRangeHandle()
{
rangeHandle.height = lowerHandle.y - (upperHandle.y + upperHandle.height) rangeHandle.height = lowerHandle.y - (upperHandle.y + upperHandle.height)
} }
// set the active handle to show only one label at a time // set the active handle to show only one label at a time
function setActiveHandle(handle) { function setActiveHandle(handle)
{
activeHandle = handle activeHandle = handle
} }
function normalizeValue(value) { function normalizeValue(value)
{
return Math.min(Math.max(value, sliderRoot.minimumValue), sliderRoot.maximumValue) return Math.min(Math.max(value, sliderRoot.minimumValue), sliderRoot.maximumValue)
} }
// slider track // slider track
Rectangle { Rectangle
{
id: track id: track
width: sliderRoot.trackThickness width: sliderRoot.trackThickness
@ -86,7 +96,8 @@ Item {
} }
// Range handle // Range handle
Item { Item
{
id: rangeHandle id: rangeHandle
y: upperHandle.y + upperHandle.height y: upperHandle.y + upperHandle.height
@ -96,7 +107,9 @@ Item {
visible: sliderRoot.layersVisible visible: sliderRoot.layersVisible
// set the new value when dragging // set the new value when dragging
function onHandleDragged () { function onHandleDragged()
{
sliderRoot.manuallyChanged = true
upperHandle.y = y - upperHandle.height upperHandle.y = y - upperHandle.height
lowerHandle.y = y + height lowerHandle.y = y + height
@ -109,7 +122,14 @@ Item {
UM.SimulationView.setMinimumLayer(lowerValue) UM.SimulationView.setMinimumLayer(lowerValue)
} }
function setValue (value) { function setValueManually(value)
{
sliderRoot.manuallyChanged = true
upperHandle.setValue(value)
}
function setValue(value)
{
var range = sliderRoot.upperValue - sliderRoot.lowerValue var range = sliderRoot.upperValue - sliderRoot.lowerValue
value = Math.min(value, sliderRoot.maximumValue) value = Math.min(value, sliderRoot.maximumValue)
value = Math.max(value, sliderRoot.minimumValue + range) value = Math.max(value, sliderRoot.minimumValue + range)
@ -118,17 +138,20 @@ Item {
UM.SimulationView.setMinimumLayer(value - range) UM.SimulationView.setMinimumLayer(value - range)
} }
Rectangle { Rectangle
{
width: sliderRoot.trackThickness - 2 * sliderRoot.trackBorderWidth width: sliderRoot.trackThickness - 2 * sliderRoot.trackBorderWidth
height: parent.height + sliderRoot.handleSize height: parent.height + sliderRoot.handleSize
anchors.centerIn: parent anchors.centerIn: parent
color: sliderRoot.rangeHandleColor color: sliderRoot.rangeHandleColor
} }
MouseArea { MouseArea
{
anchors.fill: parent anchors.fill: parent
drag { drag
{
target: parent target: parent
axis: Drag.YAxis axis: Drag.YAxis
minimumY: upperHandle.height minimumY: upperHandle.height
@ -139,7 +162,8 @@ Item {
onPressed: sliderRoot.setActiveHandle(rangeHandle) onPressed: sliderRoot.setActiveHandle(rangeHandle)
} }
SimulationSliderLabel { SimulationSliderLabel
{
id: rangleHandleLabel id: rangleHandleLabel
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
@ -152,12 +176,13 @@ Item {
maximumValue: sliderRoot.maximumValue maximumValue: sliderRoot.maximumValue
value: sliderRoot.upperValue value: sliderRoot.upperValue
busy: UM.SimulationView.busy busy: UM.SimulationView.busy
setValue: rangeHandle.setValue // connect callback functions setValue: rangeHandle.setValueManually // connect callback functions
} }
} }
// Upper handle // Upper handle
Rectangle { Rectangle
{
id: upperHandle id: upperHandle
y: sliderRoot.height - (sliderRoot.minimumRangeHandleSize + 2 * sliderRoot.handleSize) y: sliderRoot.height - (sliderRoot.minimumRangeHandleSize + 2 * sliderRoot.handleSize)
@ -168,10 +193,13 @@ Item {
color: upperHandleLabel.activeFocus ? sliderRoot.handleActiveColor : sliderRoot.upperHandleColor color: upperHandleLabel.activeFocus ? sliderRoot.handleActiveColor : sliderRoot.upperHandleColor
visible: sliderRoot.layersVisible visible: sliderRoot.layersVisible
function onHandleDragged () { function onHandleDragged()
{
sliderRoot.manuallyChanged = true
// don't allow the lower handle to be heigher than the upper handle // don't allow the lower handle to be heigher than the upper handle
if (lowerHandle.y - (y + height) < sliderRoot.minimumRangeHandleSize) { if (lowerHandle.y - (y + height) < sliderRoot.minimumRangeHandleSize)
{
lowerHandle.y = y + height + sliderRoot.minimumRangeHandleSize lowerHandle.y = y + height + sliderRoot.minimumRangeHandleSize
} }
@ -183,15 +211,23 @@ Item {
} }
// get the upper value based on the slider position // get the upper value based on the slider position
function getValue () { function getValue()
{
var result = y / (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)) var result = y / (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize))
result = sliderRoot.maximumValue + result * (sliderRoot.minimumValue - (sliderRoot.maximumValue - sliderRoot.minimumValue)) result = sliderRoot.maximumValue + result * (sliderRoot.minimumValue - (sliderRoot.maximumValue - sliderRoot.minimumValue))
result = sliderRoot.roundValues ? Math.round(result) : result result = sliderRoot.roundValues ? Math.round(result) : result
return result return result
} }
function setValueManually(value)
{
sliderRoot.manuallyChanged = true
upperHandle.setValue(value)
}
// set the slider position based on the upper value // set the slider position based on the upper value
function setValue (value) { function setValue(value)
{
// Normalize values between range, since using arrow keys will create out-of-the-range values // Normalize values between range, since using arrow keys will create out-of-the-range values
value = sliderRoot.normalizeValue(value) value = sliderRoot.normalizeValue(value)
@ -209,10 +245,12 @@ Item {
Keys.onDownPressed: upperHandleLabel.setValue(upperHandleLabel.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1)) Keys.onDownPressed: upperHandleLabel.setValue(upperHandleLabel.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
// dragging // dragging
MouseArea { MouseArea
{
anchors.fill: parent anchors.fill: parent
drag { drag
{
target: parent target: parent
axis: Drag.YAxis axis: Drag.YAxis
minimumY: 0 minimumY: 0
@ -220,13 +258,15 @@ Item {
} }
onPositionChanged: parent.onHandleDragged() onPositionChanged: parent.onHandleDragged()
onPressed: { onPressed:
{
sliderRoot.setActiveHandle(upperHandle) sliderRoot.setActiveHandle(upperHandle)
upperHandleLabel.forceActiveFocus() upperHandleLabel.forceActiveFocus()
} }
} }
SimulationSliderLabel { SimulationSliderLabel
{
id: upperHandleLabel id: upperHandleLabel
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
@ -239,12 +279,13 @@ Item {
maximumValue: sliderRoot.maximumValue maximumValue: sliderRoot.maximumValue
value: sliderRoot.upperValue value: sliderRoot.upperValue
busy: UM.SimulationView.busy busy: UM.SimulationView.busy
setValue: upperHandle.setValue // connect callback functions setValue: upperHandle.setValueManually // connect callback functions
} }
} }
// Lower handle // Lower handle
Rectangle { Rectangle
{
id: lowerHandle id: lowerHandle
y: sliderRoot.height - sliderRoot.handleSize y: sliderRoot.height - sliderRoot.handleSize
@ -256,10 +297,13 @@ Item {
visible: sliderRoot.layersVisible visible: sliderRoot.layersVisible
function onHandleDragged () { function onHandleDragged()
{
sliderRoot.manuallyChanged = true
// don't allow the upper handle to be lower than the lower handle // don't allow the upper handle to be lower than the lower handle
if (y - (upperHandle.y + upperHandle.height) < sliderRoot.minimumRangeHandleSize) { if (y - (upperHandle.y + upperHandle.height) < sliderRoot.minimumRangeHandleSize)
{
upperHandle.y = y - (upperHandle.heigth + sliderRoot.minimumRangeHandleSize) upperHandle.y = y - (upperHandle.heigth + sliderRoot.minimumRangeHandleSize)
} }
@ -271,15 +315,24 @@ Item {
} }
// get the lower value from the current slider position // get the lower value from the current slider position
function getValue () { function getValue()
{
var result = (y - (sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)) / (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)); var result = (y - (sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)) / (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize));
result = sliderRoot.maximumValue - sliderRoot.minimumRange + result * (sliderRoot.minimumValue - (sliderRoot.maximumValue - sliderRoot.minimumRange)) result = sliderRoot.maximumValue - sliderRoot.minimumRange + result * (sliderRoot.minimumValue - (sliderRoot.maximumValue - sliderRoot.minimumRange))
result = sliderRoot.roundValues ? Math.round(result) : result result = sliderRoot.roundValues ? Math.round(result) : result
return result return result
} }
function setValueManually(value)
{
sliderRoot.manuallyChanged = true
lowerHandle.setValue(value)
}
// set the slider position based on the lower value // set the slider position based on the lower value
function setValue (value) { function setValue(value)
{
// Normalize values between range, since using arrow keys will create out-of-the-range values // Normalize values between range, since using arrow keys will create out-of-the-range values
value = sliderRoot.normalizeValue(value) value = sliderRoot.normalizeValue(value)
@ -297,10 +350,12 @@ Item {
Keys.onDownPressed: lowerHandleLabel.setValue(lowerHandleLabel.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1)) Keys.onDownPressed: lowerHandleLabel.setValue(lowerHandleLabel.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
// dragging // dragging
MouseArea { MouseArea
{
anchors.fill: parent anchors.fill: parent
drag { drag
{
target: parent target: parent
axis: Drag.YAxis axis: Drag.YAxis
minimumY: upperHandle.height + sliderRoot.minimumRangeHandleSize minimumY: upperHandle.height + sliderRoot.minimumRangeHandleSize
@ -308,13 +363,15 @@ Item {
} }
onPositionChanged: parent.onHandleDragged() onPositionChanged: parent.onHandleDragged()
onPressed: { onPressed:
{
sliderRoot.setActiveHandle(lowerHandle) sliderRoot.setActiveHandle(lowerHandle)
lowerHandleLabel.forceActiveFocus() lowerHandleLabel.forceActiveFocus()
} }
} }
SimulationSliderLabel { SimulationSliderLabel
{
id: lowerHandleLabel id: lowerHandleLabel
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
@ -327,7 +384,7 @@ Item {
maximumValue: sliderRoot.maximumValue maximumValue: sliderRoot.maximumValue
value: sliderRoot.lowerValue value: sliderRoot.lowerValue
busy: UM.SimulationView.busy busy: UM.SimulationView.busy
setValue: lowerHandle.setValue // connect callback functions setValue: lowerHandle.setValueManually // connect callback functions
} }
} }
} }

View file

@ -9,7 +9,8 @@ import QtQuick.Controls.Styles 1.1
import UM 1.0 as UM import UM 1.0 as UM
import Cura 1.0 as Cura import Cura 1.0 as Cura
Item { Item
{
id: sliderRoot id: sliderRoot
// handle properties // handle properties
@ -34,26 +35,32 @@ Item {
property real handleValue: maximumValue property real handleValue: maximumValue
property bool pathsVisible: true property bool pathsVisible: true
property bool manuallyChanged: true // Indicates whether the value was changed manually or during simulation
function getHandleValueFromSliderHandle () { function getHandleValueFromSliderHandle()
{
return handle.getValue() return handle.getValue()
} }
function setHandleValue (value) { function setHandleValue(value)
{
handle.setValue(value) handle.setValue(value)
updateRangeHandle() updateRangeHandle()
} }
function updateRangeHandle () { function updateRangeHandle()
{
rangeHandle.width = handle.x - sliderRoot.handleSize rangeHandle.width = handle.x - sliderRoot.handleSize
} }
function normalizeValue(value) { function normalizeValue(value)
{
return Math.min(Math.max(value, sliderRoot.minimumValue), sliderRoot.maximumValue) return Math.min(Math.max(value, sliderRoot.minimumValue), sliderRoot.maximumValue)
} }
// slider track // slider track
Rectangle { Rectangle
{
id: track id: track
width: sliderRoot.width - sliderRoot.handleSize width: sliderRoot.width - sliderRoot.handleSize
@ -67,7 +74,8 @@ Item {
} }
// Progress indicator // Progress indicator
Item { Item
{
id: rangeHandle id: rangeHandle
x: handle.width x: handle.width
@ -76,7 +84,8 @@ Item {
anchors.verticalCenter: sliderRoot.verticalCenter anchors.verticalCenter: sliderRoot.verticalCenter
visible: sliderRoot.pathsVisible visible: sliderRoot.pathsVisible
Rectangle { Rectangle
{
height: sliderRoot.trackThickness - 2 * sliderRoot.trackBorderWidth height: sliderRoot.trackThickness - 2 * sliderRoot.trackBorderWidth
width: parent.width + sliderRoot.handleSize width: parent.width + sliderRoot.handleSize
anchors.centerIn: parent anchors.centerIn: parent
@ -85,7 +94,8 @@ Item {
} }
// Handle // Handle
Rectangle { Rectangle
{
id: handle id: handle
x: sliderRoot.handleSize x: sliderRoot.handleSize
@ -96,7 +106,9 @@ Item {
color: handleLabel.activeFocus ? sliderRoot.handleActiveColor : sliderRoot.handleColor color: handleLabel.activeFocus ? sliderRoot.handleActiveColor : sliderRoot.handleColor
visible: sliderRoot.pathsVisible visible: sliderRoot.pathsVisible
function onHandleDragged () { function onHandleDragged()
{
sliderRoot.manuallyChanged = true
// update the range handle // update the range handle
sliderRoot.updateRangeHandle() sliderRoot.updateRangeHandle()
@ -106,15 +118,23 @@ Item {
} }
// get the value based on the slider position // get the value based on the slider position
function getValue () { function getValue()
{
var result = x / (sliderRoot.width - sliderRoot.handleSize) var result = x / (sliderRoot.width - sliderRoot.handleSize)
result = result * sliderRoot.maximumValue result = result * sliderRoot.maximumValue
result = sliderRoot.roundValues ? Math.round(result) : result result = sliderRoot.roundValues ? Math.round(result) : result
return result return result
} }
function setValueManually(value)
{
sliderRoot.manuallyChanged = true
handle.setValue(value)
}
// set the slider position based on the value // set the slider position based on the value
function setValue (value) { function setValue(value)
{
// Normalize values between range, since using arrow keys will create out-of-the-range values // Normalize values between range, since using arrow keys will create out-of-the-range values
value = sliderRoot.normalizeValue(value) value = sliderRoot.normalizeValue(value)
@ -132,23 +152,23 @@ Item {
Keys.onLeftPressed: handleLabel.setValue(handleLabel.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1)) Keys.onLeftPressed: handleLabel.setValue(handleLabel.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
// dragging // dragging
MouseArea { MouseArea
{
anchors.fill: parent anchors.fill: parent
drag { drag
{
target: parent target: parent
axis: Drag.XAxis axis: Drag.XAxis
minimumX: 0 minimumX: 0
maximumX: sliderRoot.width - sliderRoot.handleSize maximumX: sliderRoot.width - sliderRoot.handleSize
} }
onPressed: { onPressed: handleLabel.forceActiveFocus()
handleLabel.forceActiveFocus()
}
onPositionChanged: parent.onHandleDragged() onPositionChanged: parent.onHandleDragged()
} }
SimulationSliderLabel { SimulationSliderLabel
{
id: handleLabel id: handleLabel
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
@ -162,7 +182,7 @@ Item {
maximumValue: sliderRoot.maximumValue maximumValue: sliderRoot.maximumValue
value: sliderRoot.handleValue value: sliderRoot.handleValue
busy: UM.SimulationView.busy busy: UM.SimulationView.busy
setValue: handle.setValue // connect callback functions setValue: handle.setValueManually // connect callback functions
} }
} }
} }

View file

@ -44,12 +44,11 @@ UM.PointingRectangle {
id: valueLabel id: valueLabel
anchors { anchors {
left: parent.left
leftMargin: Math.round(UM.Theme.getSize("default_margin").width / 2)
verticalCenter: parent.verticalCenter verticalCenter: parent.verticalCenter
horizontalCenter: parent.horizontalCenter
} }
width: maximumValue.toString().length * 12 * screenScaleFactor width: (maximumValue.toString().length + 1) * 10 * screenScaleFactor
text: sliderLabelRoot.value + startFrom // the current handle value, add 1 because layers is an array text: sliderLabelRoot.value + startFrom // the current handle value, add 1 because layers is an array
horizontalAlignment: TextInput.AlignRight horizontalAlignment: TextInput.AlignRight

View file

@ -18,10 +18,13 @@ from UM.Platform import Platform
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
from UM.Resources import Resources from UM.Resources import Resources
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.Selection import Selection from UM.Scene.Selection import Selection
from UM.Signal import Signal from UM.Signal import Signal
from UM.View.GL.OpenGL import OpenGL from UM.View.GL.OpenGL import OpenGL
from UM.View.GL.OpenGLContext import OpenGLContext from UM.View.GL.OpenGLContext import OpenGLContext
from UM.View.View import View from UM.View.View import View
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from cura.Scene.ConvexHullNode import ConvexHullNode from cura.Scene.ConvexHullNode import ConvexHullNode
@ -30,11 +33,20 @@ from cura.CuraApplication import CuraApplication
from .NozzleNode import NozzleNode from .NozzleNode import NozzleNode
from .SimulationPass import SimulationPass from .SimulationPass import SimulationPass
from .SimulationViewProxy import SimulationViewProxy from .SimulationViewProxy import SimulationViewProxy
import numpy
import os.path
from typing import Optional, TYPE_CHECKING, List
if TYPE_CHECKING:
from UM.Scene.SceneNode import SceneNode
from UM.Scene.Scene import Scene
from UM.View.GL.ShaderProgram import ShaderProgram
from UM.View.RenderPass import RenderPass
from UM.Settings.ContainerStack import ContainerStack
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
import numpy
import os.path
## View used to display g-code paths. ## View used to display g-code paths.
class SimulationView(View): class SimulationView(View):
@ -44,7 +56,7 @@ class SimulationView(View):
LAYER_VIEW_TYPE_FEEDRATE = 2 LAYER_VIEW_TYPE_FEEDRATE = 2
LAYER_VIEW_TYPE_THICKNESS = 3 LAYER_VIEW_TYPE_THICKNESS = 3
def __init__(self): def __init__(self) -> None:
super().__init__() super().__init__()
self._max_layers = 0 self._max_layers = 0
@ -64,21 +76,21 @@ class SimulationView(View):
self._busy = False self._busy = False
self._simulation_running = False self._simulation_running = False
self._ghost_shader = None self._ghost_shader = None # type: Optional["ShaderProgram"]
self._layer_pass = None self._layer_pass = None # type: Optional[SimulationPass]
self._composite_pass = None self._composite_pass = None # type: Optional[RenderPass]
self._old_layer_bindings = None self._old_layer_bindings = None
self._simulationview_composite_shader = None self._simulationview_composite_shader = None # type: Optional["ShaderProgram"]
self._old_composite_shader = None self._old_composite_shader = None
self._global_container_stack = None self._global_container_stack = None # type: Optional[ContainerStack]
self._proxy = SimulationViewProxy() self._proxy = SimulationViewProxy()
self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged) self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged)
self._resetSettings() self._resetSettings()
self._legend_items = None self._legend_items = None
self._show_travel_moves = False self._show_travel_moves = False
self._nozzle_node = None self._nozzle_node = None # type: Optional[NozzleNode]
Application.getInstance().getPreferences().addPreference("view/top_layer_count", 5) Application.getInstance().getPreferences().addPreference("view/top_layer_count", 5)
Application.getInstance().getPreferences().addPreference("view/only_show_top_layers", False) Application.getInstance().getPreferences().addPreference("view/only_show_top_layers", False)
@ -102,29 +114,29 @@ class SimulationView(View):
self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled"), self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled"),
title = catalog.i18nc("@info:title", "Simulation View")) title = catalog.i18nc("@info:title", "Simulation View"))
def _evaluateCompatibilityMode(self): def _evaluateCompatibilityMode(self) -> bool:
return OpenGLContext.isLegacyOpenGL() or bool(Application.getInstance().getPreferences().getValue("view/force_layer_view_compatibility_mode")) return OpenGLContext.isLegacyOpenGL() or bool(Application.getInstance().getPreferences().getValue("view/force_layer_view_compatibility_mode"))
def _resetSettings(self): def _resetSettings(self) -> None:
self._layer_view_type = 0 # 0 is material color, 1 is color by linetype, 2 is speed, 3 is layer thickness self._layer_view_type = 0 # type: int # 0 is material color, 1 is color by linetype, 2 is speed, 3 is layer thickness
self._extruder_count = 0 self._extruder_count = 0
self._extruder_opacity = [1.0, 1.0, 1.0, 1.0] self._extruder_opacity = [1.0, 1.0, 1.0, 1.0]
self._show_travel_moves = 0 self._show_travel_moves = False
self._show_helpers = 1 self._show_helpers = True
self._show_skin = 1 self._show_skin = True
self._show_infill = 1 self._show_infill = True
self.resetLayerData() self.resetLayerData()
def getActivity(self): def getActivity(self) -> bool:
return self._activity return self._activity
def setActivity(self, activity): def setActivity(self, activity: bool) -> None:
if self._activity == activity: if self._activity == activity:
return return
self._activity = activity self._activity = activity
self.activityChanged.emit() self.activityChanged.emit()
def getSimulationPass(self): def getSimulationPass(self) -> SimulationPass:
if not self._layer_pass: if not self._layer_pass:
# Currently the RenderPass constructor requires a size > 0 # Currently the RenderPass constructor requires a size > 0
# This should be fixed in RenderPass's constructor. # This should be fixed in RenderPass's constructor.
@ -133,30 +145,30 @@ class SimulationView(View):
self._layer_pass.setSimulationView(self) self._layer_pass.setSimulationView(self)
return self._layer_pass return self._layer_pass
def getCurrentLayer(self): def getCurrentLayer(self) -> int:
return self._current_layer_num return self._current_layer_num
def getMinimumLayer(self): def getMinimumLayer(self) -> int:
return self._minimum_layer_num return self._minimum_layer_num
def getMaxLayers(self): def getMaxLayers(self) -> int:
return self._max_layers return self._max_layers
def getCurrentPath(self): def getCurrentPath(self) -> int:
return self._current_path_num return self._current_path_num
def getMinimumPath(self): def getMinimumPath(self) -> int:
return self._minimum_path_num return self._minimum_path_num
def getMaxPaths(self): def getMaxPaths(self) -> int:
return self._max_paths return self._max_paths
def getNozzleNode(self): def getNozzleNode(self) -> NozzleNode:
if not self._nozzle_node: if not self._nozzle_node:
self._nozzle_node = NozzleNode() self._nozzle_node = NozzleNode()
return self._nozzle_node return self._nozzle_node
def _onSceneChanged(self, node): def _onSceneChanged(self, node: "SceneNode") -> None:
if node.getMeshData() is None: if node.getMeshData() is None:
self.resetLayerData() self.resetLayerData()
@ -164,21 +176,21 @@ class SimulationView(View):
self.calculateMaxLayers() self.calculateMaxLayers()
self.calculateMaxPathsOnLayer(self._current_layer_num) self.calculateMaxPathsOnLayer(self._current_layer_num)
def isBusy(self): def isBusy(self) -> bool:
return self._busy return self._busy
def setBusy(self, busy): def setBusy(self, busy: bool) -> None:
if busy != self._busy: if busy != self._busy:
self._busy = busy self._busy = busy
self.busyChanged.emit() self.busyChanged.emit()
def isSimulationRunning(self): def isSimulationRunning(self) -> bool:
return self._simulation_running return self._simulation_running
def setSimulationRunning(self, running): def setSimulationRunning(self, running: bool) -> None:
self._simulation_running = running self._simulation_running = running
def resetLayerData(self): def resetLayerData(self) -> None:
self._current_layer_mesh = None self._current_layer_mesh = None
self._current_layer_jumps = None self._current_layer_jumps = None
self._max_feedrate = sys.float_info.min self._max_feedrate = sys.float_info.min
@ -186,7 +198,7 @@ class SimulationView(View):
self._max_thickness = sys.float_info.min self._max_thickness = sys.float_info.min
self._min_thickness = sys.float_info.max self._min_thickness = sys.float_info.max
def beginRendering(self): def beginRendering(self) -> None:
scene = self.getController().getScene() scene = self.getController().getScene()
renderer = self.getRenderer() renderer = self.getRenderer()
@ -204,7 +216,7 @@ class SimulationView(View):
if (node.getMeshData()) and node.isVisible(): if (node.getMeshData()) and node.isVisible():
renderer.queueNode(node, transparent = True, shader = self._ghost_shader) renderer.queueNode(node, transparent = True, shader = self._ghost_shader)
def setLayer(self, value): def setLayer(self, value: int) -> None:
if self._current_layer_num != value: if self._current_layer_num != value:
self._current_layer_num = value self._current_layer_num = value
if self._current_layer_num < 0: if self._current_layer_num < 0:
@ -218,7 +230,7 @@ class SimulationView(View):
self.currentLayerNumChanged.emit() self.currentLayerNumChanged.emit()
def setMinimumLayer(self, value): def setMinimumLayer(self, value: int) -> None:
if self._minimum_layer_num != value: if self._minimum_layer_num != value:
self._minimum_layer_num = value self._minimum_layer_num = value
if self._minimum_layer_num < 0: if self._minimum_layer_num < 0:
@ -232,7 +244,7 @@ class SimulationView(View):
self.currentLayerNumChanged.emit() self.currentLayerNumChanged.emit()
def setPath(self, value): def setPath(self, value: int) -> None:
if self._current_path_num != value: if self._current_path_num != value:
self._current_path_num = value self._current_path_num = value
if self._current_path_num < 0: if self._current_path_num < 0:
@ -246,7 +258,7 @@ class SimulationView(View):
self.currentPathNumChanged.emit() self.currentPathNumChanged.emit()
def setMinimumPath(self, value): def setMinimumPath(self, value: int) -> None:
if self._minimum_path_num != value: if self._minimum_path_num != value:
self._minimum_path_num = value self._minimum_path_num = value
if self._minimum_path_num < 0: if self._minimum_path_num < 0:
@ -263,24 +275,24 @@ class SimulationView(View):
## Set the layer view type ## Set the layer view type
# #
# \param layer_view_type integer as in SimulationView.qml and this class # \param layer_view_type integer as in SimulationView.qml and this class
def setSimulationViewType(self, layer_view_type): def setSimulationViewType(self, layer_view_type: int) -> None:
self._layer_view_type = layer_view_type self._layer_view_type = layer_view_type
self.currentLayerNumChanged.emit() self.currentLayerNumChanged.emit()
## Return the layer view type, integer as in SimulationView.qml and this class ## Return the layer view type, integer as in SimulationView.qml and this class
def getSimulationViewType(self): def getSimulationViewType(self) -> int:
return self._layer_view_type return self._layer_view_type
## Set the extruder opacity ## Set the extruder opacity
# #
# \param extruder_nr 0..3 # \param extruder_nr 0..3
# \param opacity 0.0 .. 1.0 # \param opacity 0.0 .. 1.0
def setExtruderOpacity(self, extruder_nr, opacity): def setExtruderOpacity(self, extruder_nr: int, opacity: float) -> None:
if 0 <= extruder_nr <= 3: if 0 <= extruder_nr <= 3:
self._extruder_opacity[extruder_nr] = opacity self._extruder_opacity[extruder_nr] = opacity
self.currentLayerNumChanged.emit() self.currentLayerNumChanged.emit()
def getExtruderOpacities(self): def getExtruderOpacities(self)-> List[float]:
return self._extruder_opacity return self._extruder_opacity
def setShowTravelMoves(self, show): def setShowTravelMoves(self, show):
@ -290,46 +302,46 @@ class SimulationView(View):
def getShowTravelMoves(self): def getShowTravelMoves(self):
return self._show_travel_moves return self._show_travel_moves
def setShowHelpers(self, show): def setShowHelpers(self, show: bool) -> None:
self._show_helpers = show self._show_helpers = show
self.currentLayerNumChanged.emit() self.currentLayerNumChanged.emit()
def getShowHelpers(self): def getShowHelpers(self) -> bool:
return self._show_helpers return self._show_helpers
def setShowSkin(self, show): def setShowSkin(self, show: bool) -> None:
self._show_skin = show self._show_skin = show
self.currentLayerNumChanged.emit() self.currentLayerNumChanged.emit()
def getShowSkin(self): def getShowSkin(self) -> bool:
return self._show_skin return self._show_skin
def setShowInfill(self, show): def setShowInfill(self, show: bool) -> None:
self._show_infill = show self._show_infill = show
self.currentLayerNumChanged.emit() self.currentLayerNumChanged.emit()
def getShowInfill(self): def getShowInfill(self) -> bool:
return self._show_infill return self._show_infill
def getCompatibilityMode(self): def getCompatibilityMode(self) -> bool:
return self._compatibility_mode return self._compatibility_mode
def getExtruderCount(self): def getExtruderCount(self) -> int:
return self._extruder_count return self._extruder_count
def getMinFeedrate(self): def getMinFeedrate(self) -> float:
return self._min_feedrate return self._min_feedrate
def getMaxFeedrate(self): def getMaxFeedrate(self) -> float:
return self._max_feedrate return self._max_feedrate
def getMinThickness(self): def getMinThickness(self) -> float:
return self._min_thickness return self._min_thickness
def getMaxThickness(self): def getMaxThickness(self) -> float:
return self._max_thickness return self._max_thickness
def calculateMaxLayers(self): def calculateMaxLayers(self) -> None:
scene = self.getController().getScene() scene = self.getController().getScene()
self._old_max_layers = self._max_layers self._old_max_layers = self._max_layers
@ -383,7 +395,7 @@ class SimulationView(View):
self.maxLayersChanged.emit() self.maxLayersChanged.emit()
self._startUpdateTopLayers() self._startUpdateTopLayers()
def calculateMaxPathsOnLayer(self, layer_num): def calculateMaxPathsOnLayer(self, layer_num: int) -> None:
# Update the currentPath # Update the currentPath
scene = self.getController().getScene() scene = self.getController().getScene()
for node in DepthFirstIterator(scene.getRoot()): for node in DepthFirstIterator(scene.getRoot()):
@ -415,10 +427,10 @@ class SimulationView(View):
def getProxy(self, engine, script_engine): def getProxy(self, engine, script_engine):
return self._proxy return self._proxy
def endRendering(self): def endRendering(self) -> None:
pass pass
def event(self, event): def event(self, event) -> bool:
modifiers = QApplication.keyboardModifiers() modifiers = QApplication.keyboardModifiers()
ctrl_is_active = modifiers & Qt.ControlModifier ctrl_is_active = modifiers & Qt.ControlModifier
shift_is_active = modifiers & Qt.ShiftModifier shift_is_active = modifiers & Qt.ShiftModifier
@ -447,7 +459,7 @@ class SimulationView(View):
if QOpenGLContext.currentContext() is None: if QOpenGLContext.currentContext() is None:
Logger.log("d", "current context of OpenGL is empty on Mac OS X, will try to create shaders later") Logger.log("d", "current context of OpenGL is empty on Mac OS X, will try to create shaders later")
CuraApplication.getInstance().callLater(lambda e=event: self.event(e)) CuraApplication.getInstance().callLater(lambda e=event: self.event(e))
return return False
# Make sure the SimulationPass is created # Make sure the SimulationPass is created
layer_pass = self.getSimulationPass() layer_pass = self.getSimulationPass()
@ -480,11 +492,14 @@ class SimulationView(View):
Application.getInstance().globalContainerStackChanged.disconnect(self._onGlobalStackChanged) Application.getInstance().globalContainerStackChanged.disconnect(self._onGlobalStackChanged)
if self._global_container_stack: if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged) self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
if self._nozzle_node:
self._nozzle_node.setParent(None) self._nozzle_node.setParent(None)
self.getRenderer().removeRenderPass(self._layer_pass) self.getRenderer().removeRenderPass(self._layer_pass)
self._composite_pass.setLayerBindings(self._old_layer_bindings) if self._composite_pass:
self._composite_pass.setCompositeShader(self._old_composite_shader) self._composite_pass.setLayerBindings(self._old_layer_bindings)
self._composite_pass.setCompositeShader(self._old_composite_shader)
return False
def getCurrentLayerMesh(self): def getCurrentLayerMesh(self):
return self._current_layer_mesh return self._current_layer_mesh
@ -492,7 +507,7 @@ class SimulationView(View):
def getCurrentLayerJumps(self): def getCurrentLayerJumps(self):
return self._current_layer_jumps return self._current_layer_jumps
def _onGlobalStackChanged(self): def _onGlobalStackChanged(self) -> None:
if self._global_container_stack: if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged) self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
self._global_container_stack = Application.getInstance().getGlobalContainerStack() self._global_container_stack = Application.getInstance().getGlobalContainerStack()
@ -504,17 +519,17 @@ class SimulationView(View):
else: else:
self._wireprint_warning_message.hide() self._wireprint_warning_message.hide()
def _onPropertyChanged(self, key, property_name): def _onPropertyChanged(self, key: str, property_name: str) -> None:
if key == "wireframe_enabled" and property_name == "value": if key == "wireframe_enabled" and property_name == "value":
if self._global_container_stack.getProperty("wireframe_enabled", "value"): if self._global_container_stack and self._global_container_stack.getProperty("wireframe_enabled", "value"):
self._wireprint_warning_message.show() self._wireprint_warning_message.show()
else: else:
self._wireprint_warning_message.hide() self._wireprint_warning_message.hide()
def _onCurrentLayerNumChanged(self): def _onCurrentLayerNumChanged(self) -> None:
self.calculateMaxPathsOnLayer(self._current_layer_num) self.calculateMaxPathsOnLayer(self._current_layer_num)
def _startUpdateTopLayers(self): def _startUpdateTopLayers(self) -> None:
if not self._compatibility_mode: if not self._compatibility_mode:
return return
@ -525,10 +540,10 @@ class SimulationView(View):
self.setBusy(True) self.setBusy(True)
self._top_layers_job = _CreateTopLayersJob(self._controller.getScene(), self._current_layer_num, self._solid_layers) self._top_layers_job = _CreateTopLayersJob(self._controller.getScene(), self._current_layer_num, self._solid_layers)
self._top_layers_job.finished.connect(self._updateCurrentLayerMesh) self._top_layers_job.finished.connect(self._updateCurrentLayerMesh) # type: ignore # mypy doesn't understand the whole private class thing that's going on here.
self._top_layers_job.start() self._top_layers_job.start() # type: ignore
def _updateCurrentLayerMesh(self, job): def _updateCurrentLayerMesh(self, job: "_CreateTopLayersJob") -> None:
self.setBusy(False) self.setBusy(False)
if not job.getResult(): if not job.getResult():
@ -539,9 +554,9 @@ class SimulationView(View):
self._current_layer_jumps = job.getResult().get("jumps") self._current_layer_jumps = job.getResult().get("jumps")
self._controller.getScene().sceneChanged.emit(self._controller.getScene().getRoot()) self._controller.getScene().sceneChanged.emit(self._controller.getScene().getRoot())
self._top_layers_job = None self._top_layers_job = None # type: Optional["_CreateTopLayersJob"]
def _updateWithPreferences(self): def _updateWithPreferences(self) -> None:
self._solid_layers = int(Application.getInstance().getPreferences().getValue("view/top_layer_count")) self._solid_layers = int(Application.getInstance().getPreferences().getValue("view/top_layer_count"))
self._only_show_top_layers = bool(Application.getInstance().getPreferences().getValue("view/only_show_top_layers")) self._only_show_top_layers = bool(Application.getInstance().getPreferences().getValue("view/only_show_top_layers"))
self._compatibility_mode = self._evaluateCompatibilityMode() self._compatibility_mode = self._evaluateCompatibilityMode()
@ -563,7 +578,7 @@ class SimulationView(View):
self._startUpdateTopLayers() self._startUpdateTopLayers()
self.preferencesChanged.emit() self.preferencesChanged.emit()
def _onPreferencesChanged(self, preference): def _onPreferencesChanged(self, preference: str) -> None:
if preference not in { if preference not in {
"view/top_layer_count", "view/top_layer_count",
"view/only_show_top_layers", "view/only_show_top_layers",
@ -581,7 +596,7 @@ class SimulationView(View):
class _CreateTopLayersJob(Job): class _CreateTopLayersJob(Job):
def __init__(self, scene, layer_number, solid_layers): def __init__(self, scene: "Scene", layer_number: int, solid_layers: int) -> None:
super().__init__() super().__init__()
self._scene = scene self._scene = scene
@ -589,7 +604,7 @@ class _CreateTopLayersJob(Job):
self._solid_layers = solid_layers self._solid_layers = solid_layers
self._cancel = False self._cancel = False
def run(self): def run(self) -> None:
layer_data = None layer_data = None
for node in DepthFirstIterator(self._scene.getRoot()): for node in DepthFirstIterator(self._scene.getRoot()):
layer_data = node.callDecoration("getLayerData") layer_data = node.callDecoration("getLayerData")
@ -638,6 +653,6 @@ class _CreateTopLayersJob(Job):
self.setResult({"layers": layer_mesh.build(), "jumps": jump_mesh}) self.setResult({"layers": layer_mesh.build(), "jumps": jump_mesh})
def cancel(self): def cancel(self) -> None:
self._cancel = True self._cancel = True
super().cancel() super().cancel()

View file

@ -1,4 +1,4 @@
// Copyright (c) 2017 Ultimaker B.V. // Copyright (c) 2018 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.
import QtQuick 2.4 import QtQuick 2.4
@ -12,30 +12,43 @@ import Cura 1.0 as Cura
Item Item
{ {
id: base id: base
width: { width:
if (UM.SimulationView.compatibilityMode) { {
if (UM.SimulationView.compatibilityMode)
{
return UM.Theme.getSize("layerview_menu_size_compatibility").width; return UM.Theme.getSize("layerview_menu_size_compatibility").width;
} else { }
else
{
return UM.Theme.getSize("layerview_menu_size").width; return UM.Theme.getSize("layerview_menu_size").width;
} }
} }
height: { height: {
if (viewSettings.collapsed) { if (viewSettings.collapsed)
if (UM.SimulationView.compatibilityMode) { {
if (UM.SimulationView.compatibilityMode)
{
return UM.Theme.getSize("layerview_menu_size_compatibility_collapsed").height; return UM.Theme.getSize("layerview_menu_size_compatibility_collapsed").height;
} }
return UM.Theme.getSize("layerview_menu_size_collapsed").height; return UM.Theme.getSize("layerview_menu_size_collapsed").height;
} else if (UM.SimulationView.compatibilityMode) { }
else if (UM.SimulationView.compatibilityMode)
{
return UM.Theme.getSize("layerview_menu_size_compatibility").height; return UM.Theme.getSize("layerview_menu_size_compatibility").height;
} else if (UM.Preferences.getValue("layerview/layer_view_type") == 0) { }
else if (UM.Preferences.getValue("layerview/layer_view_type") == 0)
{
return UM.Theme.getSize("layerview_menu_size_material_color_mode").height + UM.SimulationView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height) return UM.Theme.getSize("layerview_menu_size_material_color_mode").height + UM.SimulationView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height)
} else { }
else
{
return UM.Theme.getSize("layerview_menu_size").height + UM.SimulationView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height) return UM.Theme.getSize("layerview_menu_size").height + UM.SimulationView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height)
} }
} }
Behavior on height { NumberAnimation { duration: 100 } } Behavior on height { NumberAnimation { duration: 100 } }
property var buttonTarget: { property var buttonTarget:
{
if(parent != null) if(parent != null)
{ {
var force_binding = parent.y; // ensure this gets reevaluated when the panel moves var force_binding = parent.y; // ensure this gets reevaluated when the panel moves
@ -44,7 +57,8 @@ Item
return Qt.point(0,0) return Qt.point(0,0)
} }
Rectangle { Rectangle
{
id: layerViewMenu id: layerViewMenu
anchors.right: parent.right anchors.right: parent.right
anchors.top: parent.top anchors.top: parent.top
@ -83,7 +97,8 @@ Item
} }
} }
ColumnLayout { ColumnLayout
{
id: viewSettings id: viewSettings
property bool collapsed: false property bool collapsed: false
@ -195,7 +210,8 @@ Item
width: width width: width
} }
Connections { Connections
{
target: UM.Preferences target: UM.Preferences
onPreferenceChanged: onPreferenceChanged:
{ {
@ -212,18 +228,22 @@ Item
} }
} }
Repeater { Repeater
{
model: Cura.ExtrudersModel{} model: Cura.ExtrudersModel{}
CheckBox { CheckBox
{
id: extrudersModelCheckBox id: extrudersModelCheckBox
checked: viewSettings.extruder_opacities[index] > 0.5 || viewSettings.extruder_opacities[index] == undefined || viewSettings.extruder_opacities[index] == "" checked: viewSettings.extruder_opacities[index] > 0.5 || viewSettings.extruder_opacities[index] == undefined || viewSettings.extruder_opacities[index] == ""
onClicked: { onClicked:
{
viewSettings.extruder_opacities[index] = checked ? 1.0 : 0.0 viewSettings.extruder_opacities[index] = checked ? 1.0 : 0.0
UM.Preferences.setValue("layerview/extruder_opacities", viewSettings.extruder_opacities.join("|")); UM.Preferences.setValue("layerview/extruder_opacities", viewSettings.extruder_opacities.join("|"));
} }
visible: !UM.SimulationView.compatibilityMode visible: !UM.SimulationView.compatibilityMode
enabled: index + 1 <= 4 enabled: index + 1 <= 4
Rectangle { Rectangle
{
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.right: extrudersModelCheckBox.right anchors.right: extrudersModelCheckBox.right
width: UM.Theme.getSize("layerview_legend_size").width width: UM.Theme.getSize("layerview_legend_size").width
@ -253,8 +273,10 @@ Item
} }
} }
Repeater { Repeater
model: ListModel { {
model: ListModel
{
id: typesLegendModel id: typesLegendModel
Component.onCompleted: Component.onCompleted:
{ {
@ -285,13 +307,16 @@ Item
} }
} }
CheckBox { CheckBox
{
id: legendModelCheckBox id: legendModelCheckBox
checked: model.initialValue checked: model.initialValue
onClicked: { onClicked:
{
UM.Preferences.setValue(model.preference, checked); UM.Preferences.setValue(model.preference, checked);
} }
Rectangle { Rectangle
{
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.right: legendModelCheckBox.right anchors.right: legendModelCheckBox.right
width: UM.Theme.getSize("layerview_legend_size").width width: UM.Theme.getSize("layerview_legend_size").width
@ -320,18 +345,22 @@ Item
} }
} }
CheckBox { CheckBox
{
checked: viewSettings.only_show_top_layers checked: viewSettings.only_show_top_layers
onClicked: { onClicked:
{
UM.Preferences.setValue("view/only_show_top_layers", checked ? 1.0 : 0.0); UM.Preferences.setValue("view/only_show_top_layers", checked ? 1.0 : 0.0);
} }
text: catalog.i18nc("@label", "Only Show Top Layers") text: catalog.i18nc("@label", "Only Show Top Layers")
visible: UM.SimulationView.compatibilityMode visible: UM.SimulationView.compatibilityMode
style: UM.Theme.styles.checkbox style: UM.Theme.styles.checkbox
} }
CheckBox { CheckBox
{
checked: viewSettings.top_layer_count == 5 checked: viewSettings.top_layer_count == 5
onClicked: { onClicked:
{
UM.Preferences.setValue("view/top_layer_count", checked ? 5 : 1); UM.Preferences.setValue("view/top_layer_count", checked ? 5 : 1);
} }
text: catalog.i18nc("@label", "Show 5 Detailed Layers On Top") text: catalog.i18nc("@label", "Show 5 Detailed Layers On Top")
@ -339,8 +368,10 @@ Item
style: UM.Theme.styles.checkbox style: UM.Theme.styles.checkbox
} }
Repeater { Repeater
model: ListModel { {
model: ListModel
{
id: typesLegendModelNoCheck id: typesLegendModelNoCheck
Component.onCompleted: Component.onCompleted:
{ {
@ -355,11 +386,13 @@ Item
} }
} }
Label { Label
{
text: label text: label
visible: viewSettings.show_legend visible: viewSettings.show_legend
id: typesLegendModelLabel id: typesLegendModelLabel
Rectangle { Rectangle
{
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
anchors.right: typesLegendModelLabel.right anchors.right: typesLegendModelLabel.right
width: UM.Theme.getSize("layerview_legend_size").width width: UM.Theme.getSize("layerview_legend_size").width
@ -378,30 +411,37 @@ Item
} }
// Text for the minimum, maximum and units for the feedrates and layer thickness // Text for the minimum, maximum and units for the feedrates and layer thickness
Item { Item
{
id: gradientLegend id: gradientLegend
visible: viewSettings.show_gradient visible: viewSettings.show_gradient
width: parent.width width: parent.width
height: UM.Theme.getSize("layerview_row").height height: UM.Theme.getSize("layerview_row").height
anchors { anchors
{
topMargin: UM.Theme.getSize("slider_layerview_margin").height topMargin: UM.Theme.getSize("slider_layerview_margin").height
horizontalCenter: parent.horizontalCenter horizontalCenter: parent.horizontalCenter
} }
Label { Label
{
text: minText() text: minText()
anchors.left: parent.left anchors.left: parent.left
color: UM.Theme.getColor("setting_control_text") color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default") font: UM.Theme.getFont("default")
function minText() { function minText()
if (UM.SimulationView.layerActivity && CuraApplication.platformActivity) { {
if (UM.SimulationView.layerActivity && CuraApplication.platformActivity)
{
// Feedrate selected // Feedrate selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 2) { if (UM.Preferences.getValue("layerview/layer_view_type") == 2)
{
return parseFloat(UM.SimulationView.getMinFeedrate()).toFixed(2) return parseFloat(UM.SimulationView.getMinFeedrate()).toFixed(2)
} }
// Layer thickness selected // Layer thickness selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 3) { if (UM.Preferences.getValue("layerview/layer_view_type") == 3)
{
return parseFloat(UM.SimulationView.getMinThickness()).toFixed(2) return parseFloat(UM.SimulationView.getMinThickness()).toFixed(2)
} }
} }
@ -409,20 +449,25 @@ Item
} }
} }
Label { Label
{
text: unitsText() text: unitsText()
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
color: UM.Theme.getColor("setting_control_text") color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default") font: UM.Theme.getFont("default")
function unitsText() { function unitsText()
if (UM.SimulationView.layerActivity && CuraApplication.platformActivity) { {
if (UM.SimulationView.layerActivity && CuraApplication.platformActivity)
{
// Feedrate selected // Feedrate selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 2) { if (UM.Preferences.getValue("layerview/layer_view_type") == 2)
{
return "mm/s" return "mm/s"
} }
// Layer thickness selected // Layer thickness selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 3) { if (UM.Preferences.getValue("layerview/layer_view_type") == 3)
{
return "mm" return "mm"
} }
} }
@ -430,20 +475,25 @@ Item
} }
} }
Label { Label
{
text: maxText() text: maxText()
anchors.right: parent.right anchors.right: parent.right
color: UM.Theme.getColor("setting_control_text") color: UM.Theme.getColor("setting_control_text")
font: UM.Theme.getFont("default") font: UM.Theme.getFont("default")
function maxText() { function maxText()
if (UM.SimulationView.layerActivity && CuraApplication.platformActivity) { {
if (UM.SimulationView.layerActivity && CuraApplication.platformActivity)
{
// Feedrate selected // Feedrate selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 2) { if (UM.Preferences.getValue("layerview/layer_view_type") == 2)
{
return parseFloat(UM.SimulationView.getMaxFeedrate()).toFixed(2) return parseFloat(UM.SimulationView.getMaxFeedrate()).toFixed(2)
} }
// Layer thickness selected // Layer thickness selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 3) { if (UM.Preferences.getValue("layerview/layer_view_type") == 3)
{
return parseFloat(UM.SimulationView.getMaxThickness()).toFixed(2) return parseFloat(UM.SimulationView.getMaxThickness()).toFixed(2)
} }
} }
@ -453,7 +503,8 @@ Item
} }
// Gradient colors for feedrate // Gradient colors for feedrate
Rectangle { // In QML 5.9 can be changed by LinearGradient Rectangle
{ // In QML 5.9 can be changed by LinearGradient
// Invert values because then the bar is rotated 90 degrees // Invert values because then the bar is rotated 90 degrees
id: feedrateGradient id: feedrateGradient
visible: viewSettings.show_feedrate_gradient visible: viewSettings.show_feedrate_gradient
@ -463,20 +514,25 @@ Item
border.width: UM.Theme.getSize("default_lining").width border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining") border.color: UM.Theme.getColor("lining")
transform: Rotation {origin.x: 0; origin.y: 0; angle: 90} transform: Rotation {origin.x: 0; origin.y: 0; angle: 90}
gradient: Gradient { gradient: Gradient
GradientStop { {
GradientStop
{
position: 0.000 position: 0.000
color: Qt.rgba(1, 0.5, 0, 1) color: Qt.rgba(1, 0.5, 0, 1)
} }
GradientStop { GradientStop
{
position: 0.625 position: 0.625
color: Qt.rgba(0.375, 0.5, 0, 1) color: Qt.rgba(0.375, 0.5, 0, 1)
} }
GradientStop { GradientStop
{
position: 0.75 position: 0.75
color: Qt.rgba(0.25, 1, 0, 1) color: Qt.rgba(0.25, 1, 0, 1)
} }
GradientStop { GradientStop
{
position: 1.0 position: 1.0
color: Qt.rgba(0, 0, 1, 1) color: Qt.rgba(0, 0, 1, 1)
} }
@ -484,7 +540,8 @@ Item
} }
// Gradient colors for layer thickness (similar to parula colormap) // Gradient colors for layer thickness (similar to parula colormap)
Rectangle { // In QML 5.9 can be changed by LinearGradient Rectangle // In QML 5.9 can be changed by LinearGradient
{
// Invert values because then the bar is rotated 90 degrees // Invert values because then the bar is rotated 90 degrees
id: thicknessGradient id: thicknessGradient
visible: viewSettings.show_thickness_gradient visible: viewSettings.show_thickness_gradient
@ -494,24 +551,30 @@ Item
border.width: UM.Theme.getSize("default_lining").width border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining") border.color: UM.Theme.getColor("lining")
transform: Rotation {origin.x: 0; origin.y: 0; angle: 90} transform: Rotation {origin.x: 0; origin.y: 0; angle: 90}
gradient: Gradient { gradient: Gradient
GradientStop { {
GradientStop
{
position: 0.000 position: 0.000
color: Qt.rgba(1, 1, 0, 1) color: Qt.rgba(1, 1, 0, 1)
} }
GradientStop { GradientStop
{
position: 0.25 position: 0.25
color: Qt.rgba(1, 0.75, 0.25, 1) color: Qt.rgba(1, 0.75, 0.25, 1)
} }
GradientStop { GradientStop
{
position: 0.5 position: 0.5
color: Qt.rgba(0, 0.75, 0.5, 1) color: Qt.rgba(0, 0.75, 0.5, 1)
} }
GradientStop { GradientStop
{
position: 0.75 position: 0.75
color: Qt.rgba(0, 0.375, 0.75, 1) color: Qt.rgba(0, 0.375, 0.75, 1)
} }
GradientStop { GradientStop
{
position: 1.0 position: 1.0
color: Qt.rgba(0, 0, 0.5, 1) color: Qt.rgba(0, 0, 0.5, 1)
} }
@ -520,19 +583,22 @@ Item
} }
} }
Item { Item
{
id: slidersBox id: slidersBox
width: parent.width width: parent.width
visible: UM.SimulationView.layerActivity && CuraApplication.platformActivity visible: UM.SimulationView.layerActivity && CuraApplication.platformActivity
anchors { anchors
{
top: parent.bottom top: parent.bottom
topMargin: UM.Theme.getSize("slider_layerview_margin").height topMargin: UM.Theme.getSize("slider_layerview_margin").height
left: parent.left left: parent.left
} }
PathSlider { PathSlider
{
id: pathSlider id: pathSlider
height: UM.Theme.getSize("slider_handle").width height: UM.Theme.getSize("slider_handle").width
@ -553,25 +619,37 @@ Item
rangeColor: UM.Theme.getColor("slider_groove_fill") rangeColor: UM.Theme.getColor("slider_groove_fill")
// update values when layer data changes // update values when layer data changes
Connections { Connections
{
target: UM.SimulationView target: UM.SimulationView
onMaxPathsChanged: pathSlider.setHandleValue(UM.SimulationView.currentPath) onMaxPathsChanged: pathSlider.setHandleValue(UM.SimulationView.currentPath)
onCurrentPathChanged: pathSlider.setHandleValue(UM.SimulationView.currentPath) onCurrentPathChanged:
{
// Only pause the simulation when the layer was changed manually, not when the simulation is running
if (pathSlider.manuallyChanged)
{
playButton.pauseSimulation()
}
pathSlider.setHandleValue(UM.SimulationView.currentPath)
}
} }
// make sure the slider handlers show the correct value after switching views // make sure the slider handlers show the correct value after switching views
Component.onCompleted: { Component.onCompleted:
{
pathSlider.setHandleValue(UM.SimulationView.currentPath) pathSlider.setHandleValue(UM.SimulationView.currentPath)
} }
} }
LayerSlider { LayerSlider
{
id: layerSlider id: layerSlider
width: UM.Theme.getSize("slider_handle").width width: UM.Theme.getSize("slider_handle").width
height: UM.Theme.getSize("layerview_menu_size").height height: UM.Theme.getSize("layerview_menu_size").height
anchors { anchors
{
top: !UM.SimulationView.compatibilityMode ? pathSlider.bottom : parent.top top: !UM.SimulationView.compatibilityMode ? pathSlider.bottom : parent.top
topMargin: !UM.SimulationView.compatibilityMode ? UM.Theme.getSize("default_margin").height : 0 topMargin: !UM.SimulationView.compatibilityMode ? UM.Theme.getSize("default_margin").height : 0
right: parent.right right: parent.right
@ -593,56 +671,78 @@ Item
handleLabelWidth: UM.Theme.getSize("slider_layerview_background").width handleLabelWidth: UM.Theme.getSize("slider_layerview_background").width
// update values when layer data changes // update values when layer data changes
Connections { Connections
{
target: UM.SimulationView target: UM.SimulationView
onMaxLayersChanged: layerSlider.setUpperValue(UM.SimulationView.currentLayer) onMaxLayersChanged: layerSlider.setUpperValue(UM.SimulationView.currentLayer)
onMinimumLayerChanged: layerSlider.setLowerValue(UM.SimulationView.minimumLayer) onMinimumLayerChanged: layerSlider.setLowerValue(UM.SimulationView.minimumLayer)
onCurrentLayerChanged: layerSlider.setUpperValue(UM.SimulationView.currentLayer) onCurrentLayerChanged:
{
// Only pause the simulation when the layer was changed manually, not when the simulation is running
if (layerSlider.manuallyChanged)
{
playButton.pauseSimulation()
}
layerSlider.setUpperValue(UM.SimulationView.currentLayer)
}
} }
// make sure the slider handlers show the correct value after switching views // make sure the slider handlers show the correct value after switching views
Component.onCompleted: { Component.onCompleted:
{
layerSlider.setLowerValue(UM.SimulationView.minimumLayer) layerSlider.setLowerValue(UM.SimulationView.minimumLayer)
layerSlider.setUpperValue(UM.SimulationView.currentLayer) layerSlider.setUpperValue(UM.SimulationView.currentLayer)
} }
} }
// Play simulation button // Play simulation button
Button { Button
{
id: playButton id: playButton
iconSource: "./resources/simulation_resume.svg" iconSource: "./resources/simulation_resume.svg"
style: UM.Theme.styles.small_tool_button style: UM.Theme.styles.small_tool_button
visible: !UM.SimulationView.compatibilityMode visible: !UM.SimulationView.compatibilityMode
anchors { anchors
{
verticalCenter: pathSlider.verticalCenter verticalCenter: pathSlider.verticalCenter
} }
property var status: 0 // indicates if it's stopped (0) or playing (1) property var status: 0 // indicates if it's stopped (0) or playing (1)
onClicked: { onClicked:
switch(status) { {
case 0: { switch(status)
{
case 0:
{
resumeSimulation() resumeSimulation()
break break
} }
case 1: { case 1:
{
pauseSimulation() pauseSimulation()
break break
} }
} }
} }
function pauseSimulation() { function pauseSimulation()
{
UM.SimulationView.setSimulationRunning(false) UM.SimulationView.setSimulationRunning(false)
iconSource = "./resources/simulation_resume.svg" iconSource = "./resources/simulation_resume.svg"
simulationTimer.stop() simulationTimer.stop()
status = 0 status = 0
layerSlider.manuallyChanged = true
pathSlider.manuallyChanged = true
} }
function resumeSimulation() { function resumeSimulation()
{
UM.SimulationView.setSimulationRunning(true) UM.SimulationView.setSimulationRunning(true)
iconSource = "./resources/simulation_pause.svg" iconSource = "./resources/simulation_pause.svg"
simulationTimer.start() simulationTimer.start()
layerSlider.manuallyChanged = false
pathSlider.manuallyChanged = false
} }
} }
@ -652,7 +752,8 @@ Item
interval: 100 interval: 100
running: false running: false
repeat: true repeat: true
onTriggered: { onTriggered:
{
var currentPath = UM.SimulationView.currentPath var currentPath = UM.SimulationView.currentPath
var numPaths = UM.SimulationView.numPaths var numPaths = UM.SimulationView.numPaths
var currentLayer = UM.SimulationView.currentLayer var currentLayer = UM.SimulationView.currentLayer
@ -692,12 +793,15 @@ Item
UM.SimulationView.setCurrentPath(currentPath+1) UM.SimulationView.setCurrentPath(currentPath+1)
} }
} }
// The status must be set here instead of in the resumeSimulation function otherwise it won't work
// correctly, because part of the logic is in this trigger function.
playButton.status = 1 playButton.status = 1
} }
} }
} }
FontMetrics { FontMetrics
{
id: fontMetrics id: fontMetrics
font: UM.Theme.getFont("default") font: UM.Theme.getFont("default")
} }

View file

@ -256,6 +256,7 @@ fragment41core =
out vec4 frag_color; out vec4 frag_color;
uniform mediump vec4 u_ambientColor; uniform mediump vec4 u_ambientColor;
uniform mediump vec4 u_minimumAlbedo;
uniform highp vec3 u_lightPosition; uniform highp vec3 u_lightPosition;
void main() void main()
@ -263,7 +264,7 @@ fragment41core =
mediump vec4 finalColor = vec4(0.0); mediump vec4 finalColor = vec4(0.0);
float alpha = f_color.a; float alpha = f_color.a;
finalColor.rgb += f_color.rgb * 0.3; finalColor.rgb += f_color.rgb * 0.2 + u_minimumAlbedo.rgb;
highp vec3 normal = normalize(f_normal); highp vec3 normal = normalize(f_normal);
highp vec3 light_dir = normalize(u_lightPosition - f_vertex); highp vec3 light_dir = normalize(u_lightPosition - f_vertex);
@ -285,6 +286,7 @@ u_extruder_opacity = [1.0, 1.0, 1.0, 1.0]
u_specularColor = [0.4, 0.4, 0.4, 1.0] u_specularColor = [0.4, 0.4, 0.4, 1.0]
u_ambientColor = [0.3, 0.3, 0.3, 0.0] u_ambientColor = [0.3, 0.3, 0.3, 0.0]
u_diffuseColor = [1.0, 0.79, 0.14, 1.0] u_diffuseColor = [1.0, 0.79, 0.14, 1.0]
u_minimumAlbedo = [0.1, 0.1, 0.1, 1.0]
u_shininess = 20.0 u_shininess = 20.0
u_show_travel_moves = 0 u_show_travel_moves = 0

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides the Simulation view.", "description": "Provides the Simulation view.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -5,6 +5,7 @@ import json
import os import os
import platform import platform
import time import time
from typing import cast, Optional, Set
from PyQt5.QtCore import pyqtSlot, QObject from PyQt5.QtCore import pyqtSlot, QObject
@ -16,7 +17,7 @@ from UM.i18n import i18nCatalog
from UM.Logger import Logger from UM.Logger import Logger
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
from UM.Qt.Duration import DurationFormat from UM.Qt.Duration import DurationFormat
from typing import cast, Optional
from .SliceInfoJob import SliceInfoJob from .SliceInfoJob import SliceInfoJob
@ -95,13 +96,29 @@ class SliceInfo(QObject, Extension):
def setSendSliceInfo(self, enabled: bool): def setSendSliceInfo(self, enabled: bool):
Application.getInstance().getPreferences().setValue("info/send_slice_info", enabled) Application.getInstance().getPreferences().setValue("info/send_slice_info", enabled)
def _getUserModifiedSettingKeys(self) -> list:
from cura.CuraApplication import CuraApplication
application = cast(CuraApplication, Application.getInstance())
machine_manager = application.getMachineManager()
global_stack = machine_manager.activeMachine
user_modified_setting_keys = set() # type: Set[str]
for stack in [global_stack] + list(global_stack.extruders.values()):
# Get all settings in user_changes and quality_changes
all_keys = stack.userChanges.getAllKeys() | stack.qualityChanges.getAllKeys()
user_modified_setting_keys |= all_keys
return list(sorted(user_modified_setting_keys))
def _onWriteStarted(self, output_device): def _onWriteStarted(self, output_device):
try: try:
if not Application.getInstance().getPreferences().getValue("info/send_slice_info"): if not Application.getInstance().getPreferences().getValue("info/send_slice_info"):
Logger.log("d", "'info/send_slice_info' is turned off.") Logger.log("d", "'info/send_slice_info' is turned off.")
return # Do nothing, user does not want to send data return # Do nothing, user does not want to send data
application = Application.getInstance() from cura.CuraApplication import CuraApplication
application = cast(CuraApplication, Application.getInstance())
machine_manager = application.getMachineManager() machine_manager = application.getMachineManager()
print_information = application.getPrintInformation() print_information = application.getPrintInformation()
@ -164,6 +181,8 @@ class SliceInfo(QObject, Extension):
data["quality_profile"] = global_stack.quality.getMetaData().get("quality_type") data["quality_profile"] = global_stack.quality.getMetaData().get("quality_type")
data["user_modified_setting_keys"] = self._getUserModifiedSettingKeys()
data["models"] = [] data["models"] = []
# Listing all files placed on the build plate # Listing all files placed on the build plate
for node in DepthFirstIterator(application.getController().getScene().getRoot()): for node in DepthFirstIterator(application.getController().getScene().getRoot()):

View file

@ -56,6 +56,7 @@
} }
], ],
"quality_profile": "fast", "quality_profile": "fast",
"user_modified_setting_keys": ["layer_height", "wall_line_width", "infill_sparse_density"],
"models": [ "models": [
{ {
"hash": "b72789b9beb5366dff20b1cf501020c3d4d4df7dc2295ecd0fddd0a6436df070", "hash": "b72789b9beb5366dff20b1cf501020c3d4d4df7dc2295ecd0fddd0a6436df070",

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Submits anonymous slice info. Can be disabled through preferences.", "description": "Submits anonymous slice info. Can be disabled through preferences.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides a normal solid mesh view.", "description": "Provides a normal solid mesh view.",
"api": 4, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

Some files were not shown because too many files have changed in this diff Show more