Merge branch 'master' into full_screen_test

This commit is contained in:
Jaime van Kessel 2018-09-28 11:24:22 +02:00 committed by GitHub
commit c2f0a33b42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
226 changed files with 85323 additions and 17316 deletions

69
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,12 +52,55 @@ 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') {
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 { try {
make('test') sh """
cd ..
export PYTHONPATH=.:"${uranium_dir}"
${env.CURA_ENVIRONMENT_PATH}/${branch}/bin/pytest -x --verbose --full-trace --capture=no ./tests
"""
} catch(e) { } catch(e) {
currentBuild.result = "UNSTABLE" 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

@ -17,7 +17,13 @@ class CameraImageProvider(QQuickImageProvider):
if image.isNull(): if image.isNull():
image = QImage() image = QImage()
return image, QSize(15, 15)
except AttributeError:
try:
image = output_device.activeCamera.getImage()
return image, QSize(15, 15) return image, QSize(15, 15)
except AttributeError: except AttributeError:
pass 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
@ -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
@ -367,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)
@ -428,6 +432,7 @@ class CuraApplication(QtApplication):
# Readers & Writers: # Readers & Writers:
"GCodeWriter", "GCodeWriter",
"STLReader", "STLReader",
"3MFWriter",
# Tools: # Tools:
"CameraTool", "CameraTool",
@ -480,7 +485,9 @@ 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)
preferences.addPreference("cura/favorite_materials", ";".join([])) 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") self._need_to_show_user_agreement = not preferences.getValue("general/accepted_user_agreement")
@ -502,6 +509,7 @@ class CuraApplication(QtApplication):
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):
@ -804,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):
@ -826,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
@ -1702,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

@ -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

@ -4,8 +4,7 @@
from collections import defaultdict, OrderedDict from collections import defaultdict, OrderedDict
import copy import copy
import uuid import uuid
import json from typing import Dict, Optional, TYPE_CHECKING, Any, Set, List, cast, Tuple
from typing import Dict, Optional, TYPE_CHECKING
from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot
@ -22,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
@ -47,18 +45,27 @@ class MaterialManager(QObject):
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.
@ -77,15 +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)
self._favorites = set() self._favorites = set() # type: Set[str]
def initialize(self): 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
@ -94,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)
@ -110,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
@ -138,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
@ -155,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:
@ -172,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():
@ -192,18 +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)
self.materialsUpdated.emit()
favorites = self._application.getPreferences().getValue("cura/favorite_materials") favorites = self._application.getPreferences().getValue("cura/favorite_materials")
for item in favorites.split(";"): for item in favorites.split(";"):
self._favorites.add(item) self._favorites.add(item)
self.favoritesUpdated.emit()
def __addMaterialMetadataIntoLookupTree(self, material_metadata: dict) -> None: self.materialsUpdated.emit()
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
@ -290,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)
@ -330,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
@ -338,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":
@ -368,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:
@ -377,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
@ -426,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:
@ -502,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
@ -520,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)
@ -530,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:
@ -582,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
@ -619,18 +634,18 @@ class MaterialManager(QObject):
return new_id return new_id
@pyqtSlot(str) @pyqtSlot(str)
def addFavorite(self, root_material_id: str): def addFavorite(self, root_material_id: str) -> None:
self._favorites.add(root_material_id) self._favorites.add(root_material_id)
self.favoritesUpdated.emit() self.materialsUpdated.emit()
# Ensure all settings are saved. # Ensure all settings are saved.
self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites))) self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites)))
self._application.saveSettings() self._application.saveSettings()
@pyqtSlot(str) @pyqtSlot(str)
def removeFavorite(self, root_material_id: str): def removeFavorite(self, root_material_id: str) -> None:
self._favorites.remove(root_material_id) self._favorites.remove(root_material_id)
self.favoritesUpdated.emit() self.materialsUpdated.emit()
# Ensure all settings are saved. # Ensure all settings are saved.
self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites))) self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(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

@ -34,9 +34,6 @@ class BaseMaterialsModel(ListModel):
# Update this model when list of materials changes # Update this model when list of materials changes
self._material_manager.materialsUpdated.connect(self._update) self._material_manager.materialsUpdated.connect(self._update)
# Update this model when list of favorites changes
self._material_manager.favoritesUpdated.connect(self._update)
self.addRoleName(Qt.UserRole + 1, "root_material_id") self.addRoleName(Qt.UserRole + 1, "root_material_id")
self.addRoleName(Qt.UserRole + 2, "id") self.addRoleName(Qt.UserRole + 2, "id")
self.addRoleName(Qt.UserRole + 3, "GUID") self.addRoleName(Qt.UserRole + 3, "GUID")
@ -113,7 +110,7 @@ class BaseMaterialsModel(ListModel):
## This is another convenience function which is shared by all material ## This is another convenience function which is shared by all material
# models so it's put here to avoid having so much duplicated code. # models so it's put here to avoid having so much duplicated code.
def _createMaterialItem(self, root_material_id, container_node): def _createMaterialItem(self, root_material_id, container_node):
metadata = container_node.metadata metadata = container_node.getMetadata()
item = { item = {
"root_material_id": root_material_id, "root_material_id": root_material_id,
"id": metadata["id"], "id": metadata["id"],

View file

@ -23,7 +23,7 @@ class FavoriteMaterialsModel(BaseMaterialsModel):
item_list = [] item_list = []
for root_material_id, container_node in self._available_materials.items(): for root_material_id, container_node in self._available_materials.items():
metadata = container_node.metadata metadata = container_node.getMetadata()
# 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)):

View file

@ -23,7 +23,7 @@ class GenericMaterialsModel(BaseMaterialsModel):
item_list = [] item_list = []
for root_material_id, container_node in self._available_materials.items(): for root_material_id, container_node in self._available_materials.items():
metadata = container_node.metadata metadata = container_node.getMetadata()
# 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)):

View file

@ -12,7 +12,8 @@ class MaterialTypesModel(ListModel):
super().__init__(parent) super().__init__(parent)
self.addRoleName(Qt.UserRole + 1, "name") self.addRoleName(Qt.UserRole + 1, "name")
self.addRoleName(Qt.UserRole + 2, "colors") self.addRoleName(Qt.UserRole + 2, "brand")
self.addRoleName(Qt.UserRole + 3, "colors")
class MaterialBrandsModel(BaseMaterialsModel): class MaterialBrandsModel(BaseMaterialsModel):
@ -41,21 +42,19 @@ class MaterialBrandsModel(BaseMaterialsModel):
# Part 1: Generate the entire tree of brands -> material types -> spcific materials # Part 1: Generate the entire tree of brands -> material types -> spcific materials
for root_material_id, container_node in self._available_materials.items(): for root_material_id, container_node in self._available_materials.items():
metadata = container_node.metadata
# 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(container_node.getMetaDataEntry("removed", False)):
continue continue
# Add brands we haven't seen yet to the dict, skipping generics # Add brands we haven't seen yet to the dict, skipping generics
brand = metadata["brand"] brand = container_node.getMetaDataEntry("brand", "")
if brand.lower() == "generic": if brand.lower() == "generic":
continue continue
if brand not in brand_group_dict: if brand not in brand_group_dict:
brand_group_dict[brand] = {} brand_group_dict[brand] = {}
# Add material types we haven't seen yet to the dict # Add material types we haven't seen yet to the dict
material_type = metadata["material"] material_type = container_node.getMetaDataEntry("material", "")
if material_type not in brand_group_dict[brand]: if material_type not in brand_group_dict[brand]:
brand_group_dict[brand][material_type] = [] brand_group_dict[brand][material_type] = []
@ -86,6 +85,7 @@ class MaterialBrandsModel(BaseMaterialsModel):
for material_type, material_list in material_dict.items(): for material_type, material_list in material_dict.items():
material_type_item = { material_type_item = {
"name": material_type, "name": material_type,
"brand": brand,
"colors": BaseMaterialsModel(self) "colors": BaseMaterialsModel(self)
} }
material_type_item["colors"].clear() material_type_item["colors"].clear()

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,7 +17,7 @@ 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.

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

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
if has_variant_materials:
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 not is_global_quality: if not 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():
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
@ -268,13 +283,16 @@ class QualityManager(QObject):
# 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:
@ -295,35 +313,42 @@ class QualityManager(QObject):
# The last fall back will be the global qualities (either from the machine-specific node or the generic # The last fall back will be the global qualities (either from the machine-specific node or the generic
# node), but we only use one. For details see the overview comments above. # node), but we only use one. For details see the overview comments above.
if machine_node.quality_type_map:
if machine_node is not None and machine_node.quality_type_map:
nodes_to_check += [machine_node] nodes_to_check += [machine_node]
else: elif default_machine_node is not None:
nodes_to_check += [default_machine_node] nodes_to_check += [default_machine_node]
for node in nodes_to_check: 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:
@ -339,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
@ -361,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.
@ -393,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.")
@ -421,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

@ -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"]):
if self._extruder_configurations != extruder_configurations:
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:
if self._hotend_id != material:
self._material = 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)
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._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()
if self._manager is not None:
reply = self._manager.put(request, data.encode()) reply = self._manager.put(request, data.encode())
self._registerOnFinishedCallback(reply, on_finished) 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()
if self._manager is not None:
reply = self._manager.get(request) reply = self._manager.get(request)
self._registerOnFinishedCallback(reply, on_finished) 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()
if self._manager is not None:
reply = self._manager.post(request, data) reply = self._manager.post(request, data)
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)
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,6 +231,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
self._last_request_time = time() self._last_request_time = time()
if self._manager is not None:
reply = self._manager.post(request, multi_post_part) reply = self._manager.post(request, multi_post_part)
self._kept_alive_multiparts[reply] = multi_post_part self._kept_alive_multiparts[reply] = multi_post_part
@ -238,6 +241,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
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

@ -43,9 +43,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
@ -283,7 +283,3 @@ class PrinterOutputModel(QObject):
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]
if Application.getInstance() is not None:
Application.getInstance().callLater(self.createRecomputeConvexHullTimer) 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,7 +141,7 @@ 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
@ -142,7 +151,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
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:
if self._global_stack:
return Polygon(numpy.array(self._global_stack.getHeadAndFansCoordinates(), numpy.float32)) 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,19 +43,19 @@ 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)
@ -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,7 +105,7 @@ 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)):
@ -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

@ -5,6 +5,7 @@ from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For comm
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):
@ -188,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

@ -367,6 +367,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())
@ -375,7 +376,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()
@ -385,7 +386,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:
@ -412,7 +415,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()
@ -435,7 +438,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
@ -448,7 +451,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
@ -473,7 +476,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:
@ -638,7 +641,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:
@ -890,7 +893,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

@ -934,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

@ -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

@ -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

@ -333,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)
@ -440,7 +440,6 @@ 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

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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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,19 +492,22 @@ 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)
if self._composite_pass:
self._composite_pass.setLayerBindings(self._old_layer_bindings) self._composite_pass.setLayerBindings(self._old_layer_bindings)
self._composite_pass.setCompositeShader(self._old_composite_shader) 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
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

@ -623,7 +623,15 @@ Item
{ {
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
@ -668,7 +676,15 @@ Item
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
@ -716,6 +732,8 @@ Item
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()
@ -723,6 +741,8 @@ Item
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
} }
} }
@ -773,6 +793,8 @@ 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
} }
} }

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

@ -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

@ -33,6 +33,7 @@ Window
{ {
id: header id: header
} }
Item Item
{ {
id: mainView id: mainView
@ -75,6 +76,7 @@ Window
visible: toolbox.viewCategory == "installed" visible: toolbox.viewCategory == "installed"
} }
} }
ToolboxFooter ToolboxFooter
{ {
id: footer id: footer

View file

@ -82,9 +82,16 @@ Item
} }
spacing: Math.floor(UM.Theme.getSize("narrow_margin").height) spacing: Math.floor(UM.Theme.getSize("narrow_margin").height)
width: childrenRect.width width: childrenRect.width
Label Label
{ {
text: catalog.i18nc("@label", "Contact") + ":" text: catalog.i18nc("@label", "Website") + ":"
font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text_medium")
}
Label
{
text: catalog.i18nc("@label", "Email") + ":"
font: UM.Theme.getFont("very_small") font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text_medium") color: UM.Theme.getColor("text_medium")
} }
@ -100,18 +107,32 @@ Item
topMargin: UM.Theme.getSize("default_margin").height topMargin: UM.Theme.getSize("default_margin").height
} }
spacing: Math.floor(UM.Theme.getSize("narrow_margin").height) spacing: Math.floor(UM.Theme.getSize("narrow_margin").height)
Label
{
text:
{
if (details.website)
{
return "<a href=\"" + details.website + "\">" + details.website + "</a>"
}
return ""
}
font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
}
Label Label
{ {
text: text:
{ {
if (details.email) if (details.email)
{ {
return "<a href=\"mailto:"+details.email+"\">"+details.name+"</a>" return "<a href=\"mailto:" + details.email + "\">" + details.email + "</a>"
}
else
{
return "<a href=\""+details.website+"\">"+details.name+"</a>"
} }
return ""
} }
font: UM.Theme.getFont("very_small") font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")

View file

@ -1,14 +1,25 @@
// Copyright (c) 2018 Ultimaker B.V. // Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher. // Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2 import QtQuick 2.7
import QtQuick.Controls 1.4 import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4 import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM import UM 1.1 as UM
Item Item
{ {
id: base
property var packageData property var packageData
property var technicalDataSheetUrl: {
var link = undefined
if ("Technical Data Sheet" in packageData.links)
{
link = packageData.links["Technical Data Sheet"]
}
return link
}
anchors.topMargin: UM.Theme.getSize("default_margin").height anchors.topMargin: UM.Theme.getSize("default_margin").height
height: visible ? childrenRect.height : 0 height: visible ? childrenRect.height : 0
visible: packageData.type == "material" && packageData.has_configs visible: packageData.type == "material" && packageData.has_configs
@ -132,4 +143,25 @@ Item
width: Math.floor(table.width * 0.1) width: Math.floor(table.width * 0.1)
} }
} }
Label
{
id: technical_data_sheet
anchors.top: table.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height / 2
visible: base.technicalDataSheetUrl !== undefined
text:
{
if (base.technicalDataSheetUrl !== undefined)
{
return "<a href='%1'>%2</a>".arg(base.technicalDataSheetUrl).arg("Technical Data Sheet")
}
return ""
}
font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
}
} }

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V. // Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher. // Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2 import QtQuick 2.7
import QtQuick.Controls 1.4 import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4 import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM import UM 1.1 as UM

View file

@ -32,7 +32,7 @@ Item
width: UM.Theme.getSize("toolbox_thumbnail_medium").width width: UM.Theme.getSize("toolbox_thumbnail_medium").width
height: UM.Theme.getSize("toolbox_thumbnail_medium").height height: UM.Theme.getSize("toolbox_thumbnail_medium").height
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
source: details.icon_url || "../images/logobot.svg" source: details === null ? "" : (details.icon_url || "../images/logobot.svg")
mipmap: true mipmap: true
anchors anchors
{ {
@ -55,7 +55,7 @@ Item
rightMargin: UM.Theme.getSize("wide_margin").width rightMargin: UM.Theme.getSize("wide_margin").width
bottomMargin: UM.Theme.getSize("default_margin").height bottomMargin: UM.Theme.getSize("default_margin").height
} }
text: details.name || "" text: details === null ? "" : (details.name || "")
font: UM.Theme.getFont("large") font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
@ -114,7 +114,7 @@ Item
height: childrenRect.height height: childrenRect.height
Label Label
{ {
text: details.version || catalog.i18nc("@label", "Unknown") text: details === null ? "" : (details.version || catalog.i18nc("@label", "Unknown"))
font: UM.Theme.getFont("very_small") font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")
} }
@ -122,6 +122,10 @@ Item
{ {
text: text:
{ {
if (details === null)
{
return ""
}
var date = new Date(details.last_updated) var date = new Date(details.last_updated)
return date.toLocaleString(UM.Preferences.getValue("general/language")) return date.toLocaleString(UM.Preferences.getValue("general/language"))
} }
@ -132,6 +136,10 @@ Item
{ {
text: text:
{ {
if (details === null)
{
return ""
}
if (details.author_email) if (details.author_email)
{ {
return "<a href=\"mailto:" + details.author_email+"?Subject=Cura: " + details.name + "\">" + details.author_name + "</a>" return "<a href=\"mailto:" + details.author_email+"?Subject=Cura: " + details.name + "\">" + details.author_name + "</a>"
@ -148,7 +156,7 @@ Item
} }
Label Label
{ {
text: details.download_count || catalog.i18nc("@label", "Unknown") text: details === null ? "" : (details.download_count || catalog.i18nc("@label", "Unknown"))
font: UM.Theme.getFont("very_small") font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")
} }

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V. // Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher. // Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2 import QtQuick 2.7
import QtQuick.Controls 1.4 import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4 import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM import UM 1.1 as UM

View file

@ -9,7 +9,8 @@ import UM 1.1 as UM
Item Item
{ {
property int packageCount: (toolbox.viewCategory == "material" && model.type === undefined) ? toolbox.getTotalNumberOfPackagesByAuthor(model.id) : 1 id: toolboxDownloadsGridTile
property int packageCount: (toolbox.viewCategory == "material" && model.type === undefined) ? toolbox.getTotalNumberOfMaterialPackagesByAuthor(model.id) : 1
property int installedPackages: (toolbox.viewCategory == "material" && model.type === undefined) ? toolbox.getNumberOfInstalledPackagesByAuthor(model.id) : (toolbox.isInstalled(model.id) ? 1 : 0) property int installedPackages: (toolbox.viewCategory == "material" && model.type === undefined) ? toolbox.getNumberOfInstalledPackagesByAuthor(model.id) : (toolbox.isInstalled(model.id) ? 1 : 0)
height: childrenRect.height height: childrenRect.height
Layout.alignment: Qt.AlignTop | Qt.AlignLeft Layout.alignment: Qt.AlignTop | Qt.AlignLeft

View file

@ -12,7 +12,9 @@ ScrollView
width: parent.width width: parent.width
height: parent.height height: parent.height
style: UM.Theme.styles.scrollview style: UM.Theme.styles.scrollview
flickableItem.flickableDirection: Flickable.VerticalFlick flickableItem.flickableDirection: Flickable.VerticalFlick
Column Column
{ {
width: base.width width: base.width
@ -30,7 +32,7 @@ ScrollView
id: allPlugins id: allPlugins
width: parent.width width: parent.width
heading: toolbox.viewCategory == "material" ? catalog.i18nc("@label", "Community Contributions") : catalog.i18nc("@label", "Community Plugins") heading: toolbox.viewCategory == "material" ? catalog.i18nc("@label", "Community Contributions") : catalog.i18nc("@label", "Community Plugins")
model: toolbox.viewCategory == "material" ? toolbox.authorsModel : toolbox.packagesModel model: toolbox.viewCategory == "material" ? toolbox.materialsAvailableModel : toolbox.pluginsAvailableModel
} }
ToolboxDownloadsGrid ToolboxDownloadsGrid

View file

@ -9,7 +9,7 @@ import UM 1.1 as UM
Rectangle Rectangle
{ {
property int packageCount: toolbox.viewCategory == "material" ? toolbox.getTotalNumberOfPackagesByAuthor(model.id) : 1 property int packageCount: toolbox.viewCategory == "material" ? toolbox.getTotalNumberOfMaterialPackagesByAuthor(model.id) : 1
property int installedPackages: toolbox.viewCategory == "material" ? toolbox.getNumberOfInstalledPackagesByAuthor(model.id) : (toolbox.isInstalled(model.id) ? 1 : 0) property int installedPackages: toolbox.viewCategory == "material" ? toolbox.getNumberOfInstalledPackagesByAuthor(model.id) : (toolbox.isInstalled(model.id) ? 1 : 0)
id: tileBase id: tileBase
width: UM.Theme.getSize("toolbox_thumbnail_large").width + (2 * UM.Theme.getSize("default_lining").width) width: UM.Theme.getSize("toolbox_thumbnail_large").width + (2 * UM.Theme.getSize("default_lining").width)

View file

@ -21,8 +21,10 @@ Item
left: parent.left left: parent.left
leftMargin: UM.Theme.getSize("default_margin").width leftMargin: UM.Theme.getSize("default_margin").width
} }
ToolboxTabButton ToolboxTabButton
{ {
id: pluginsTabButton
text: catalog.i18nc("@title:tab", "Plugins") text: catalog.i18nc("@title:tab", "Plugins")
active: toolbox.viewCategory == "plugin" && enabled active: toolbox.viewCategory == "plugin" && enabled
enabled: toolbox.viewPage != "loading" && toolbox.viewPage != "errored" enabled: toolbox.viewPage != "loading" && toolbox.viewPage != "errored"
@ -36,6 +38,7 @@ Item
ToolboxTabButton ToolboxTabButton
{ {
id: materialsTabButton
text: catalog.i18nc("@title:tab", "Materials") text: catalog.i18nc("@title:tab", "Materials")
active: toolbox.viewCategory == "material" && enabled active: toolbox.viewCategory == "material" && enabled
enabled: toolbox.viewPage != "loading" && toolbox.viewPage != "errored" enabled: toolbox.viewPage != "loading" && toolbox.viewPage != "errored"
@ -49,6 +52,7 @@ Item
} }
ToolboxTabButton ToolboxTabButton
{ {
id: installedTabButton
text: catalog.i18nc("@title:tab", "Installed") text: catalog.i18nc("@title:tab", "Installed")
active: toolbox.viewCategory == "installed" active: toolbox.viewCategory == "installed"
anchors anchors

View file

@ -6,6 +6,7 @@ import QtQuick.Dialogs 1.1
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import QtQuick.Controls 1.4 import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4 import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM import UM 1.1 as UM
ScrollView ScrollView
@ -16,6 +17,7 @@ ScrollView
height: parent.height height: parent.height
style: UM.Theme.styles.scrollview style: UM.Theme.styles.scrollview
flickableItem.flickableDirection: Flickable.VerticalFlick flickableItem.flickableDirection: Flickable.VerticalFlick
Column Column
{ {
spacing: UM.Theme.getSize("default_margin").height spacing: UM.Theme.getSize("default_margin").height

View file

@ -33,6 +33,9 @@ class AuthorsModel(ListModel):
def _update(self): def _update(self):
items = [] items = []
if not self._metadata:
self.setItems([])
return
for author in self._metadata: for author in self._metadata:
items.append({ items.append({

View file

@ -5,9 +5,13 @@ import re
from typing import Dict from typing import Dict
from PyQt5.QtCore import Qt, pyqtProperty from PyQt5.QtCore import Qt, pyqtProperty
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
from .ConfigsModel import ConfigsModel from .ConfigsModel import ConfigsModel
## Model that holds cura packages. By setting the filter property the instances held by this model can be changed. ## Model that holds cura packages. By setting the filter property the instances held by this model can be changed.
class PackagesModel(ListModel): class PackagesModel(ListModel):
def __init__(self, parent = None): def __init__(self, parent = None):
@ -34,6 +38,8 @@ class PackagesModel(ListModel):
self.addRoleName(Qt.UserRole + 17, "supported_configs") self.addRoleName(Qt.UserRole + 17, "supported_configs")
self.addRoleName(Qt.UserRole + 18, "download_count") self.addRoleName(Qt.UserRole + 18, "download_count")
self.addRoleName(Qt.UserRole + 19, "tags") self.addRoleName(Qt.UserRole + 19, "tags")
self.addRoleName(Qt.UserRole + 20, "links")
self.addRoleName(Qt.UserRole + 21, "website")
# List of filters for queries. The result is the union of the each list of results. # List of filters for queries. The result is the union of the each list of results.
self._filter = {} # type: Dict[str, str] self._filter = {} # type: Dict[str, str]
@ -45,10 +51,16 @@ class PackagesModel(ListModel):
def _update(self): def _update(self):
items = [] items = []
for package in self._metadata: if self._metadata is None:
Logger.logException("w", "Failed to load packages for Toolbox")
self.setItems(items)
return
for package in self._metadata:
has_configs = False has_configs = False
configs_model = None configs_model = None
links_dict = {}
if "data" in package: if "data" in package:
if "supported_configs" in package["data"]: if "supported_configs" in package["data"]:
if len(package["data"]["supported_configs"]) > 0: if len(package["data"]["supported_configs"]) > 0:
@ -56,10 +68,14 @@ class PackagesModel(ListModel):
configs_model = ConfigsModel() configs_model = ConfigsModel()
configs_model.setConfigs(package["data"]["supported_configs"]) configs_model.setConfigs(package["data"]["supported_configs"])
# Links is a list of dictionaries with "title" and "url". Convert this list into a dict so it's easier
# to process.
link_list = package['data']['links'] if 'links' in package['data'] else []
links_dict = {d["title"]: d["url"] for d in link_list}
if "author_id" not in package["author"] or "display_name" not in package["author"]: if "author_id" not in package["author"] or "display_name" not in package["author"]:
package["author"]["author_id"] = "" package["author"]["author_id"] = ""
package["author"]["display_name"] = "" package["author"]["display_name"] = ""
# raise Exception("Detected a package with malformed author data.")
items.append({ items.append({
"id": package["package_id"], "id": package["package_id"],
@ -80,17 +96,19 @@ class PackagesModel(ListModel):
"has_configs": has_configs, "has_configs": has_configs,
"supported_configs": configs_model, "supported_configs": configs_model,
"download_count": package["download_count"] if "download_count" in package else 0, "download_count": package["download_count"] if "download_count" in package else 0,
"tags": package["tags"] if "tags" in package else [] "tags": package["tags"] if "tags" in package else [],
"links": links_dict,
"website": package["website"] if "website" in package else None,
}) })
# Filter on all the key-word arguments. # Filter on all the key-word arguments.
for key, value in self._filter.items(): for key, value in self._filter.items():
if key is "tags": if key is "tags":
key_filter = lambda item, value = value: value in item["tags"] key_filter = lambda item, v = value: v in item["tags"]
elif "*" in value: elif "*" in value:
key_filter = lambda candidate, key = key, value = value: self._matchRegExp(candidate, key, value) key_filter = lambda candidate, k = key, v = value: self._matchRegExp(candidate, k, v)
else: else:
key_filter = lambda candidate, key = key, value = value: self._matchString(candidate, key, value) key_filter = lambda candidate, k = key, v = value: self._matchString(candidate, k, v)
items = filter(key_filter, items) items = filter(key_filter, items)
# Execute all filters. # Execute all filters.

View file

@ -1,12 +1,11 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Toolbox is released under the terms of the LGPLv3 or higher. # Toolbox is released under the terms of the LGPLv3 or higher.
from typing import Dict, Optional, Union, Any, cast
import json import json
import os import os
import tempfile import tempfile
import platform import platform
from typing import cast, List from typing import cast, Any, Dict, List, Set, TYPE_CHECKING, Tuple, Optional, Union
from PyQt5.QtCore import QUrl, QObject, pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtCore import QUrl, QObject, pyqtProperty, pyqtSignal, pyqtSlot
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
@ -20,9 +19,13 @@ from UM.Version import Version
import cura import cura
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from .AuthorsModel import AuthorsModel from .AuthorsModel import AuthorsModel
from .PackagesModel import PackagesModel from .PackagesModel import PackagesModel
if TYPE_CHECKING:
from cura.Settings.GlobalStack import GlobalStack
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
@ -36,7 +39,7 @@ class Toolbox(QObject, Extension):
self._application = application # type: CuraApplication self._application = application # type: CuraApplication
self._sdk_version = None # type: Optional[int] self._sdk_version = None # type: Optional[Union[str, int]]
self._cloud_api_version = None # type: Optional[int] self._cloud_api_version = None # type: Optional[int]
self._cloud_api_root = None # type: Optional[str] self._cloud_api_root = None # type: Optional[str]
self._api_url = None # type: Optional[str] self._api_url = None # type: Optional[str]
@ -60,7 +63,8 @@ class Toolbox(QObject, Extension):
] ]
self._request_urls = {} # type: Dict[str, QUrl] self._request_urls = {} # type: Dict[str, QUrl]
self._to_update = [] # type: List[str] # Package_ids that are waiting to be updated self._to_update = [] # type: List[str] # Package_ids that are waiting to be updated
self._old_plugin_ids = [] # type: List[str] self._old_plugin_ids = set() # type: Set[str]
self._old_plugin_metadata = dict() # type: Dict[str, Dict[str, Any]]
# Data: # Data:
self._metadata = { self._metadata = {
@ -83,7 +87,7 @@ class Toolbox(QObject, Extension):
"plugins_available": PackagesModel(self), "plugins_available": PackagesModel(self),
"plugins_installed": PackagesModel(self), "plugins_installed": PackagesModel(self),
"materials_showcase": AuthorsModel(self), "materials_showcase": AuthorsModel(self),
"materials_available": PackagesModel(self), "materials_available": AuthorsModel(self),
"materials_installed": PackagesModel(self), "materials_installed": PackagesModel(self),
"materials_generic": PackagesModel(self) "materials_generic": PackagesModel(self)
} # type: Dict[str, ListModel] } # type: Dict[str, ListModel]
@ -117,8 +121,6 @@ class Toolbox(QObject, Extension):
self._application.initializationFinished.connect(self._onAppInitialized) self._application.initializationFinished.connect(self._onAppInitialized)
# Signals: # Signals:
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
# Downloading changes # Downloading changes
@ -137,11 +139,11 @@ class Toolbox(QObject, Extension):
showLicenseDialog = pyqtSignal() showLicenseDialog = pyqtSignal()
uninstallVariablesChanged = pyqtSignal() uninstallVariablesChanged = pyqtSignal()
def _resetUninstallVariables(self): def _resetUninstallVariables(self) -> None:
self._package_id_to_uninstall = None self._package_id_to_uninstall = None # type: Optional[str]
self._package_name_to_uninstall = "" self._package_name_to_uninstall = ""
self._package_used_materials = [] self._package_used_materials = [] # type: List[Tuple[GlobalStack, str, str]]
self._package_used_qualities = [] self._package_used_qualities = [] # type: List[Tuple[GlobalStack, str, str]]
@pyqtSlot(result = str) @pyqtSlot(result = str)
def getLicenseDialogPluginName(self) -> str: def getLicenseDialogPluginName(self) -> str:
@ -205,7 +207,7 @@ class Toolbox(QObject, Extension):
return cura.CuraVersion.CuraCloudAPIVersion # type: ignore return cura.CuraVersion.CuraCloudAPIVersion # type: ignore
# Get the packages version depending on Cura version settings. # Get the packages version depending on Cura version settings.
def _getSDKVersion(self) -> int: def _getSDKVersion(self) -> Union[int, str]:
if not hasattr(cura, "CuraVersion"): if not hasattr(cura, "CuraVersion"):
return self._plugin_registry.APIVersion return self._plugin_registry.APIVersion
if not hasattr(cura.CuraVersion, "CuraSDKVersion"): # type: ignore if not hasattr(cura.CuraVersion, "CuraSDKVersion"): # type: ignore
@ -229,10 +231,12 @@ class Toolbox(QObject, Extension):
# Make remote requests: # Make remote requests:
self._makeRequestByType("packages") self._makeRequestByType("packages")
self._makeRequestByType("authors") self._makeRequestByType("authors")
self._makeRequestByType("plugins_showcase") # TODO: Uncomment in the future when the tag-filtered api calls work in the cloud server
self._makeRequestByType("materials_showcase") # self._makeRequestByType("plugins_showcase")
self._makeRequestByType("materials_available") # self._makeRequestByType("plugins_available")
self._makeRequestByType("materials_generic") # self._makeRequestByType("materials_showcase")
# self._makeRequestByType("materials_available")
# self._makeRequestByType("materials_generic")
# Gather installed packages: # Gather installed packages:
self._updateInstalledModels() self._updateInstalledModels()
@ -285,8 +289,8 @@ class Toolbox(QObject, Extension):
installed_package_ids = self._package_manager.getAllInstalledPackageIDs() installed_package_ids = self._package_manager.getAllInstalledPackageIDs()
scheduled_to_remove_package_ids = self._package_manager.getToRemovePackageIDs() scheduled_to_remove_package_ids = self._package_manager.getToRemovePackageIDs()
self._old_plugin_ids = [] self._old_plugin_ids = set()
self._old_plugin_metadata = [] # type: List[Dict[str, Any]] self._old_plugin_metadata = dict()
for plugin_id in old_plugin_ids: for plugin_id in old_plugin_ids:
# Neither the installed packages nor the packages that are scheduled to remove are old plugins # Neither the installed packages nor the packages that are scheduled to remove are old plugins
@ -296,12 +300,20 @@ class Toolbox(QObject, Extension):
old_metadata = self._plugin_registry.getMetaData(plugin_id) old_metadata = self._plugin_registry.getMetaData(plugin_id)
new_metadata = self._convertPluginMetadata(old_metadata) new_metadata = self._convertPluginMetadata(old_metadata)
self._old_plugin_ids.append(plugin_id) self._old_plugin_ids.add(plugin_id)
self._old_plugin_metadata.append(new_metadata) self._old_plugin_metadata[new_metadata["package_id"]] = new_metadata
all_packages = self._package_manager.getAllInstalledPackagesInfo() all_packages = self._package_manager.getAllInstalledPackagesInfo()
if "plugin" in all_packages: if "plugin" in all_packages:
self._metadata["plugins_installed"] = all_packages["plugin"] + self._old_plugin_metadata # For old plugins, we only want to include the old custom plugin that were installed via the old toolbox.
# The bundled plugins will be included in JSON files in the "bundled_packages" folder, so the bundled
# plugins should be excluded from the old plugins list/dict.
all_plugin_package_ids = set(package["package_id"] for package in all_packages["plugin"])
self._old_plugin_ids = set(plugin_id for plugin_id in self._old_plugin_ids
if plugin_id not in all_plugin_package_ids)
self._old_plugin_metadata = {k: v for k, v in self._old_plugin_metadata.items() if k in self._old_plugin_ids}
self._metadata["plugins_installed"] = all_packages["plugin"] + list(self._old_plugin_metadata.values())
self._models["plugins_installed"].setMetadata(self._metadata["plugins_installed"]) self._models["plugins_installed"].setMetadata(self._metadata["plugins_installed"])
self.metadataChanged.emit() self.metadataChanged.emit()
if "material" in all_packages: if "material" in all_packages:
@ -344,26 +356,26 @@ class Toolbox(QObject, Extension):
self.uninstall(package_id) self.uninstall(package_id)
@pyqtProperty(str, notify = uninstallVariablesChanged) @pyqtProperty(str, notify = uninstallVariablesChanged)
def pluginToUninstall(self): def pluginToUninstall(self) -> str:
return self._package_name_to_uninstall return self._package_name_to_uninstall
@pyqtProperty(str, notify = uninstallVariablesChanged) @pyqtProperty(str, notify = uninstallVariablesChanged)
def uninstallUsedMaterials(self): def uninstallUsedMaterials(self) -> str:
return "\n".join(["%s (%s)" % (str(global_stack.getName()), material) for global_stack, extruder_nr, material in self._package_used_materials]) return "\n".join(["%s (%s)" % (str(global_stack.getName()), material) for global_stack, extruder_nr, material in self._package_used_materials])
@pyqtProperty(str, notify = uninstallVariablesChanged) @pyqtProperty(str, notify = uninstallVariablesChanged)
def uninstallUsedQualities(self): def uninstallUsedQualities(self) -> str:
return "\n".join(["%s (%s)" % (str(global_stack.getName()), quality) for global_stack, extruder_nr, quality in self._package_used_qualities]) return "\n".join(["%s (%s)" % (str(global_stack.getName()), quality) for global_stack, extruder_nr, quality in self._package_used_qualities])
@pyqtSlot() @pyqtSlot()
def closeConfirmResetDialog(self): def closeConfirmResetDialog(self) -> None:
if self._confirm_reset_dialog is not None: if self._confirm_reset_dialog is not None:
self._confirm_reset_dialog.close() self._confirm_reset_dialog.close()
## Uses "uninstall variables" to reset qualities and materials, then uninstall ## Uses "uninstall variables" to reset qualities and materials, then uninstall
# It's used as an action on Confirm reset on Uninstall # It's used as an action on Confirm reset on Uninstall
@pyqtSlot() @pyqtSlot()
def resetMaterialsQualitiesAndUninstall(self): def resetMaterialsQualitiesAndUninstall(self) -> None:
application = CuraApplication.getInstance() application = CuraApplication.getInstance()
material_manager = application.getMaterialManager() material_manager = application.getMaterialManager()
quality_manager = application.getQualityManager() quality_manager = application.getQualityManager()
@ -376,8 +388,8 @@ class Toolbox(QObject, Extension):
default_quality_group = quality_manager.getDefaultQualityType(global_stack) default_quality_group = quality_manager.getDefaultQualityType(global_stack)
machine_manager.setQualityGroup(default_quality_group, global_stack = global_stack) machine_manager.setQualityGroup(default_quality_group, global_stack = global_stack)
if self._package_id_to_uninstall is not None:
self._markPackageMaterialsAsToBeUninstalled(self._package_id_to_uninstall) self._markPackageMaterialsAsToBeUninstalled(self._package_id_to_uninstall)
self.uninstall(self._package_id_to_uninstall) self.uninstall(self._package_id_to_uninstall)
self._resetUninstallVariables() self._resetUninstallVariables()
self.closeConfirmResetDialog() self.closeConfirmResetDialog()
@ -471,11 +483,13 @@ class Toolbox(QObject, Extension):
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
@pyqtSlot(str, result = bool) @pyqtSlot(str, result = bool)
def canUpdate(self, package_id: str) -> bool: def canUpdate(self, package_id: str) -> bool:
if self.isOldPlugin(package_id):
return True
local_package = self._package_manager.getInstalledPackageInfo(package_id) local_package = self._package_manager.getInstalledPackageInfo(package_id)
if local_package is None: if local_package is None:
Logger.log("i", "Could not find package [%s] as installed in the package manager, fall back to check the old plugins",
package_id)
local_package = self.getOldPluginPackageMetadata(package_id)
if local_package is None:
Logger.log("i", "Could not find package [%s] in the old plugins", package_id)
return False return False
remote_package = self.getRemotePackage(package_id) remote_package = self.getRemotePackage(package_id)
@ -484,7 +498,16 @@ class Toolbox(QObject, Extension):
local_version = Version(local_package["package_version"]) local_version = Version(local_package["package_version"])
remote_version = Version(remote_package["package_version"]) remote_version = Version(remote_package["package_version"])
return remote_version > local_version can_upgrade = False
if remote_version > local_version:
can_upgrade = True
# A package with the same version can be built to have different SDK versions. So, for a package with the same
# version, we also need to check if the current one has a lower SDK version. If so, this package should also
# be upgradable.
elif remote_version == local_version:
can_upgrade = local_package.get("sdk_version", 0) < remote_package.get("sdk_version", 0)
return can_upgrade
@pyqtSlot(str, result = bool) @pyqtSlot(str, result = bool)
def canDowngrade(self, package_id: str) -> bool: def canDowngrade(self, package_id: str) -> bool:
@ -504,7 +527,11 @@ class Toolbox(QObject, Extension):
@pyqtSlot(str, result = bool) @pyqtSlot(str, result = bool)
def isInstalled(self, package_id: str) -> bool: def isInstalled(self, package_id: str) -> bool:
return self._package_manager.isPackageInstalled(package_id) result = self._package_manager.isPackageInstalled(package_id)
# Also check the old plugins list if it's not found in the package manager.
if not result:
result = self.isOldPlugin(package_id)
return result
@pyqtSlot(str, result = int) @pyqtSlot(str, result = int)
def getNumberOfInstalledPackagesByAuthor(self, author_id: str) -> int: def getNumberOfInstalledPackagesByAuthor(self, author_id: str) -> int:
@ -514,10 +541,12 @@ class Toolbox(QObject, Extension):
count += 1 count += 1
return count return count
# This slot is only used to get the number of material packages by author, not any other type of packages.
@pyqtSlot(str, result = int) @pyqtSlot(str, result = int)
def getTotalNumberOfPackagesByAuthor(self, author_id: str) -> int: def getTotalNumberOfMaterialPackagesByAuthor(self, author_id: str) -> int:
count = 0 count = 0
for package in self._metadata["materials_available"]: for package in self._metadata["packages"]:
if package["package_type"] == "material":
if package["author"]["author_id"] == author_id: if package["author"]["author_id"] == author_id:
count += 1 count += 1
return count return count
@ -529,12 +558,14 @@ class Toolbox(QObject, Extension):
return False return False
# Check for plugins that were installed with the old plugin browser # Check for plugins that were installed with the old plugin browser
@pyqtSlot(str, result = bool)
def isOldPlugin(self, plugin_id: str) -> bool: def isOldPlugin(self, plugin_id: str) -> bool:
if plugin_id in self._old_plugin_ids: if plugin_id in self._old_plugin_ids:
return True return True
return False return False
def getOldPluginPackageMetadata(self, plugin_id: str) -> Optional[Dict[str, Any]]:
return self._old_plugin_metadata.get(plugin_id)
def loadingComplete(self) -> bool: def loadingComplete(self) -> bool:
populated = 0 populated = 0
for list in self._metadata.items(): for list in self._metadata.items():
@ -606,8 +637,22 @@ class Toolbox(QObject, Extension):
self.resetDownload() self.resetDownload()
return return
# HACK: These request are not handled independently at this moment, but together from the "packages" call
do_not_handle = [
"materials_available",
"materials_showcase",
"materials_generic",
"plugins_available",
"plugins_showcase",
]
if reply.operation() == QNetworkAccessManager.GetOperation: if reply.operation() == QNetworkAccessManager.GetOperation:
for type, url in self._request_urls.items(): for type, url in self._request_urls.items():
# HACK: Do nothing because we'll handle these from the "packages" call
if type in do_not_handle:
continue
if reply.url() == url: if reply.url() == url:
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200:
try: try:
@ -624,17 +669,6 @@ class Toolbox(QObject, Extension):
Logger.log("e", "Could not find the %s model.", type) Logger.log("e", "Could not find the %s model.", type)
break break
# HACK: Eventually get rid of the code from here...
if type is "plugins_showcase" or type is "materials_showcase":
self._metadata["plugins_showcase"] = json_data["data"]["plugin"]["packages"]
self._models["plugins_showcase"].setMetadata(self._metadata["plugins_showcase"])
self._metadata["materials_showcase"] = json_data["data"]["material"]["authors"]
self._models["materials_showcase"].setMetadata(self._metadata["materials_showcase"])
else:
# ...until here.
# This hack arises for multiple reasons but the main
# one is because there are not separate API calls
# for different kinds of showcases.
self._metadata[type] = json_data["data"] self._metadata[type] = json_data["data"]
self._models[type].setMetadata(self._metadata[type]) self._models[type].setMetadata(self._metadata[type])
@ -642,6 +676,8 @@ class Toolbox(QObject, Extension):
# TODO: Make multiple API calls in the future to handle this # TODO: Make multiple API calls in the future to handle this
if type is "packages": if type is "packages":
self._models[type].setFilter({"type": "plugin"}) self._models[type].setFilter({"type": "plugin"})
self.buildMaterialsModels()
self.buildPluginsModels()
if type is "authors": if type is "authors":
self._models[type].setFilter({"package_types": "material"}) self._models[type].setFilter({"package_types": "material"})
if type is "materials_generic": if type is "materials_generic":
@ -680,7 +716,7 @@ class Toolbox(QObject, Extension):
self._temp_plugin_file.close() self._temp_plugin_file.close()
self._onDownloadComplete(file_path) self._onDownloadComplete(file_path)
def _onDownloadComplete(self, file_path: str): def _onDownloadComplete(self, file_path: str) -> None:
Logger.log("i", "Toolbox: Download complete.") Logger.log("i", "Toolbox: Download complete.")
package_info = self._package_manager.getPackageInfo(file_path) package_info = self._package_manager.getPackageInfo(file_path)
if not package_info: if not package_info:
@ -739,9 +775,7 @@ class Toolbox(QObject, Extension):
def viewPage(self) -> str: def viewPage(self) -> str:
return self._view_page return self._view_page
# Exposed Models:
# Expose Models:
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
@pyqtProperty(QObject, notify = metadataChanged) @pyqtProperty(QObject, notify = metadataChanged)
def authorsModel(self) -> AuthorsModel: def authorsModel(self) -> AuthorsModel:
@ -755,6 +789,10 @@ class Toolbox(QObject, Extension):
def pluginsShowcaseModel(self) -> PackagesModel: def pluginsShowcaseModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["plugins_showcase"]) return cast(PackagesModel, self._models["plugins_showcase"])
@pyqtProperty(QObject, notify = metadataChanged)
def pluginsAvailableModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["plugins_available"])
@pyqtProperty(QObject, notify = metadataChanged) @pyqtProperty(QObject, notify = metadataChanged)
def pluginsInstalledModel(self) -> PackagesModel: def pluginsInstalledModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["plugins_installed"]) return cast(PackagesModel, self._models["plugins_installed"])
@ -763,6 +801,10 @@ class Toolbox(QObject, Extension):
def materialsShowcaseModel(self) -> AuthorsModel: def materialsShowcaseModel(self) -> AuthorsModel:
return cast(AuthorsModel, self._models["materials_showcase"]) return cast(AuthorsModel, self._models["materials_showcase"])
@pyqtProperty(QObject, notify = metadataChanged)
def materialsAvailableModel(self) -> AuthorsModel:
return cast(AuthorsModel, self._models["materials_available"])
@pyqtProperty(QObject, notify = metadataChanged) @pyqtProperty(QObject, notify = metadataChanged)
def materialsInstalledModel(self) -> PackagesModel: def materialsInstalledModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["materials_installed"]) return cast(PackagesModel, self._models["materials_installed"])
@ -771,8 +813,6 @@ class Toolbox(QObject, Extension):
def materialsGenericModel(self) -> PackagesModel: def materialsGenericModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["materials_generic"]) return cast(PackagesModel, self._models["materials_generic"])
# Filter Models: # Filter Models:
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
@pyqtSlot(str, str, str) @pyqtSlot(str, str, str)
@ -798,3 +838,48 @@ class Toolbox(QObject, Extension):
return return
self._models[model_type].setFilter({}) self._models[model_type].setFilter({})
self.filterChanged.emit() self.filterChanged.emit()
# HACK(S):
# --------------------------------------------------------------------------
def buildMaterialsModels(self) -> None:
self._metadata["materials_showcase"] = []
self._metadata["materials_available"] = []
processed_authors = [] # type: List[str]
for item in self._metadata["packages"]:
if item["package_type"] == "material":
author = item["author"]
if author["author_id"] in processed_authors:
continue
# Generic materials to be in the same section
if "generic" in item["tags"]:
self._metadata["materials_generic"].append(item)
else:
if "showcase" in item["tags"]:
self._metadata["materials_showcase"].append(author)
else:
self._metadata["materials_available"].append(author)
processed_authors.append(author["author_id"])
self._models["materials_showcase"].setMetadata(self._metadata["materials_showcase"])
self._models["materials_available"].setMetadata(self._metadata["materials_available"])
self._models["materials_generic"].setMetadata(self._metadata["materials_generic"])
def buildPluginsModels(self) -> None:
self._metadata["plugins_showcase"] = []
self._metadata["plugins_available"] = []
for item in self._metadata["packages"]:
if item["package_type"] == "plugin":
if "showcase" in item["tags"]:
self._metadata["plugins_showcase"].append(item)
else:
self._metadata["plugins_available"].append(item)
self._models["plugins_showcase"].setMetadata(self._metadata["plugins_showcase"])
self._models["plugins_available"].setMetadata(self._metadata["plugins_available"])

View file

@ -1,5 +1,6 @@
#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 cast from typing import cast
from Charon.VirtualFile import VirtualFile #To open UFP files. from Charon.VirtualFile import VirtualFile #To open UFP files.
@ -9,6 +10,7 @@ from io import StringIO #For converting g-code to bytes.
from UM.Application import Application from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
from UM.Mesh.MeshWriter import MeshWriter #The writer we need to implement. from UM.Mesh.MeshWriter import MeshWriter #The writer we need to implement.
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
from UM.PluginRegistry import PluginRegistry #To get the g-code writer. from UM.PluginRegistry import PluginRegistry #To get the g-code writer.
from PyQt5.QtCore import QBuffer from PyQt5.QtCore import QBuffer
@ -22,6 +24,15 @@ catalog = i18nCatalog("cura")
class UFPWriter(MeshWriter): class UFPWriter(MeshWriter):
def __init__(self): def __init__(self):
super().__init__(add_to_recent_files = False) super().__init__(add_to_recent_files = False)
MimeTypeDatabase.addMimeType(
MimeType(
name = "application/x-cura-stl-file",
comment = "Cura UFP File",
suffixes = ["ufp"]
)
)
self._snapshot = None self._snapshot = None
Application.getInstance().getOutputDeviceManager().writeStarted.connect(self._createSnapshot) Application.getInstance().getOutputDeviceManager().writeStarted.connect(self._createSnapshot)

View file

@ -11,16 +11,6 @@ except ImportError:
from UM.i18n import i18nCatalog #To translate the file format description. from UM.i18n import i18nCatalog #To translate the file format description.
from UM.Mesh.MeshWriter import MeshWriter #For the binary mode flag. from UM.Mesh.MeshWriter import MeshWriter #For the binary mode flag.
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
MimeTypeDatabase.addMimeType(
MimeType(
name = "application/x-cura-stl-file",
comment = "Cura UFP File",
suffixes = ["ufp"]
)
)
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")

View file

@ -1,241 +0,0 @@
import QtQuick 2.2
import QtQuick.Controls 1.4
import UM 1.3 as UM
import Cura 1.0 as Cura
Component
{
Rectangle
{
id: base
property var manager: Cura.MachineManager.printerOutputDevices[0]
property var lineColor: "#DCDCDC" // TODO: Should be linked to theme.
property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme.
visible: manager != null
anchors.fill: parent
color: UM.Theme.getColor("viewport_background")
UM.I18nCatalog
{
id: catalog
name: "cura"
}
Label
{
id: activePrintersLabel
font: UM.Theme.getFont("large")
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.top: parent.top
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right:parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
text: Cura.MachineManager.printerOutputDevices[0].name
elide: Text.ElideRight
}
Rectangle
{
id: printJobArea
border.width: UM.Theme.getSize("default_lining").width
border.color: lineColor
anchors.top: activePrintersLabel.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: parent.right
anchors.rightMargin:UM.Theme.getSize("default_margin").width
radius: cornerRadius
height: childrenRect.height
Item
{
id: printJobTitleBar
width: parent.width
height: printJobTitleLabel.height + 2 * UM.Theme.getSize("default_margin").height
Label
{
id: printJobTitleLabel
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.top: parent.top
anchors.topMargin: UM.Theme.getSize("default_margin").height
text: catalog.i18nc("@title", "Print jobs")
font: UM.Theme.getFont("default")
opacity: 0.75
}
Rectangle
{
anchors.bottom: parent.bottom
height: UM.Theme.getSize("default_lining").width
color: lineColor
width: parent.width
}
}
Column
{
id: printJobColumn
anchors.top: printJobTitleBar.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
Item
{
width: parent.width
height: childrenRect.height
opacity: 0.65
Label
{
text: catalog.i18nc("@label", "Printing")
font: UM.Theme.getFont("very_small")
}
Label
{
text: manager.activePrintJobs.length
font: UM.Theme.getFont("small")
anchors.right: parent.right
}
}
Item
{
width: parent.width
height: childrenRect.height
opacity: 0.65
Label
{
text: catalog.i18nc("@label", "Queued")
font: UM.Theme.getFont("very_small")
}
Label
{
text: manager.queuedPrintJobs.length
font: UM.Theme.getFont("small")
anchors.right: parent.right
}
}
}
OpenPanelButton
{
anchors.top: printJobColumn.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: UM.Theme.getSize("default_margin").height
id: configButton
onClicked: base.manager.openPrintJobControlPanel()
text: catalog.i18nc("@action:button", "View print jobs")
}
Item
{
// spacer
anchors.top: configButton.bottom
width: UM.Theme.getSize("default_margin").width
height: UM.Theme.getSize("default_margin").height
}
}
Rectangle
{
id: printersArea
border.width: UM.Theme.getSize("default_lining").width
border.color: lineColor
anchors.top: printJobArea.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: parent.right
anchors.rightMargin:UM.Theme.getSize("default_margin").width
radius: cornerRadius
height: childrenRect.height
Item
{
id: printersTitleBar
width: parent.width
height: printJobTitleLabel.height + 2 * UM.Theme.getSize("default_margin").height
Label
{
id: printersTitleLabel
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.top: parent.top
anchors.topMargin: UM.Theme.getSize("default_margin").height
text: catalog.i18nc("@label:title", "Printers")
font: UM.Theme.getFont("default")
opacity: 0.75
}
Rectangle
{
anchors.bottom: parent.bottom
height: UM.Theme.getSize("default_lining").width
color: lineColor
width: parent.width
}
}
Column
{
id: printersColumn
anchors.top: printersTitleBar.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
Repeater
{
model: manager.connectedPrintersTypeCount
Item
{
width: parent.width
height: childrenRect.height
opacity: 0.65
Label
{
text: modelData.machine_type
font: UM.Theme.getFont("very_small")
}
Label
{
text: modelData.count
font: UM.Theme.getFont("small")
anchors.right: parent.right
}
}
}
}
OpenPanelButton
{
anchors.top: printersColumn.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: UM.Theme.getSize("default_margin").height
id: printerConfigButton
onClicked: base.manager.openPrinterControlPanel()
text: catalog.i18nc("@action:button", "View printers")
}
Item
{
// spacer
anchors.top: printerConfigButton.bottom
width: UM.Theme.getSize("default_margin").width
height: UM.Theme.getSize("default_margin").height
}
}
}
}

View file

@ -1,118 +0,0 @@
import QtQuick 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.3 as UM
import Cura 1.0 as Cura
Component
{
Rectangle
{
id: monitorFrame
width: maximumWidth
height: maximumHeight
color: UM.Theme.getColor("viewport_background")
property var emphasisColor: UM.Theme.getColor("setting_control_border_highlight")
property var lineColor: "#DCDCDC" // TODO: Should be linked to theme.
property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme.
UM.I18nCatalog
{
id: catalog
name: "cura"
}
Label
{
id: activePrintersLabel
font: UM.Theme.getFont("large")
anchors {
top: parent.top
topMargin: UM.Theme.getSize("default_margin").height * 2 // a bit more spacing to give it some breathing room
horizontalCenter: parent.horizontalCenter
}
text: OutputDevice.printers.length == 0 ? catalog.i18nc("@label: arg 1 is group name", "%1 is not set up to host a group of connected Ultimaker 3 printers").arg(Cura.MachineManager.printerOutputDevices[0].name) : ""
visible: OutputDevice.printers.length == 0
}
Item
{
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
width: Math.min(800 * screenScaleFactor, maximumWidth)
height: children.height
visible: OutputDevice.printers.length != 0
Label
{
id: addRemovePrintersLabel
anchors.right: parent.right
text: catalog.i18nc("@label link to connect manager", "Add/Remove printers")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
}
MouseArea
{
anchors.fill: addRemovePrintersLabel
hoverEnabled: true
onClicked: Cura.MachineManager.printerOutputDevices[0].openPrinterControlPanel()
onEntered: addRemovePrintersLabel.font.underline = true
onExited: addRemovePrintersLabel.font.underline = false
}
}
ScrollView
{
id: printerScrollView
anchors.margins: UM.Theme.getSize("default_margin").width
anchors.top: activePrintersLabel.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_lining").width // To ensure border can be drawn.
anchors.rightMargin: UM.Theme.getSize("default_lining").width
anchors.right: parent.right
ListView
{
anchors.fill: parent
spacing: -UM.Theme.getSize("default_lining").height
model: OutputDevice.printers
delegate: PrinterInfoBlock
{
printer: modelData
width: Math.min(800 * screenScaleFactor, maximumWidth)
height: 125 * screenScaleFactor
// Add a 1 pix margin, as the border is sometimes cut off otherwise.
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
PrinterVideoStream
{
visible: OutputDevice.activePrinter != null
anchors.fill:parent
}
onVisibleChanged:
{
if (!monitorFrame.visible)
{
// After switching the Tab ensure that active printer is Null, the video stream image
// might be active
OutputDevice.setActivePrinter(null)
}
}
}
}

View file

@ -1,71 +0,0 @@
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1
import UM 1.1 as UM
Button {
objectName: "openPanelSaveAreaButton"
id: openPanelSaveAreaButton
UM.I18nCatalog { id: catalog; name: "cura"; }
height: UM.Theme.getSize("save_button_save_to_button").height
tooltip: catalog.i18nc("@info:tooltip", "Opens the print jobs page with your default web browser.")
text: catalog.i18nc("@action:button", "View print jobs")
// FIXME: This button style is copied and duplicated from SaveButton.qml
style: ButtonStyle {
background: Rectangle
{
border.width: UM.Theme.getSize("default_lining").width
border.color:
{
if(!control.enabled)
return UM.Theme.getColor("action_button_disabled_border");
else if(control.pressed)
return UM.Theme.getColor("print_button_ready_pressed_border");
else if(control.hovered)
return UM.Theme.getColor("print_button_ready_hovered_border");
else
return UM.Theme.getColor("print_button_ready_border");
}
color:
{
if(!control.enabled)
return UM.Theme.getColor("action_button_disabled");
else if(control.pressed)
return UM.Theme.getColor("print_button_ready_pressed");
else if(control.hovered)
return UM.Theme.getColor("print_button_ready_hovered");
else
return UM.Theme.getColor("print_button_ready");
}
Behavior on color { ColorAnimation { duration: 50; } }
implicitWidth: actualLabel.contentWidth + (UM.Theme.getSize("sidebar_margin").width * 2)
Label {
id: actualLabel
anchors.centerIn: parent
color:
{
if(!control.enabled)
return UM.Theme.getColor("action_button_disabled_text");
else if(control.pressed)
return UM.Theme.getColor("print_button_ready_text");
else if(control.hovered)
return UM.Theme.getColor("print_button_ready_text");
else
return UM.Theme.getColor("print_button_ready_text");
}
font: UM.Theme.getFont("action_button")
text: control.text;
}
}
label: Item { }
}
}

View file

@ -1,33 +0,0 @@
import QtQuick 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.2 as UM
Item
{
id: extruderInfo
property var printCoreConfiguration
width: Math.round(parent.width / 2)
height: childrenRect.height
Label
{
id: materialLabel
text: printCoreConfiguration.activeMaterial != null ? printCoreConfiguration.activeMaterial.name : ""
elide: Text.ElideRight
width: parent.width
font: UM.Theme.getFont("very_small")
}
Label
{
id: printCoreLabel
text: printCoreConfiguration.hotendID
anchors.top: materialLabel.bottom
elide: Text.ElideRight
width: parent.width
font: UM.Theme.getFont("very_small")
opacity: 0.5
}
}

View file

@ -1,431 +0,0 @@
import QtQuick 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.3 as UM
Rectangle
{
function strPadLeft(string, pad, length)
{
return (new Array(length + 1).join(pad) + string).slice(-length);
}
function getPrettyTime(time)
{
return OutputDevice.formatDuration(time)
}
function formatPrintJobPercent(printJob)
{
if (printJob == null)
{
return "";
}
if (printJob.timeTotal === 0)
{
return "";
}
return Math.min(100, Math.round(printJob.timeElapsed / printJob.timeTotal * 100)) + "%";
}
function printerStatusText(printer)
{
switch (printer.state)
{
case "pre_print":
return catalog.i18nc("@label:status", "Preparing to print")
case "printing":
return catalog.i18nc("@label:status", "Printing");
case "idle":
return catalog.i18nc("@label:status", "Available");
case "unreachable":
return catalog.i18nc("@label:status", "Lost connection with the printer");
case "maintenance":
return catalog.i18nc("@label:status", "Unavailable");
default:
return catalog.i18nc("@label:status", "Unknown");
}
}
id: printerDelegate
property var printer: null
property var printJob: printer != null ? printer.activePrintJob: null
border.width: UM.Theme.getSize("default_lining").width
border.color: mouse.containsMouse ? emphasisColor : lineColor
z: mouse.containsMouse ? 1 : 0 // Push this item up a bit on mouse over to ensure that the highlighted bottom border is visible.
MouseArea
{
id: mouse
anchors.fill:parent
onClicked: OutputDevice.setActivePrinter(printer)
hoverEnabled: true;
// Only clickable if no printer is selected
enabled: OutputDevice.activePrinter == null && printer.state !== "unreachable"
}
Row
{
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.margins: UM.Theme.getSize("default_margin").width
Rectangle
{
width: Math.round(parent.width / 3)
height: parent.height
Label // Print job name
{
id: jobNameLabel
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
text: printJob != null ? printJob.name : ""
font: UM.Theme.getFont("default_bold")
elide: Text.ElideRight
}
Label
{
id: jobOwnerLabel
anchors.top: jobNameLabel.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
text: printJob != null ? printJob.owner : ""
opacity: 0.50
elide: Text.ElideRight
}
Label
{
id: totalTimeLabel
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
text: printJob != null ? getPrettyTime(printJob.timeTotal) : ""
opacity: 0.65
font: UM.Theme.getFont("default")
elide: Text.ElideRight
}
}
Rectangle
{
width: Math.round(parent.width / 3 * 2)
height: parent.height
Label // Friendly machine name
{
id: printerNameLabel
anchors.top: parent.top
anchors.left: parent.left
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width - showCameraIcon.width)
text: printer.name
font: UM.Theme.getFont("default_bold")
elide: Text.ElideRight
}
Label // Machine variant
{
id: printerTypeLabel
anchors.top: printerNameLabel.bottom
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width)
text: printer.type
anchors.left: parent.left
elide: Text.ElideRight
font: UM.Theme.getFont("very_small")
opacity: 0.50
}
Rectangle // Camera icon
{
id: showCameraIcon
width: 40 * screenScaleFactor
height: width
radius: width
anchors.right: printProgressArea.left
anchors.rightMargin: UM.Theme.getSize("default_margin").width
color: emphasisColor
opacity: printer != null && printer.state === "unreachable" ? 0.3 : 1
Image
{
width: parent.width
height: width
anchors.right: parent.right
anchors.rightMargin: parent.rightMargin
source: "camera-icon.svg"
}
}
Row // PrintCore config
{
id: extruderInfo
anchors.bottom: parent.bottom
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width)
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
PrintCoreConfiguration
{
id: leftExtruderInfo
width: Math.round((parent.width - extruderSeperator.width) / 2)
printCoreConfiguration: printer.extruders[0]
}
Rectangle
{
id: extruderSeperator
width: UM.Theme.getSize("default_lining").width
height: parent.height
color: lineColor
}
PrintCoreConfiguration
{
id: rightExtruderInfo
width: Math.round((parent.width - extruderSeperator.width) / 2)
printCoreConfiguration: printer.extruders[1]
}
}
Rectangle // Print progress
{
id: printProgressArea
anchors.right: parent.right
anchors.top: parent.top
height: showExtended ? parent.height: printProgressTitleBar.height
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width)
border.width: UM.Theme.getSize("default_lining").width
border.color: lineColor
radius: cornerRadius
property var showExtended: {
if(printJob != null)
{
var extendStates = ["sent_to_printer", "wait_for_configuration", "printing", "pre_print", "post_print", "wait_cleanup", "queued"];
return extendStates.indexOf(printJob.state) !== -1;
}
return printer.state == "disabled"
}
Item // Status and Percent
{
id: printProgressTitleBar
property var showPercent: {
return printJob != null && (["printing", "post_print", "pre_print", "sent_to_printer"].indexOf(printJob.state) !== -1);
}
width: parent.width
//TODO: hardcoded value
height: 40 * screenScaleFactor
anchors.left: parent.left
Label
{
id: statusText
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: progressText.left
anchors.rightMargin: UM.Theme.getSize("default_margin").width
anchors.verticalCenter: parent.verticalCenter
text: {
if (printer.state == "disabled")
{
return catalog.i18nc("@label:status", "Disabled");
}
if (printer.state === "unreachable")
{
return printerStatusText(printer);
}
if (printJob != null)
{
switch (printJob.state)
{
case "printing":
case "post_print":
return catalog.i18nc("@label:status", "Printing")
case "wait_for_configuration":
return catalog.i18nc("@label:status", "Reserved")
case "wait_cleanup":
case "wait_user_action":
return catalog.i18nc("@label:status", "Finished")
case "pre_print":
case "sent_to_printer":
return catalog.i18nc("@label", "Preparing to print")
case "queued":
return catalog.i18nc("@label:status", "Action required");
case "pausing":
case "paused":
return catalog.i18nc("@label:status", "Paused");
case "resuming":
return catalog.i18nc("@label:status", "Resuming");
case "aborted":
return catalog.i18nc("@label:status", "Print aborted");
default:
// If print job has unknown status show printer.status
return printerStatusText(printer);
}
}
return printerStatusText(printer);
}
elide: Text.ElideRight
font: UM.Theme.getFont("small")
}
Label
{
id: progressText
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
anchors.top: statusText.top
text: formatPrintJobPercent(printJob)
visible: printProgressTitleBar.showPercent
//TODO: Hardcoded value
opacity: 0.65
font: UM.Theme.getFont("very_small")
}
Image
{
width: statusText.height
height: width
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
anchors.top: statusText.top
visible: !printProgressTitleBar.showPercent
source: {
if (printer.state == "disabled")
{
return "blocked-icon.svg";
}
if (printer.state === "unreachable")
{
return "";
}
if (printJob != null)
{
if(printJob.state === "queued")
{
return "action-required-icon.svg";
}
else if (printJob.state === "wait_cleanup")
{
return "checkmark-icon.svg";
}
}
return ""; // We're not going to show it, so it will not be resolved as a url.
}
}
Rectangle
{
//TODO: This will become a progress bar in the future
width: parent.width
height: UM.Theme.getSize("default_lining").height
anchors.bottom: parent.bottom
anchors.left: parent.left
visible: printProgressArea.showExtended
color: lineColor
}
}
Column
{
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.top: printProgressTitleBar.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width - 2 * UM.Theme.getSize("default_margin").width
visible: printProgressArea.showExtended
Label // Status detail
{
text:
{
if (printer.state == "disabled")
{
return catalog.i18nc("@label", "Not accepting print jobs");
}
if (printer.state === "unreachable")
{
return "";
}
if(printJob != null)
{
switch (printJob.state)
{
case "printing":
case "post_print":
return catalog.i18nc("@label", "Finishes at: ") + OutputDevice.getTimeCompleted(printJob.timeTotal - printJob.timeElapsed)
case "wait_cleanup":
return catalog.i18nc("@label", "Clear build plate")
case "sent_to_printer":
case "pre_print":
return catalog.i18nc("@label", "Preparing to print")
case "wait_for_configuration":
return catalog.i18nc("@label", "Not accepting print jobs")
case "queued":
return catalog.i18nc("@label", "Waiting for configuration change");
default:
return "";
}
}
return "";
}
anchors.left: parent.left
anchors.right: parent.right
elide: Text.ElideRight
wrapMode: Text.Wrap
font: UM.Theme.getFont("default")
}
Label // Status 2nd row
{
text: {
if(printJob != null)
{
if(printJob.state == "printing" || printJob.state == "post_print")
{
return OutputDevice.getDateCompleted(printJob.timeTotal - printJob.timeElapsed)
}
}
return "";
}
elide: Text.ElideRight
font: UM.Theme.getFont("default")
}
}
}
}
}
}

View file

@ -1,54 +0,0 @@
import QtQuick 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.3 as UM
import Cura 1.0 as Cura
Rectangle
{
id: base
width: 250 * screenScaleFactor
height: 250 * screenScaleFactor
signal clicked()
MouseArea
{
anchors.fill:parent
onClicked: base.clicked()
}
Rectangle
{
// TODO: Actually add UM icon / picture
width: 100 * screenScaleFactor
height: 100 * screenScaleFactor
border.width: UM.Theme.getSize("default_lining").width
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: UM.Theme.getSize("default_margin").height
}
Label
{
id: nameLabel
anchors.bottom: ipLabel.top
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.rightMargin: UM.Theme.getSize("default_margin").width
text: modelData.friendly_name.toString()
font: UM.Theme.getFont("large")
elide: Text.ElideMiddle;
height: UM.Theme.getSize("section").height;
}
Label
{
id: ipLabel
text: modelData.ip_address.toString()
anchors.bottom: parent.bottom
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
font: UM.Theme.getFont("default")
height:10 * screenScaleFactor
anchors.horizontalCenter: parent.horizontalCenter
}
}

View file

@ -1,11 +1,11 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from . import DiscoverUM3Action from .src import DiscoverUM3Action
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
from . import UM3OutputDevicePlugin from .src import UM3OutputDevicePlugin
def getMetaData(): def getMetaData():
return {} return {}

View file

@ -0,0 +1,729 @@
import QtQuick 2.3
import QtQuick.Dialogs 1.1
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.3
import QtGraphicalEffects 1.0
import QtQuick.Controls 2.0 as Controls2
import UM 1.3 as UM
import Cura 1.0 as Cura
Component
{
Rectangle
{
id: base
property var lineColor: "#DCDCDC" // TODO: Should be linked to theme.
property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme.
visible: OutputDevice != null
anchors.fill: parent
color: "white"
UM.I18nCatalog
{
id: catalog
name: "cura"
}
Label
{
id: printingLabel
font: UM.Theme.getFont("large")
anchors
{
margins: 2 * UM.Theme.getSize("default_margin").width
leftMargin: 4 * UM.Theme.getSize("default_margin").width
top: parent.top
left: parent.left
right: parent.right
}
text: catalog.i18nc("@label", "Printing")
elide: Text.ElideRight
}
Label
{
id: managePrintersLabel
anchors.rightMargin: 4 * UM.Theme.getSize("default_margin").width
anchors.right: printerScrollView.right
anchors.bottom: printingLabel.bottom
text: catalog.i18nc("@label link to connect manager", "Manage printers")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("primary")
linkColor: UM.Theme.getColor("primary")
}
MouseArea
{
anchors.fill: managePrintersLabel
hoverEnabled: true
onClicked: Cura.MachineManager.printerOutputDevices[0].openPrinterControlPanel()
onEntered: managePrintersLabel.font.underline = true
onExited: managePrintersLabel.font.underline = false
}
ScrollView
{
id: printerScrollView
anchors
{
top: printingLabel.bottom
left: parent.left
right: parent.right
topMargin: UM.Theme.getSize("default_margin").height
bottom: parent.bottom
bottomMargin: UM.Theme.getSize("default_margin").height
}
style: UM.Theme.styles.scrollview
ListView
{
anchors
{
top: parent.top
bottom: parent.bottom
left: parent.left
right: parent.right
leftMargin: 2 * UM.Theme.getSize("default_margin").width
rightMargin: 2 * UM.Theme.getSize("default_margin").width
}
spacing: UM.Theme.getSize("default_margin").height -10
model: OutputDevice.printers
delegate: Item
{
width: parent.width
height: base.height + 2 * base.shadowRadius // To ensure that the shadow doesn't get cut off.
Rectangle
{
width: parent.width - 2 * shadowRadius
height: childrenRect.height + UM.Theme.getSize("default_margin").height
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
id: base
property var shadowRadius: 5
property var collapsed: true
layer.enabled: true
layer.effect: DropShadow
{
radius: base.shadowRadius
verticalOffset: 2
color: "#3F000000" // 25% shadow
}
Item
{
id: printerInfo
height: machineIcon.height
anchors
{
top: parent.top
left: parent.left
right: parent.right
margins: UM.Theme.getSize("default_margin").width
}
MouseArea
{
anchors.fill: parent
onClicked: base.collapsed = !base.collapsed
}
Item
{
id: machineIcon
// Yeah, this is hardcoded now, but I can't think of a good way to fix this.
// The UI is going to get another update soon, so it's probably not worth the effort...
width: 58
height: 58
anchors.top: parent.top
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.left: parent.left
UM.RecolorImage
{
anchors.centerIn: parent
source:
{
switch(modelData.type)
{
case "Ultimaker 3":
return "../svg/UM3-icon.svg"
case "Ultimaker 3 Extended":
return "../svg/UM3x-icon.svg"
case "Ultimaker S5":
return "../svg/UMs5-icon.svg"
}
}
width: sourceSize.width
height: sourceSize.height
color:
{
if(modelData.state == "disabled")
{
return UM.Theme.getColor("setting_control_disabled")
}
if(modelData.activePrintJob != undefined)
{
return UM.Theme.getColor("primary")
}
return UM.Theme.getColor("setting_control_disabled")
}
}
}
Item
{
height: childrenRect.height
anchors
{
right: collapseIcon.left
rightMargin: UM.Theme.getSize("default_margin").width
left: machineIcon.right
leftMargin: UM.Theme.getSize("default_margin").width
verticalCenter: machineIcon.verticalCenter
}
Label
{
id: machineNameLabel
text: modelData.name
width: parent.width
elide: Text.ElideRight
font: UM.Theme.getFont("default_bold")
}
Label
{
id: activeJobLabel
text:
{
if (modelData.state == "disabled")
{
return catalog.i18nc("@label", "Not available")
} else if (modelData.state == "unreachable")
{
return catalog.i18nc("@label", "Unreachable")
}
if (modelData.activePrintJob != null)
{
return modelData.activePrintJob.name
}
return catalog.i18nc("@label", "Available")
}
anchors.top: machineNameLabel.bottom
width: parent.width
elide: Text.ElideRight
font: UM.Theme.getFont("default")
opacity: 0.6
}
}
UM.RecolorImage
{
id: collapseIcon
width: 15
height: 15
sourceSize.width: width
sourceSize.height: height
source: base.collapsed ? UM.Theme.getIcon("arrow_left") : UM.Theme.getIcon("arrow_bottom")
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
color: "black"
}
}
Item
{
id: detailedInfo
property var printJob: modelData.activePrintJob
visible: height == childrenRect.height
anchors.top: printerInfo.bottom
width: parent.width
height: !base.collapsed ? childrenRect.height : 0
opacity: visible ? 1 : 0
Behavior on height { NumberAnimation { duration: 100 } }
Behavior on opacity { NumberAnimation { duration: 100 } }
Rectangle
{
id: topSpacer
color: UM.Theme.getColor("viewport_background")
height: 2
anchors
{
left: parent.left
right: parent.right
margins: UM.Theme.getSize("default_margin").width
top: parent.top
topMargin: UM.Theme.getSize("default_margin").width
}
}
PrinterFamilyPill
{
id: printerFamilyPill
color: UM.Theme.getColor("viewport_background")
anchors.top: topSpacer.bottom
anchors.topMargin: 2 * UM.Theme.getSize("default_margin").height
text: modelData.type
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
padding: 3
}
Row
{
id: extrudersInfo
anchors.top: printerFamilyPill.bottom
anchors.topMargin: 2 * UM.Theme.getSize("default_margin").height
anchors.left: parent.left
anchors.leftMargin: 2 * UM.Theme.getSize("default_margin").width
anchors.right: parent.right
anchors.rightMargin: 2 * UM.Theme.getSize("default_margin").width
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
PrintCoreConfiguration
{
id: leftExtruderInfo
width: Math.round(parent.width / 2)
printCoreConfiguration: modelData.printerConfiguration.extruderConfigurations[0]
}
PrintCoreConfiguration
{
id: rightExtruderInfo
width: Math.round(parent.width / 2)
printCoreConfiguration: modelData.printerConfiguration.extruderConfigurations[1]
}
}
Rectangle
{
id: jobSpacer
color: UM.Theme.getColor("viewport_background")
height: 2
anchors
{
left: parent.left
right: parent.right
margins: UM.Theme.getSize("default_margin").width
top: extrudersInfo.bottom
topMargin: 2 * UM.Theme.getSize("default_margin").height
}
}
Item
{
id: jobInfo
property var showJobInfo: modelData.activePrintJob != null && modelData.activePrintJob.state != "queued"
anchors.top: jobSpacer.bottom
anchors.topMargin: 2 * UM.Theme.getSize("default_margin").height
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: UM.Theme.getSize("default_margin").width
anchors.leftMargin: 2 * UM.Theme.getSize("default_margin").width
height: showJobInfo ? childrenRect.height + 2 * UM.Theme.getSize("default_margin").height: 0
visible: showJobInfo
Label
{
id: printJobName
text: modelData.activePrintJob != null ? modelData.activePrintJob.name : ""
font: UM.Theme.getFont("default_bold")
anchors.left: parent.left
anchors.right: contextButton.left
anchors.rightMargin: UM.Theme.getSize("default_margin").width
elide: Text.ElideRight
}
Label
{
id: ownerName
anchors.top: printJobName.bottom
text: modelData.activePrintJob != null ? modelData.activePrintJob.owner : ""
font: UM.Theme.getFont("default")
opacity: 0.6
width: parent.width
elide: Text.ElideRight
}
function switchPopupState()
{
if (popup.visible)
{
popup.close()
}
else
{
popup.open()
}
}
Controls2.Button
{
id: contextButton
text: "\u22EE" //Unicode; Three stacked points.
font.pixelSize: 25
width: 35
height: width
anchors
{
right: parent.right
top: parent.top
}
hoverEnabled: true
background: Rectangle
{
opacity: contextButton.down || contextButton.hovered ? 1 : 0
width: contextButton.width
height: contextButton.height
radius: 0.5 * width
color: UM.Theme.getColor("viewport_background")
}
onClicked: parent.switchPopupState()
}
Controls2.Popup
{
// TODO Change once updating to Qt5.10 - The 'opened' property is in 5.10 but the behavior is now implemented with the visible property
id: popup
clip: true
closePolicy: Controls2.Popup.CloseOnPressOutsideParent
x: parent.width - width
y: contextButton.height
width: 160
height: contentItem.height + 2 * padding
visible: false
transformOrigin: Controls2.Popup.Top
contentItem: Item
{
width: popup.width - 2 * popup.padding
height: childrenRect.height + 15
Controls2.Button
{
id: pauseButton
text: modelData.activePrintJob != null && modelData.activePrintJob.state == "paused" ? catalog.i18nc("@label", "Resume") : catalog.i18nc("@label", "Pause")
onClicked:
{
if(modelData.activePrintJob.state == "paused")
{
modelData.activePrintJob.setState("print")
}
else if(modelData.activePrintJob.state == "printing")
{
modelData.activePrintJob.setState("pause")
}
popup.close()
}
width: parent.width
enabled: modelData.activePrintJob != null && ["paused", "printing"].indexOf(modelData.activePrintJob.state) >= 0
anchors.top: parent.top
anchors.topMargin: 10
hoverEnabled: true
background: Rectangle
{
opacity: pauseButton.down || pauseButton.hovered ? 1 : 0
color: UM.Theme.getColor("viewport_background")
}
}
Controls2.Button
{
id: abortButton
text: catalog.i18nc("@label", "Abort")
onClicked:
{
abortConfirmationDialog.visible = true;
popup.close();
}
width: parent.width
anchors.top: pauseButton.bottom
hoverEnabled: true
enabled: modelData.activePrintJob != null && ["paused", "printing", "pre_print"].indexOf(modelData.activePrintJob.state) >= 0
background: Rectangle
{
opacity: abortButton.down || abortButton.hovered ? 1 : 0
color: UM.Theme.getColor("viewport_background")
}
}
MessageDialog
{
id: abortConfirmationDialog
title: catalog.i18nc("@window:title", "Abort print")
icon: StandardIcon.Warning
text: catalog.i18nc("@label %1 is the name of a print job.", "Are you sure you want to abort %1?").arg(modelData.activePrintJob.name)
standardButtons: StandardButton.Yes | StandardButton.No
Component.onCompleted: visible = false
onYes: modelData.activePrintJob.setState("abort")
}
}
background: Item
{
width: popup.width
height: popup.height
DropShadow
{
anchors.fill: pointedRectangle
radius: 5
color: "#3F000000" // 25% shadow
source: pointedRectangle
transparentBorder: true
verticalOffset: 2
}
Item
{
id: pointedRectangle
width: parent.width -10
height: parent.height -10
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
Rectangle
{
id: point
height: 13
width: 13
color: UM.Theme.getColor("setting_control")
transform: Rotation { angle: 45}
anchors.right: bloop.right
y: 1
}
Rectangle
{
id: bloop
color: UM.Theme.getColor("setting_control")
width: parent.width
anchors.top: parent.top
anchors.topMargin: 10
anchors.bottom: parent.bottom
anchors.bottomMargin: 5
}
}
}
exit: Transition
{
// This applies a default NumberAnimation to any changes a state change makes to x or y properties
NumberAnimation { property: "visible"; duration: 75; }
}
enter: Transition
{
// This applies a default NumberAnimation to any changes a state change makes to x or y properties
NumberAnimation { property: "visible"; duration: 75; }
}
onClosed: visible = false
onOpened: visible = true
}
Image
{
id: printJobPreview
source: modelData.activePrintJob != null ? modelData.activePrintJob.previewImageUrl : ""
anchors.top: ownerName.bottom
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width / 2
height: width
opacity:
{
if(modelData.activePrintJob == null)
{
return 1.0
}
switch(modelData.activePrintJob.state)
{
case "wait_cleanup":
case "wait_user_action":
case "paused":
return 0.5
default:
return 1.0
}
}
}
UM.RecolorImage
{
id: statusImage
anchors.centerIn: printJobPreview
source:
{
if(modelData.activePrintJob == null)
{
return ""
}
switch(modelData.activePrintJob.state)
{
case "paused":
return "../svg/paused-icon.svg"
case "wait_cleanup":
if(modelData.activePrintJob.timeElapsed < modelData.activePrintJob.timeTotal)
{
return "../svg/aborted-icon.svg"
}
return "../svg/approved-icon.svg"
case "wait_user_action":
return "../svg/aborted-icon.svg"
default:
return ""
}
}
visible: source != ""
width: 0.5 * printJobPreview.width
height: 0.5 * printJobPreview.height
sourceSize.width: width
sourceSize.height: height
color: "black"
}
Rectangle
{
id: showCameraIcon
width: 35 * screenScaleFactor
height: width
radius: 0.5 * width
anchors.left: parent.left
anchors.bottom: printJobPreview.bottom
color: UM.Theme.getColor("setting_control_border_highlight")
Image
{
width: parent.width
height: width
anchors.right: parent.right
anchors.rightMargin: parent.rightMargin
source: "../svg/camera-icon.svg"
}
MouseArea
{
anchors.fill:parent
onClicked:
{
OutputDevice.setActiveCamera(modelData.camera)
}
}
}
}
}
ProgressBar
{
property var progress:
{
if(modelData.activePrintJob == null)
{
return 0
}
var result = modelData.activePrintJob.timeElapsed / modelData.activePrintJob.timeTotal
if(result > 1.0)
{
result = 1.0
}
return result
}
id: jobProgressBar
width: parent.width
value: progress
anchors.top: detailedInfo.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
visible: modelData.activePrintJob != null && modelData.activePrintJob != undefined
style: ProgressBarStyle
{
property var progressText:
{
if(modelData.activePrintJob == null)
{
return ""
}
switch(modelData.activePrintJob.state)
{
case "wait_cleanup":
if(modelData.activePrintJob.timeTotal > modelData.activePrintJob.timeElapsed)
{
return catalog.i18nc("@label:status", "Aborted")
}
return catalog.i18nc("@label:status", "Finished")
case "pre_print":
case "sent_to_printer":
return catalog.i18nc("@label:status", "Preparing")
case "aborted":
case "wait_user_action":
return catalog.i18nc("@label:status", "Aborted")
case "pausing":
return catalog.i18nc("@label:status", "Pausing")
case "paused":
return catalog.i18nc("@label:status", "Paused")
case "resuming":
return catalog.i18nc("@label:status", "Resuming")
case "queued":
return catalog.i18nc("@label:status", "Action required")
default:
OutputDevice.formatDuration(modelData.activePrintJob.timeTotal - modelData.activePrintJob.timeElapsed)
}
}
background: Rectangle
{
implicitWidth: 100
implicitHeight: visible ? 24 : 0
color: UM.Theme.getColor("viewport_background")
}
progress: Rectangle
{
color: UM.Theme.getColor("primary")
id: progressItem
function getTextOffset()
{
if(progressItem.width + progressLabel.width < control.width)
{
return progressItem.width + UM.Theme.getSize("default_margin").width
}
else
{
return progressItem.width - progressLabel.width - UM.Theme.getSize("default_margin").width
}
}
Label
{
id: progressLabel
anchors.left: parent.left
anchors.leftMargin: getTextOffset()
text: progressText
anchors.verticalCenter: parent.verticalCenter
color: progressItem.width + progressLabel.width < control.width ? "black" : "white"
width: contentWidth
font: UM.Theme.getFont("default")
}
}
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,108 @@
import QtQuick 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.3 as UM
import Cura 1.0 as Cura
Component
{
Rectangle
{
id: monitorFrame
width: maximumWidth
height: maximumHeight
color: UM.Theme.getColor("viewport_background")
property var emphasisColor: UM.Theme.getColor("setting_control_border_highlight")
property var lineColor: "#DCDCDC" // TODO: Should be linked to theme.
property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme.
UM.I18nCatalog
{
id: catalog
name: "cura"
}
Label
{
id: manageQueueLabel
anchors.rightMargin: 4 * UM.Theme.getSize("default_margin").width
anchors.right: queuedPrintJobs.right
anchors.bottom: queuedLabel.bottom
text: catalog.i18nc("@label link to connect manager", "Manage queue")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("primary")
linkColor: UM.Theme.getColor("primary")
}
MouseArea
{
anchors.fill: manageQueueLabel
hoverEnabled: true
onClicked: Cura.MachineManager.printerOutputDevices[0].openPrintJobControlPanel()
onEntered: manageQueueLabel.font.underline = true
onExited: manageQueueLabel.font.underline = false
}
Label
{
id: queuedLabel
anchors.left: queuedPrintJobs.left
anchors.top: parent.top
anchors.topMargin: 2 * UM.Theme.getSize("default_margin").height
anchors.leftMargin: 3 * UM.Theme.getSize("default_margin").width
text: catalog.i18nc("@label", "Queued")
font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text")
}
ScrollView
{
id: queuedPrintJobs
anchors
{
top: queuedLabel.bottom
topMargin: UM.Theme.getSize("default_margin").height
horizontalCenter: parent.horizontalCenter
bottomMargin: 0
bottom: parent.bottom
}
style: UM.Theme.styles.scrollview
width: Math.min(800 * screenScaleFactor, maximumWidth)
ListView
{
anchors.fill: parent
//anchors.margins: UM.Theme.getSize("default_margin").height
spacing: UM.Theme.getSize("default_margin").height - 10 // 2x the shadow radius
model: OutputDevice.queuedPrintJobs
delegate: PrintJobInfoBlock
{
printJob: modelData
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").height
anchors.leftMargin: UM.Theme.getSize("default_margin").height
height: 175 * screenScaleFactor
}
}
}
PrinterVideoStream
{
visible: OutputDevice.activeCamera != null
anchors.fill: parent
camera: OutputDevice.activeCamera
}
onVisibleChanged:
{
if (monitorFrame != null && !monitorFrame.visible)
{
OutputDevice.setActiveCamera(null)
}
}
}
}

View file

@ -0,0 +1,93 @@
import QtQuick 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.2 as UM
Item
{
id: extruderInfo
property var printCoreConfiguration
width: Math.round(parent.width / 2)
height: childrenRect.height
Item
{
id: extruderCircle
width: 30
height: 30
anchors.verticalCenter: printAndMaterialLabel.verticalCenter
opacity:
{
if(printCoreConfiguration == null || printCoreConfiguration.activeMaterial == null || printCoreConfiguration.hotendID == null)
{
return 0.5
}
return 1
}
Rectangle
{
anchors.fill: parent
radius: Math.round(width / 2)
border.width: 2
border.color: "black"
}
Label
{
anchors.centerIn: parent
font: UM.Theme.getFont("default_bold")
text: printCoreConfiguration.position + 1
}
}
Item
{
id: printAndMaterialLabel
anchors
{
right: parent.right
left: extruderCircle.right
margins: UM.Theme.getSize("default_margin").width
}
height: childrenRect.height
Label
{
id: materialLabel
text:
{
if(printCoreConfiguration != undefined && printCoreConfiguration.activeMaterial != undefined)
{
return printCoreConfiguration.activeMaterial.name
}
return ""
}
font: UM.Theme.getFont("default_bold")
elide: Text.ElideRight
width: parent.width
}
Label
{
id: printCoreLabel
text:
{
if(printCoreConfiguration != undefined && printCoreConfiguration.hotendID != undefined)
{
return printCoreConfiguration.hotendID
}
return ""
}
anchors.top: materialLabel.bottom
elide: Text.ElideRight
width: parent.width
opacity: 0.6
font: UM.Theme.getFont("default")
}
}
}

View file

@ -0,0 +1,401 @@
import QtQuick 2.2
import QtQuick.Dialogs 1.1
import QtQuick.Controls 2.0
import QtQuick.Controls.Styles 1.4
import QtGraphicalEffects 1.0
import UM 1.3 as UM
Item
{
id: base
property var printJob: null
property var shadowRadius: 5
function getPrettyTime(time)
{
return OutputDevice.formatDuration(time)
}
UM.I18nCatalog
{
id: catalog
name: "cura"
}
Rectangle
{
id: background
anchors
{
top: parent.top
topMargin: 3
left: parent.left
leftMargin: base.shadowRadius
rightMargin: base.shadowRadius
right: parent.right
bottom: parent.bottom
bottomMargin: base.shadowRadius
}
layer.enabled: true
layer.effect: DropShadow
{
radius: base.shadowRadius
verticalOffset: 2
color: "#3F000000" // 25% shadow
}
Item
{
// Content on the left of the infobox
anchors
{
top: parent.top
bottom: parent.bottom
left: parent.left
right: parent.horizontalCenter
margins: 2 * UM.Theme.getSize("default_margin").width
rightMargin: UM.Theme.getSize("default_margin").width
}
Label
{
id: printJobName
text: printJob.name
font: UM.Theme.getFont("default_bold")
width: parent.width
elide: Text.ElideRight
}
Label
{
id: ownerName
anchors.top: printJobName.bottom
text: printJob.owner
font: UM.Theme.getFont("default")
opacity: 0.6
width: parent.width
elide: Text.ElideRight
}
Image
{
id: printJobPreview
source: printJob.previewImageUrl
anchors.top: ownerName.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: totalTimeLabel.bottom
width: height
opacity: printJob.state == "error" ? 0.5 : 1.0
}
UM.RecolorImage
{
id: statusImage
anchors.centerIn: printJobPreview
source: printJob.state == "error" ? "../svg/aborted-icon.svg" : ""
visible: source != ""
width: 0.5 * printJobPreview.width
height: 0.5 * printJobPreview.height
sourceSize.width: width
sourceSize.height: height
color: "black"
}
Label
{
id: totalTimeLabel
opacity: 0.6
anchors.bottom: parent.bottom
anchors.right: parent.right
font: UM.Theme.getFont("default")
text: printJob != null ? getPrettyTime(printJob.timeTotal) : ""
elide: Text.ElideRight
}
}
Item
{
// Content on the right side of the infobox.
anchors
{
top: parent.top
bottom: parent.bottom
left: parent.horizontalCenter
right: parent.right
margins: 2 * UM.Theme.getSize("default_margin").width
leftMargin: UM.Theme.getSize("default_margin").width
}
Label
{
id: targetPrinterLabel
elide: Text.ElideRight
font: UM.Theme.getFont("default_bold")
text:
{
if(printJob.assignedPrinter == null)
{
if(printJob.state == "error")
{
return catalog.i18nc("@label", "Waiting for: Unavailable printer")
}
return catalog.i18nc("@label", "Waiting for: First available")
}
else
{
return catalog.i18nc("@label", "Waiting for: ") + printJob.assignedPrinter.name
}
}
anchors
{
left: parent.left
right: contextButton.left
rightMargin: UM.Theme.getSize("default_margin").width
}
}
function switchPopupState()
{
popup.visible ? popup.close() : popup.open()
}
Button
{
id: contextButton
text: "\u22EE" //Unicode; Three stacked points.
font.pixelSize: 25
width: 35
height: width
anchors
{
right: parent.right
top: parent.top
}
hoverEnabled: true
background: Rectangle
{
opacity: contextButton.down || contextButton.hovered ? 1 : 0
width: contextButton.width
height: contextButton.height
radius: 0.5 * width
color: UM.Theme.getColor("viewport_background")
}
onClicked: parent.switchPopupState()
}
Popup
{
// TODO Change once updating to Qt5.10 - The 'opened' property is in 5.10 but the behavior is now implemented with the visible property
id: popup
clip: true
closePolicy: Popup.CloseOnPressOutsideParent
x: parent.width - width
y: contextButton.height
width: 160
height: contentItem.height + 2 * padding
visible: false
transformOrigin: Popup.Top
contentItem: Item
{
width: popup.width - 2 * popup.padding
height: childrenRect.height + 15
Button
{
id: sendToTopButton
text: catalog.i18nc("@label", "Move to top")
onClicked:
{
sendToTopConfirmationDialog.visible = true;
popup.close();
}
width: parent.width
enabled: OutputDevice.queuedPrintJobs[0].key != printJob.key
anchors.top: parent.top
anchors.topMargin: 10
hoverEnabled: true
background: Rectangle
{
opacity: sendToTopButton.down || sendToTopButton.hovered ? 1 : 0
color: UM.Theme.getColor("viewport_background")
}
}
MessageDialog
{
id: sendToTopConfirmationDialog
title: catalog.i18nc("@window:title", "Move print job to top")
icon: StandardIcon.Warning
text: catalog.i18nc("@label %1 is the name of a print job.", "Are you sure you want to move %1 to the top of the queue?").arg(printJob.name)
standardButtons: StandardButton.Yes | StandardButton.No
Component.onCompleted: visible = false
onYes: OutputDevice.sendJobToTop(printJob.key)
}
Button
{
id: deleteButton
text: catalog.i18nc("@label", "Delete")
onClicked:
{
deleteConfirmationDialog.visible = true;
popup.close();
}
width: parent.width
anchors.top: sendToTopButton.bottom
hoverEnabled: true
background: Rectangle
{
opacity: deleteButton.down || deleteButton.hovered ? 1 : 0
color: UM.Theme.getColor("viewport_background")
}
}
MessageDialog
{
id: deleteConfirmationDialog
title: catalog.i18nc("@window:title", "Delete print job")
icon: StandardIcon.Warning
text: catalog.i18nc("@label %1 is the name of a print job.", "Are you sure you want to delete %1?").arg(printJob.name)
standardButtons: StandardButton.Yes | StandardButton.No
Component.onCompleted: visible = false
onYes: OutputDevice.deleteJobFromQueue(printJob.key)
}
}
background: Item
{
width: popup.width
height: popup.height
DropShadow
{
anchors.fill: pointedRectangle
radius: 5
color: "#3F000000" // 25% shadow
source: pointedRectangle
transparentBorder: true
verticalOffset: 2
}
Item
{
id: pointedRectangle
width: parent.width -10
height: parent.height -10
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
Rectangle
{
id: point
height: 13
width: 13
color: UM.Theme.getColor("setting_control")
transform: Rotation { angle: 45}
anchors.right: bloop.right
y: 1
}
Rectangle
{
id: bloop
color: UM.Theme.getColor("setting_control")
width: parent.width
anchors.top: parent.top
anchors.topMargin: 10
anchors.bottom: parent.bottom
anchors.bottomMargin: 5
}
}
}
exit: Transition
{
// This applies a default NumberAnimation to any changes a state change makes to x or y properties
NumberAnimation { property: "visible"; duration: 75; }
}
enter: Transition
{
// This applies a default NumberAnimation to any changes a state change makes to x or y properties
NumberAnimation { property: "visible"; duration: 75; }
}
onClosed: visible = false
onOpened: visible = true
}
Row
{
id: printerFamilyPills
spacing: 0.5 * UM.Theme.getSize("default_margin").width
anchors
{
left: parent.left
right: parent.right
bottom: extrudersInfo.top
bottomMargin: UM.Theme.getSize("default_margin").height
}
height: childrenRect.height
Repeater
{
model: printJob.compatibleMachineFamilies
delegate: PrinterFamilyPill
{
text: modelData
color: UM.Theme.getColor("viewport_background")
padding: 3
}
}
}
// PrintCore && Material config
Row
{
id: extrudersInfo
anchors.bottom: parent.bottom
anchors
{
left: parent.left
right: parent.right
}
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
PrintCoreConfiguration
{
id: leftExtruderInfo
width: Math.round(parent.width / 2)
printCoreConfiguration: printJob.configuration.extruderConfigurations[0]
}
PrintCoreConfiguration
{
id: rightExtruderInfo
width: Math.round(parent.width / 2)
printCoreConfiguration: printJob.configuration.extruderConfigurations[1]
}
}
}
Rectangle
{
color: UM.Theme.getColor("viewport_background")
width: 2
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.margins: UM.Theme.getSize("default_margin").height
anchors.horizontalCenter: parent.horizontalCenter
}
}
}

View file

@ -0,0 +1,28 @@
import QtQuick 2.2
import QtQuick.Controls 1.4
import UM 1.2 as UM
Item
{
property alias color: background.color
property alias text: familyNameLabel.text
property var padding: 0
implicitHeight: familyNameLabel.contentHeight + 2 * padding // Apply the padding to top and bottom.
implicitWidth: familyNameLabel.contentWidth + implicitHeight // The extra height is added to ensure the radius doesn't cut something off.
Rectangle
{
id: background
height: parent.height
width: parent.width
color: parent.color
anchors.right: parent.right
anchors.horizontalCenter: parent.horizontalCenter
radius: 0.5 * height
}
Label
{
id: familyNameLabel
anchors.centerIn: parent
text: ""
}
}

View file

@ -7,6 +7,8 @@ import UM 1.3 as UM
Item Item
{ {
property var camera: null
Rectangle Rectangle
{ {
anchors.fill:parent anchors.fill:parent
@ -17,7 +19,7 @@ Item
MouseArea MouseArea
{ {
anchors.fill: parent anchors.fill: parent
onClicked: OutputDevice.setActivePrinter(null) onClicked: OutputDevice.setActiveCamera(null)
z: 0 z: 0
} }
@ -32,7 +34,7 @@ Item
width: 20 * screenScaleFactor width: 20 * screenScaleFactor
height: 20 * screenScaleFactor height: 20 * screenScaleFactor
onClicked: OutputDevice.setActivePrinter(null) onClicked: OutputDevice.setActiveCamera(null)
style: ButtonStyle style: ButtonStyle
{ {
@ -65,23 +67,24 @@ Item
{ {
if(visible) if(visible)
{ {
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null) if(camera != null)
{ {
OutputDevice.activePrinter.camera.start() camera.start()
} }
} else } else
{ {
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null) if(camera != null)
{ {
OutputDevice.activePrinter.camera.stop() camera.stop()
} }
} }
} }
source: source:
{ {
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null && OutputDevice.activePrinter.camera.latestImage) if(camera != null && camera.latestImage != null)
{ {
return OutputDevice.activePrinter.camera.latestImage; return camera.latestImage;
} }
return ""; return "";
} }
@ -92,7 +95,7 @@ Item
anchors.fill: cameraImage anchors.fill: cameraImage
onClicked: onClicked:
{ {
OutputDevice.setActivePrinter(null) OutputDevice.setActiveCamera(null)
} }
z: 1 z: 1
} }

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 46.16 48"><defs><style>.cls-1{fill:#9a9a9a;}</style></defs><title>UM3-icon</title><g id="Symbols"><g id="system_overview_inactive" data-name="system overview inactive"><path id="Shape" class="cls-1" d="M18.4,12.2h9.26c.1,0,.1,0,.2-.2l1.73-4.27a.22.22,0,0,0-.2-.2H16.67a.22.22,0,0,0-.2.2L18.2,12C18.3,12.2,18.3,12.2,18.4,12.2Z"/><path id="Shape-2" data-name="Shape" class="cls-1" d="M38.33,35.08H7.72a.48.48,0,0,0-.5.51V37a.48.48,0,0,0,.5.51H38.44a.48.48,0,0,0,.5-.51V35.59A.64.64,0,0,0,38.33,35.08Z"/><path id="Shape-3" data-name="Shape" class="cls-1" d="M0,0V48H3.76a2.86,2.86,0,0,1,2.13-1H40.27a2.86,2.86,0,0,1,2.13,1h3.76V0ZM41.28,37a2.83,2.83,0,0,1-2.84,2.84H7.72A2.84,2.84,0,0,1,4.88,37V5.49a.65.65,0,0,1,.61-.61H40.67a.65.65,0,0,1,.61.61Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 847 B

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 46.16 58"><defs><style>.cls-1{fill:#9a9a9a;}</style></defs><title>UM3x-icon</title><g id="Symbols"><g id="system_overview_inactive" data-name="system overview inactive"><path id="Shape" class="cls-1" d="M18.4,12.2h9.26c.1,0,.1,0,.2-.2l1.73-4.27a.22.22,0,0,0-.2-.2H16.67a.22.22,0,0,0-.2.2L18.2,12C18.3,12.2,18.3,12.2,18.4,12.2Z"/><path id="Shape-2" data-name="Shape" class="cls-1" d="M38.33,45.08H7.72a.48.48,0,0,0-.5.51V47a.48.48,0,0,0,.5.51H38.44a.48.48,0,0,0,.5-.51V45.59A.64.64,0,0,0,38.33,45.08Z"/><path id="Shape-3" data-name="Shape" class="cls-1" d="M0,0V58H3.76a2.86,2.86,0,0,1,2.13-1H40.27a2.86,2.86,0,0,1,2.13,1h3.76V0ZM41.28,35.32V47a2.83,2.83,0,0,1-2.84,2.84H7.72A2.84,2.84,0,0,1,4.88,47V5.49a.65.65,0,0,1,.61-.61H40.67a.65.65,0,0,1,.61.61Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 854 B

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 58 58"><defs><style>.cls-1{fill:none;}.cls-2{fill:#9a9a9a;}</style></defs><title>UMs5-icon</title><path class="cls-1" d="M33.83,12.33c-.1.2-.1.2-.2.2H24.37c-.1,0-.1,0-.2-.2L22.44,8.06a.22.22,0,0,1,.2-.2H35.36a.22.22,0,0,1,.2.2Z"/><path class="cls-2" d="M35.36,7.86H22.64a.22.22,0,0,0-.2.2l1.73,4.27c.1.2.1.2.2.2h9.26c.1,0,.1,0,.2-.2l1.73-4.27A.22.22,0,0,0,35.36,7.86Z"/><path class="cls-2" d="M0,0V58H3.75a2.85,2.85,0,0,1,2.12-1H52.13a2.85,2.85,0,0,1,2.12,1H58V0ZM37.5,53.82a1.5,1.5,0,0,1-1.5,1.5H22a1.5,1.5,0,0,1-1.5-1.5v-4a1.5,1.5,0,0,1,1.5-1.5H36a1.5,1.5,0,0,1,1.5,1.5Zm15.63-18.5V47a2.83,2.83,0,0,1-2.83,2.84H38.5v0a2.5,2.5,0,0,0-2.5-2.5H22a2.5,2.5,0,0,0-2.5,2.5v0H7.7A2.83,2.83,0,0,1,4.87,47V5.49a.65.65,0,0,1,.6-.61H52.53a.65.65,0,0,1,.6.61Z"/></svg>

After

Width:  |  Height:  |  Size: 842 B

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