Merge pull request #5959 from Ultimaker/feature_intent_per_quality

Feature intent per quality
This commit is contained in:
Lipu Fei 2019-07-03 14:11:20 +02:00 committed by GitHub
commit 5620ee811f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 378 additions and 44 deletions

View file

@ -24,6 +24,8 @@ set(CURA_CLOUD_API_ROOT "" CACHE STRING "Alternative Cura cloud API root")
set(CURA_CLOUD_API_VERSION "" CACHE STRING "Alternative Cura cloud API version")
configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @ONLY)
configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY)

View file

@ -9,6 +9,7 @@ DEFAULT_CURA_DISPLAY_NAME = "Ultimaker Cura"
DEFAULT_CURA_VERSION = "master"
DEFAULT_CURA_BUILD_TYPE = ""
DEFAULT_CURA_DEBUG_MODE = False
DEFAULT_CURA_SDK_VERSION = "6.1.0"
try:
from cura.CuraVersion import CuraAppName # type: ignore
@ -41,4 +42,7 @@ try:
except ImportError:
CuraDebugMode = DEFAULT_CURA_DEBUG_MODE
from cura.CuraVersion import CuraSDKVersion # type: ignore
# Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for
# example Cura 3.2 with SDK version 6.1 will not work. So the SDK version is hard-coded here and left out of the
# CuraVersion.py.in template.
CuraSDKVersion = "6.1.0"

View file

@ -6,9 +6,6 @@ CuraAppDisplayName = "@CURA_APP_DISPLAY_NAME@"
CuraVersion = "@CURA_VERSION@"
CuraBuildType = "@CURA_BUILDTYPE@"
CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
CuraSDKVersion = "6.1.0"
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@"

View file

@ -0,0 +1,47 @@
#Copyright (c) 2019 Ultimaker B.V.
#Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt
import collections
from cura.Settings.IntentManager import IntentManager
from UM.Qt.ListModel import ListModel
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
## Lists the intent categories that are available for the current printer
# configuration.
class IntentCategoryModel(ListModel):
NameRole = Qt.UserRole + 1
IntentCategoryRole = Qt.UserRole + 2
WeightRole = Qt.UserRole + 3
#Translations to user-visible string. Ordered by weight.
#TODO: Create a solution for this name and weight to be used dynamically.
name_translation = collections.OrderedDict() #type: "collections.OrderedDict[str,str]"
name_translation["default"] = catalog.i18nc("@label", "Default")
name_translation["engineering"] = catalog.i18nc("@label", "Engineering")
name_translation["smooth"] = catalog.i18nc("@label", "Smooth")
## Creates a new model for a certain intent category.
# \param The category to list the intent profiles for.
def __init__(self, intent_category: str) -> None:
super().__init__()
self._intent_category = intent_category
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.IntentCategoryRole, "intent_category")
self.addRoleName(self.WeightRole, "weight")
## Updates the list of intents.
def update(self) -> None:
available_categories = IntentManager.getInstance().currentAvailableIntentCategories()
result = []
for category in available_categories:
result.append({
"name": self.name_translation.get(category, catalog.i18nc("@label", "Unknown")),
"intent_category": category,
"weight": list(self.name_translation.items()).index(category)
})
super().update(result)

View file

@ -194,9 +194,9 @@ class QualityManager(QObject):
return quality_changes_group_dict
#
# Gets all quality groups for the given machine. Both available and none available ones will be included.
# Gets all quality groups for the given machine. Both available and unavailable ones will be included.
# It returns a dictionary with "quality_type"s as keys and "QualityGroup"s as values.
# Whether a QualityGroup is available can be unknown via the field QualityGroup.is_available.
# Whether a QualityGroup is available can be known via the field QualityGroup.is_available.
# For more details, see QualityGroup.
#
def getQualityGroups(self, machine: "GlobalStack") -> Dict[str, QualityGroup]:

View file

@ -0,0 +1,141 @@
#Copyright (c) 2019 Ultimaker B.V.
#Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtSignal
from typing import Any, Dict, List, Set, Tuple, TYPE_CHECKING
from cura.CuraApplication import CuraApplication
from cura.Settings.ExtruderManager import ExtruderManager
from UM.Settings.InstanceContainer import InstanceContainer
if TYPE_CHECKING:
from UM.Settings.InstanceContainer import InstanceContainer
## Front-end for querying which intents are available for a certain
# configuration.
#
# CURRENTLY THIS CLASS CONTAINS ONLY SOME PSEUDOCODE OF WHAT WE ARE SUPPOSED
# TO IMPLEMENT.
class IntentManager(QObject):
__instance = None
def __init__(self) -> None:
super().__init__()
CuraApplication.getInstance().getMachineManager().activeStackChanged.connect(self.configurationChanged)
self.configurationChanged.connect(self.selectDefaultIntent)
pass
## This class is a singleton.
@classmethod
def getInstance(cls):
if not cls.__instance:
cls.__instance = IntentManager()
return cls.__instance
configurationChanged = pyqtSignal()
## Gets the metadata dictionaries of all intent profiles for a given
# configuration.
#
# \param definition_id ID of the printer.
# \param nozzle_name Name of the nozzle.
# \param material_id ID of the material.
# \return A list of metadata dictionaries matching the search criteria, or
# an empty list if nothing was found.
def intentMetadatas(self, definition_id: str, nozzle_name: str, material_id: str) -> List[Dict[str, Any]]:
registry = CuraApplication.getInstance().getContainerRegistry()
return registry.findContainersMetadata(definition = definition_id, variant = nozzle_name, material_id = material_id)
## Collects and returns all intent categories available for the given
# parameters. Note that the 'default' category is always available.
#
# \param definition_id ID of the printer.
# \param nozzle_name Name of the nozzle.
# \param material_id ID of the material.
# \return A set of intent category names.
def intentCategories(self, definition_id: str, nozzle_id: str, material_id: str) -> List[str]:
categories = set()
for intent in self.intentMetadatas(definition_id, nozzle_id, material_id):
categories.add(intent["intent_category"])
categories.add("default") #The "empty" intent is not an actual profile specific to the configuration but we do want it to appear in the categories list.
return list(categories)
## List of intents to be displayed in the interface.
#
# For the interface this will have to be broken up into the different
# intent categories. That is up to the model there.
#
# \return A list of tuples of intent_category and quality_type. The actual
# instance may vary per extruder.
def currentAvailableIntents(self) -> List[Tuple[str, str]]:
application = CuraApplication.getInstance()
global_stack = application.getGlobalContainerStack()
if global_stack is None:
return [("default", "normal")]
# TODO: We now do this (return a default) if the global stack is missing, but not in the code below,
# even though there should always be defaults. The problem then is what to do with the quality_types.
# Currently _also_ inconsistent with 'currentAvailableIntentCategoreis', which _does_ return default.
quality_groups = application.getQualityManager().getQualityGroups(global_stack)
available_quality_types = {quality_group.quality_type for quality_group in quality_groups.values() if quality_group.node_for_global is not None}
final_intent_ids = set() # type: Set[str]
current_definition_id = global_stack.definition.getMetaDataEntry("id")
for extruder_stack in ExtruderManager.getInstance().getUsedExtruderStacks():
nozzle_name = extruder_stack.variant.getMetaDataEntry("name")
material_id = extruder_stack.material.getMetaDataEntry("base_file")
final_intent_ids |= {metadata["id"] for metadata in self.intentMetadatas(current_definition_id, nozzle_name, material_id) if metadata["quality_type"] in available_quality_types}
result = set() # type: Set[Tuple[str, str]]
for intent_id in final_intent_ids:
intent_metadata = application.getContainerRegistry().findContainersMetadata(id = intent_id)[0]
result.add((intent_metadata["intent_category"], intent_metadata["quality_type"]))
return list(result)
## List of intent categories available in either of the extruders.
#
# This is purposefully inconsistent with the way that the quality types
# are listed. The quality types will show all quality types available in
# the printer using any configuration. This will only list the intent
# categories that are available using the current configuration (but the
# union over the extruders).
# \return List of all categories in the current configurations of all
# extruders.
def currentAvailableIntentCategories(self) -> List[str]:
global_stack = CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None:
return ["default"]
current_definition_id = global_stack.definition.getMetaDataEntry("id")
final_intent_categories = set() # type: Set[str]
for extruder_stack in ExtruderManager.getInstance().getUsedExtruderStacks():
nozzle_name = extruder_stack.variant.getMetaDataEntry("name")
material_id = extruder_stack.material.getMetaDataEntry("base_file")
final_intent_categories.update(self.intentCategories(current_definition_id, nozzle_name, material_id))
return list(final_intent_categories)
## The intent that gets selected by default when no intent is available for
# the configuration, an extruder can't match the intent that the user
# selects, or just when creating a new printer.
def getDefaultIntent(self) -> InstanceContainer:
return CuraApplication.getInstance().empty_intent_container
## Apply intent on the stacks.
def selectIntent(self, intent_category: str, quality_type: str) -> None:
application = CuraApplication.getInstance()
global_stack = application.getGlobalContainerStack()
if global_stack is None:
return
current_definition_id = global_stack.definition.getMetaDataEntry("id")
for extruder_stack in ExtruderManager.getInstance().getUsedExtruderStacks():
nozzle_name = extruder_stack.variant.getMetaDataEntry("name")
material_id = extruder_stack.material.getMetaDataEntry("base_file")
intent = application.getContainerRegistry().findContainers(definition = current_definition_id, variant = nozzle_name, material = material_id, quality_type = quality_type, intent_category = intent_category)
if intent:
extruder_stack.intent = intent[0]
else:
extruder_stack.intent = self.getDefaultIntent()
application.getMachineManager().setQualityGroupByQualityType(quality_type)
## Selects the default intents on every extruder.
def selectDefaultIntent(self) -> None:
for extruder_stack in ExtruderManager.getInstance().getUsedExtruderStacks():
extruder_stack.intent = self.getDefaultIntent()

View file

@ -6,6 +6,10 @@ definition = fdmprinter
[metadata]
setting_version = 7
type = intent
intent_category = engineering
quality_type = draft
material = generic_abs
variant = AA 0.4
[values]

140
tests/TestIntentManager.py Normal file
View file

@ -0,0 +1,140 @@
from unittest.mock import MagicMock, patch
import pytest
from typing import Any, Dict, List
from cura.Settings.IntentManager import IntentManager
from cura.Machines.QualityGroup import QualityGroup
from cura.Machines.QualityManager import QualityManager
from tests.Settings.MockContainer import MockContainer
@pytest.fixture()
def quality_manager(application, container_registry, global_stack) -> QualityManager:
application.getGlobalContainerStack = MagicMock(return_value = global_stack)
with patch("cura.CuraApplication.CuraApplication.getInstance", MagicMock(return_value=application)):
with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value=container_registry)):
manager = QualityManager(application)
return manager
@pytest.fixture()
def intent_manager(application, extruder_manager, machine_manager, quality_manager, container_registry, global_stack) -> IntentManager:
application.getExtruderManager = MagicMock(return_value = extruder_manager)
application.getGlobalContainerStack = MagicMock(return_value = global_stack)
application.getMachineManager = MagicMock(return_value = machine_manager)
application.getQualityManager = MagicMock(return_value = quality_manager)
with patch("cura.CuraApplication.CuraApplication.getInstance", MagicMock(return_value=application)):
with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value=container_registry)):
manager = IntentManager()
return manager
mocked_intent_metadata = [
{"id": "um3_aa4_pla_smooth_normal", "GUID": "abcxyz", "definition": "ultimaker3", "variant": "AA 0.4",
"material_id": "generic_pla", "intent_category": "smooth", "quality_type": "normal"},
{"id": "um3_aa4_pla_strong_abnorm", "GUID": "defqrs", "definition": "ultimaker3", "variant": "AA 0.4",
"material_id": "generic_pla", "intent_category": "strong", "quality_type": "abnorm"}] # type:List[Dict[str, str]]
mocked_qualitygroup_metadata = {
"normal": QualityGroup("um3_aa4_pla_normal", "normal"),
"abnorm": QualityGroup("um3_aa4_pla_abnorm", "abnorm")} # type:Dict[str, QualityGroup]
def mockFindMetadata(**kwargs) -> List[Dict[str, Any]]:
if "id" in kwargs:
return [x for x in mocked_intent_metadata if x["id"] == kwargs["id"]]
else:
result = []
for data in mocked_intent_metadata:
should_add = True
for key, value in kwargs.items():
if key in data.keys():
should_add &= (data[key] == value)
if should_add:
result.append(data)
return result
def mockFindContainers(**kwargs) -> List[MockContainer]:
result = []
metadatas = mockFindMetadata(**kwargs)
for metadata in metadatas:
result.append(MockContainer(metadata))
return result
def doSetup(application, extruder_manager, quality_manager, container_registry, global_stack) -> None:
container_registry.findContainersMetadata = MagicMock(side_effect=mockFindMetadata)
container_registry.findContainers = MagicMock(side_effect=mockFindContainers)
quality_manager.getQualityGroups = MagicMock(return_value=mocked_qualitygroup_metadata)
for _, qualitygroup in mocked_qualitygroup_metadata.items():
qualitygroup.node_for_global = MagicMock(name="Node for global")
application.getQualityManager = MagicMock(return_value=quality_manager)
global_stack.definition = MockContainer({"id": "ultimaker3"})
application.getGlobalContainerStack = MagicMock(return_value=global_stack)
extruder_stack_a = MockContainer({"id": "Extruder The First"})
extruder_stack_a.variant = MockContainer({"name": "AA 0.4"})
extruder_stack_a.material = MockContainer({"base_file": "generic_pla"})
extruder_stack_b = MockContainer({"id": "Extruder II: Plastic Boogaloo"})
extruder_stack_b.variant = MockContainer({"name": "AA 0.4"})
extruder_stack_b.material = MockContainer({"base_file": "generic_pla"})
extruder_manager.getUsedExtruderStacks = MagicMock(return_value=[extruder_stack_a, extruder_stack_b])
def test_intentCategories(application, intent_manager, container_registry):
# Mock .findContainersMetadata so we also test .intentMetadatas (the latter is mostly a wrapper around the former).
container_registry.findContainersMetadata = MagicMock(return_value=mocked_intent_metadata)
with patch("cura.CuraApplication.CuraApplication.getInstance", MagicMock(return_value=application)):
with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value=container_registry)):
categories = intent_manager.intentCategories("ultimaker3", "AA 0.4", "generic_pla") # type:List[str]
assert "default" in categories, "default should always be in categories"
assert "strong" in categories, "strong should be in categories"
assert "smooth" in categories, "smooth should be in categories"
def test_currentAvailableIntents(application, extruder_manager, quality_manager, intent_manager, container_registry, global_stack):
doSetup(application, extruder_manager, quality_manager, container_registry, global_stack)
with patch("cura.CuraApplication.CuraApplication.getInstance", MagicMock(return_value=application)):
with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value=container_registry)):
with patch("cura.Settings.ExtruderManager.ExtruderManager.getInstance", MagicMock(return_value=extruder_manager)):
intents = intent_manager.currentAvailableIntents()
assert ("smooth", "normal") in intents
assert ("strong", "abnorm") in intents
#assert ("default", "normal") in intents # Pending to-do in 'IntentManager'.
#assert ("default", "abnorm") in intents # Pending to-do in 'IntentManager'.
assert len(intents) == 2 # Or 4? pending to-do in 'IntentManager'.
def test_currentAvailableIntentCategories(application, extruder_manager, quality_manager, intent_manager, container_registry, global_stack):
doSetup(application, extruder_manager, quality_manager, container_registry, global_stack)
with patch("cura.CuraApplication.CuraApplication.getInstance", MagicMock(return_value=application)):
with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value=container_registry)):
with patch("cura.Settings.ExtruderManager.ExtruderManager.getInstance", MagicMock(return_value=extruder_manager)):
categories = intent_manager.currentAvailableIntentCategories()
assert "default" in categories # Currently inconsistent with 'currentAvailableIntents'!
assert "smooth" in categories
assert "strong" in categories
assert len(categories) == 3
def test_selectIntent(application, extruder_manager, quality_manager, intent_manager, container_registry, global_stack):
doSetup(application, extruder_manager, quality_manager, container_registry, global_stack)
with patch("cura.CuraApplication.CuraApplication.getInstance", MagicMock(return_value=application)):
with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value=container_registry)):
with patch("cura.Settings.ExtruderManager.ExtruderManager.getInstance", MagicMock(return_value=extruder_manager)):
intents = intent_manager.currentAvailableIntents()
for intent, quality in intents:
intent_manager.selectIntent(intent, quality)
extruder_stacks = extruder_manager.getUsedExtruderStacks()
assert len(extruder_stacks) == 2
assert extruder_stacks[0].intent.getMetaDataEntry("intent_category") == intent
assert extruder_stacks[1].intent.getMetaDataEntry("intent_category") == intent

View file

@ -2,42 +2,6 @@ from unittest.mock import MagicMock, patch
import pytest
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.MachineManager import MachineManager
@pytest.fixture()
def global_stack():
return MagicMock(name="Global Stack")
@pytest.fixture()
def container_registry() -> ContainerRegistry:
return MagicMock(name = "ContainerRegistry")
@pytest.fixture()
def extruder_manager(application, container_registry) -> ExtruderManager:
if ExtruderManager.getInstance() is not None:
# Reset the data
ExtruderManager._ExtruderManager__instance = None
with patch("cura.CuraApplication.CuraApplication.getInstance", MagicMock(return_value=application)):
with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value=container_registry)):
manager = ExtruderManager()
return manager
@pytest.fixture()
def machine_manager(application, extruder_manager, container_registry, global_stack) -> MachineManager:
application.getExtruderManager = MagicMock(return_value = extruder_manager)
application.getGlobalContainerStack = MagicMock(return_value = global_stack)
with patch("cura.Settings.CuraContainerRegistry.CuraContainerRegistry.getInstance", MagicMock(return_value=container_registry)):
manager = MachineManager(application)
return manager
def test_setActiveMachine(machine_manager):
registry = MagicMock()

View file

@ -3,7 +3,7 @@
# The purpose of this class is to create fixtures or methods that can be shared among all tests.
import unittest.mock
from unittest.mock import MagicMock, patch
import pytest
# Prevents error: "PyCapsule_GetPointer called with incorrect name" with conflicting SIP configurations between Arcus and PyQt: Import Arcus and Savitar first!
@ -13,16 +13,51 @@ from UM.Qt.QtApplication import QtApplication # QtApplication import is require
# Even though your IDE says these files are not used, don't believe it. It's lying. They need to be there.
from cura.CuraApplication import CuraApplication
from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.MachineManager import MachineManager
from cura.UI.MachineActionManager import MachineActionManager
from UM.Settings.ContainerRegistry import ContainerRegistry
# Create a CuraApplication object that will be shared among all tests. It needs to be initialized.
# Since we need to use it more that once, we create the application the first time and use its instance afterwards.
@pytest.fixture()
def application() -> CuraApplication:
app = unittest.mock.MagicMock()
app = MagicMock()
return app
# Returns a MachineActionManager instance.
@pytest.fixture()
def machine_action_manager(application) -> MachineActionManager:
return MachineActionManager(application)
@pytest.fixture()
def global_stack():
return MagicMock(name="Global Stack")
@pytest.fixture()
def container_registry(application, global_stack) -> ContainerRegistry:
result = MagicMock()
result.findContainerStacks = MagicMock(return_value = [global_stack])
application.getContainerRegistry = MagicMock(return_value = result)
return result
@pytest.fixture()
def extruder_manager(application, container_registry) -> ExtruderManager:
if ExtruderManager.getInstance() is not None:
# Reset the data
ExtruderManager._ExtruderManager__instance = None
with patch("cura.CuraApplication.CuraApplication.getInstance", MagicMock(return_value=application)):
with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value=container_registry)):
manager = ExtruderManager()
return manager
@pytest.fixture()
def machine_manager(application, extruder_manager, container_registry, global_stack) -> MachineManager:
application.getExtruderManager = MagicMock(return_value = extruder_manager)
application.getGlobalContainerStack = MagicMock(return_value = global_stack)
with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value=container_registry)):
manager = MachineManager(application)
return manager