Merge branch 'master' into top_bottom_settings_enabled_function

This commit is contained in:
Lipu Fei 2018-10-01 17:07:57 +02:00 committed by GitHub
commit d81cdee9b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
400 changed files with 89911 additions and 19211 deletions

2
.gitignore vendored
View file

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

75
Jenkinsfile vendored
View file

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

View file

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

View file

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

View file

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

View file

@ -13,6 +13,7 @@ from cura.Backups.BackupsManager import BackupsManager
# api = CuraAPI() # api = CuraAPI()
# api.backups.createBackup() # api.backups.createBackup()
# api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})`` # api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})``
class Backups: class Backups:
manager = BackupsManager() # Re-used instance of the backups manager. manager = BackupsManager() # Re-used instance of the backups manager.

View file

@ -0,0 +1,33 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from cura.CuraApplication import CuraApplication
## The Interface.Settings API provides a version-proof bridge between Cura's
# (currently) sidebar UI and plug-ins that hook into it.
#
# Usage:
# ``from cura.API import CuraAPI
# api = CuraAPI()
# api.interface.settings.getContextMenuItems()
# data = {
# "name": "My Plugin Action",
# "iconName": "my-plugin-icon",
# "actions": my_menu_actions,
# "menu_item": MyPluginAction(self)
# }
# api.interface.settings.addContextMenuItem(data)``
class Settings:
# Re-used instance of Cura:
application = CuraApplication.getInstance() # type: CuraApplication
## Add items to the sidebar context menu.
# \param menu_item dict containing the menu item to add.
def addContextMenuItem(self, menu_item: dict) -> None:
self.application.addSidebarCustomMenuItem(menu_item)
## Get all custom items currently added to the sidebar context menu.
# \return List containing all custom context menu items.
def getContextMenuItems(self) -> list:
return self.application.getSidebarCustomMenuItems()

View file

@ -0,0 +1,24 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.PluginRegistry import PluginRegistry
from cura.API.Interface.Settings import Settings
## The Interface class serves as a common root for the specific API
# methods for each interface element.
#
# Usage:
# ``from cura.API import CuraAPI
# api = CuraAPI()
# api.interface.settings.addContextMenuItem()
# api.interface.viewport.addOverlay() # Not implemented, just a hypothetical
# api.interface.toolbar.getToolButtonCount() # Not implemented, just a hypothetical
# # etc.``
class Interface:
# For now we use the same API version to be consistent.
VERSION = PluginRegistry.APIVersion
# API methods specific to the settings portion of the UI
settings = Settings()

View file

@ -2,6 +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 UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
from cura.API.Backups import Backups from cura.API.Backups import Backups
from cura.API.Interface import Interface
## The official Cura API that plug-ins can use to interact with Cura. ## The official Cura API that plug-ins can use to interact with Cura.
# #
@ -9,10 +10,14 @@ from cura.API.Backups import Backups
# this API provides a version-safe interface with proper deprecation warnings # this API provides a version-safe interface with proper deprecation warnings
# etc. Usage of any other methods than the ones provided in this API can cause # etc. Usage of any other methods than the ones provided in this API can cause
# plug-ins to be unstable. # plug-ins to be unstable.
class CuraAPI: class CuraAPI:
# For now we use the same API version to be consistent. # For now we use the same API version to be consistent.
VERSION = PluginRegistry.APIVersion VERSION = PluginRegistry.APIVersion
# Backups API. # Backups API
backups = Backups() backups = Backups()
# Interface API
interface = Interface()

View file

@ -3,6 +3,7 @@
from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from UM.Application import Application #To modify the maximum zoom level.
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Scene.Platform import Platform from UM.Scene.Platform import Platform
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
@ -27,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
@ -170,6 +171,12 @@ class BuildVolume(SceneNode):
if shape: if shape:
self._shape = shape self._shape = shape
## Get the length of the 3D diagonal through the build volume.
#
# This gives a sense of the scale of the build volume in general.
def getDiagonalSize(self) -> float:
return math.sqrt(self._width * self._width + self._height * self._height + self._depth * self._depth)
def getDisallowedAreas(self) -> List[Polygon]: def getDisallowedAreas(self) -> List[Polygon]:
return self._disallowed_areas return self._disallowed_areas
@ -235,6 +242,8 @@ class BuildVolume(SceneNode):
# Mark the node as outside build volume if the set extruder is disabled # Mark the node as outside build volume if the set extruder is disabled
extruder_position = node.callDecoration("getActiveExtruderPosition") extruder_position = node.callDecoration("getActiveExtruderPosition")
if extruder_position not in self._global_container_stack.extruders:
continue
if not self._global_container_stack.extruders[extruder_position].isEnabled: if not self._global_container_stack.extruders[extruder_position].isEnabled:
node.setOutsideBuildArea(True) node.setOutsideBuildArea(True)
continue continue
@ -470,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:
@ -519,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)
@ -527,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)
@ -552,6 +559,12 @@ class BuildVolume(SceneNode):
if self._engine_ready: if self._engine_ready:
self.rebuild() self.rebuild()
camera = Application.getInstance().getController().getCameraTool()
if camera:
diagonal = self.getDiagonalSize()
if diagonal > 1:
camera.setZoomRange(min = 0.1, max = diagonal * 5) #You can zoom out up to 5 times the diagonal. This gives some space around the volume.
def _onEngineCreated(self): def _onEngineCreated(self):
self._engine_ready = True self._engine_ready = True
self.rebuild() self.rebuild()

View file

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

View file

@ -50,7 +50,8 @@ class CuraActions(QObject):
scene = cura.CuraApplication.CuraApplication.getInstance().getController().getScene() scene = cura.CuraApplication.CuraApplication.getInstance().getController().getScene()
camera = scene.getActiveCamera() camera = scene.getActiveCamera()
if camera: if camera:
camera.setPosition(Vector(-80, 250, 700)) diagonal_size = cura.CuraApplication.CuraApplication.getInstance().getBuildVolume().getDiagonalSize()
camera.setPosition(Vector(-80, 250, 700) * diagonal_size / 375)
camera.setPerspective(True) camera.setPerspective(True)
camera.lookAt(Vector(0, 0, 0)) camera.lookAt(Vector(0, 0, 0))

View file

@ -1,11 +1,10 @@
# 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.
import copy
import os import os
import sys import sys
import time import time
from typing import cast, TYPE_CHECKING, Optional from typing import cast, TYPE_CHECKING
import numpy import numpy
@ -14,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
@ -68,9 +68,9 @@ from cura.Machines.Models.NozzleModel import NozzleModel
from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfilesDropDownMenuModel from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfilesDropDownMenuModel
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
from cura.Machines.Models.MaterialManagementModel import MaterialManagementModel from cura.Machines.Models.FavoriteMaterialsModel import FavoriteMaterialsModel
from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
from cura.Machines.Models.BrandMaterialsModel import BrandMaterialsModel from cura.Machines.Models.MaterialBrandsModel import MaterialBrandsModel
from cura.Machines.Models.QualityManagementModel import QualityManagementModel from cura.Machines.Models.QualityManagementModel import QualityManagementModel
from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
from cura.Machines.Models.MachineManagementModel import MachineManagementModel from cura.Machines.Models.MachineManagementModel import MachineManagementModel
@ -94,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
@ -104,6 +105,8 @@ from cura.Settings.UserChangesModel import UserChangesModel
from cura.Settings.ExtrudersModel import ExtrudersModel from cura.Settings.ExtrudersModel import ExtrudersModel
from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
from cura.Settings.ContainerManager import ContainerManager from cura.Settings.ContainerManager import ContainerManager
from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
import cura.Settings.cura_empty_instance_containers
from cura.ObjectsModel import ObjectsModel from cura.ObjectsModel import ObjectsModel
@ -111,17 +114,20 @@ 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")
try: try:
from cura.CuraVersion import CuraVersion, CuraBuildType, CuraDebugMode from cura.CuraVersion import CuraVersion, CuraBuildType, CuraDebugMode, CuraSDKVersion
except ImportError: except ImportError:
CuraVersion = "master" # [CodeStyle: Reflecting imported value] CuraVersion = "master" # [CodeStyle: Reflecting imported value]
CuraBuildType = "" CuraBuildType = ""
CuraDebugMode = False CuraDebugMode = False
CuraSDKVersion = ""
class CuraApplication(QtApplication): class CuraApplication(QtApplication):
@ -172,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
@ -213,7 +219,6 @@ class CuraApplication(QtApplication):
self._message_box_callback = None self._message_box_callback = None
self._message_box_callback_arguments = [] self._message_box_callback_arguments = []
self._preferred_mimetype = ""
self._i18n_catalog = None self._i18n_catalog = None
self._currently_loading_files = [] self._currently_loading_files = []
@ -226,6 +231,10 @@ class CuraApplication(QtApplication):
self._need_to_show_user_agreement = True self._need_to_show_user_agreement = True
self._sidebar_custom_menu_items = [] # type: list # Keeps list of custom menu items for the side bar
self._plugins_loaded = False
# Backups # Backups
self._auto_save = None self._auto_save = None
self._save_data_enabled = True self._save_data_enabled = True
@ -362,42 +371,23 @@ 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.
empty_container = self._container_registry.getEmptyInstanceContainer() self.empty_container = cura.Settings.cura_empty_instance_containers.empty_container # type: EmptyInstanceContainer
self.empty_container = empty_container
empty_definition_changes_container = copy.deepcopy(empty_container) self._container_registry.addContainer(
empty_definition_changes_container.setMetaDataEntry("id", "empty_definition_changes") cura.Settings.cura_empty_instance_containers.empty_definition_changes_container)
empty_definition_changes_container.setMetaDataEntry("type", "definition_changes") self.empty_definition_changes_container = cura.Settings.cura_empty_instance_containers.empty_definition_changes_container
self._container_registry.addContainer(empty_definition_changes_container)
self.empty_definition_changes_container = empty_definition_changes_container
empty_variant_container = copy.deepcopy(empty_container) self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_variant_container)
empty_variant_container.setMetaDataEntry("id", "empty_variant") self.empty_variant_container = cura.Settings.cura_empty_instance_containers.empty_variant_container
empty_variant_container.setMetaDataEntry("type", "variant")
self._container_registry.addContainer(empty_variant_container)
self.empty_variant_container = empty_variant_container
empty_material_container = copy.deepcopy(empty_container) self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_material_container)
empty_material_container.setMetaDataEntry("id", "empty_material") self.empty_material_container = cura.Settings.cura_empty_instance_containers.empty_material_container
empty_material_container.setMetaDataEntry("type", "material")
self._container_registry.addContainer(empty_material_container)
self.empty_material_container = empty_material_container
empty_quality_container = copy.deepcopy(empty_container) self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_quality_container)
empty_quality_container.setMetaDataEntry("id", "empty_quality") self.empty_quality_container = cura.Settings.cura_empty_instance_containers.empty_quality_container
empty_quality_container.setName("Not Supported")
empty_quality_container.setMetaDataEntry("quality_type", "not_supported")
empty_quality_container.setMetaDataEntry("type", "quality")
empty_quality_container.setMetaDataEntry("supported", False)
self._container_registry.addContainer(empty_quality_container)
self.empty_quality_container = empty_quality_container
empty_quality_changes_container = copy.deepcopy(empty_container) self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_quality_changes_container)
empty_quality_changes_container.setMetaDataEntry("id", "empty_quality_changes") self.empty_quality_changes_container = cura.Settings.cura_empty_instance_containers.empty_quality_changes_container
empty_quality_changes_container.setMetaDataEntry("type", "quality_changes")
empty_quality_changes_container.setMetaDataEntry("quality_type", "not_supported")
self._container_registry.addContainer(empty_quality_changes_container)
self.empty_quality_changes_container = empty_quality_changes_container
# Initializes the version upgrade manager with by providing the paths for each resource type and the latest # Initializes the version upgrade manager with by providing the paths for each resource type and the latest
# versions. # versions.
@ -442,6 +432,7 @@ class CuraApplication(QtApplication):
# Readers & Writers: # Readers & Writers:
"GCodeWriter", "GCodeWriter",
"STLReader", "STLReader",
"3MFWriter",
# Tools: # Tools:
"CameraTool", "CameraTool",
@ -494,7 +485,11 @@ class CuraApplication(QtApplication):
preferences.addPreference("view/filter_current_build_plate", False) preferences.addPreference("view/filter_current_build_plate", False)
preferences.addPreference("cura/sidebar_collapsed", False) preferences.addPreference("cura/sidebar_collapsed", False)
self._need_to_show_user_agreement = not self.getPreferences().getValue("general/accepted_user_agreement") preferences.addPreference("cura/favorite_materials", "")
preferences.addPreference("cura/expanded_brands", "")
preferences.addPreference("cura/expanded_types", "")
self._need_to_show_user_agreement = not preferences.getValue("general/accepted_user_agreement")
for key in [ for key in [
"dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin "dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin
@ -508,15 +503,13 @@ class CuraApplication(QtApplication):
self.applicationShuttingDown.connect(self.saveSettings) self.applicationShuttingDown.connect(self.saveSettings)
self.engineCreatedSignal.connect(self._onEngineCreated) self.engineCreatedSignal.connect(self._onEngineCreated)
self.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
self._onGlobalContainerChanged()
self.getCuraSceneController().setActiveBuildPlate(0) # Initialize self.getCuraSceneController().setActiveBuildPlate(0) # Initialize
CuraApplication.Created = True CuraApplication.Created = True
def _onEngineCreated(self): def _onEngineCreated(self):
self._qml_engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider()) self._qml_engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
self._qml_engine.addImageProvider("print_job_preview", PrintJobPreviewImageProvider.PrintJobPreviewImageProvider())
@pyqtProperty(bool) @pyqtProperty(bool)
def needToShowUserAgreement(self): def needToShowUserAgreement(self):
@ -625,7 +618,7 @@ class CuraApplication(QtApplication):
# Cura has multiple locations where instance containers need to be saved, so we need to handle this differently. # Cura has multiple locations where instance containers need to be saved, so we need to handle this differently.
def saveSettings(self): def saveSettings(self):
if not self.started or not self._save_data_enabled: if not self.started or not self._save_data_enabled:
# Do not do saving during application start or when data should not be safed on quit. # Do not do saving during application start or when data should not be saved on quit.
return return
ContainerRegistry.getInstance().saveDirtyContainers() ContainerRegistry.getInstance().saveDirtyContainers()
self.savePreferences() self.savePreferences()
@ -774,7 +767,10 @@ class CuraApplication(QtApplication):
# Initialize camera # Initialize camera
root = controller.getScene().getRoot() root = controller.getScene().getRoot()
camera = Camera("3d", root) camera = Camera("3d", root)
camera.setPosition(Vector(-80, 250, 700)) diagonal = self.getBuildVolume().getDiagonalSize()
if diagonal < 1: #No printer added yet. Set a default camera distance for normal-sized printers.
diagonal = 375
camera.setPosition(Vector(-80, 250, 700) * diagonal / 375)
camera.setPerspective(True) camera.setPerspective(True)
camera.lookAt(Vector(0, 0, 0)) camera.lookAt(Vector(0, 0, 0))
controller.getScene().setActiveCamera("3d") controller.getScene().setActiveCamera("3d")
@ -816,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):
@ -838,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
@ -908,6 +904,7 @@ class CuraApplication(QtApplication):
engine.rootContext().setContextProperty("CuraApplication", self) engine.rootContext().setContextProperty("CuraApplication", self)
engine.rootContext().setContextProperty("PrintInformation", self._print_information) engine.rootContext().setContextProperty("PrintInformation", self._print_information)
engine.rootContext().setContextProperty("CuraActions", self._cura_actions) engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
engine.rootContext().setContextProperty("CuraSDKVersion", CuraSDKVersion)
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type") qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
@ -924,9 +921,9 @@ class CuraApplication(QtApplication):
qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer") qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer")
qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel") qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel")
qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel") qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel")
qmlRegisterType(BrandMaterialsModel, "Cura", 1, 0, "BrandMaterialsModel") qmlRegisterType(MaterialBrandsModel, "Cura", 1, 0, "MaterialBrandsModel")
qmlRegisterType(MaterialManagementModel, "Cura", 1, 0, "MaterialManagementModel")
qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel") qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel")
qmlRegisterType(MachineManagementModel, "Cura", 1, 0, "MachineManagementModel") qmlRegisterType(MachineManagementModel, "Cura", 1, 0, "MachineManagementModel")
@ -942,6 +939,7 @@ class CuraApplication(QtApplication):
qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator") qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel") qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel")
qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance) qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance)
qmlRegisterType(SidebarCustomMenuItemsModel, "Cura", 1, 0, "SidebarCustomMenuItemsModel")
# As of Qt5.7, it is necessary to get rid of any ".." in the path for the singleton to work. # As of Qt5.7, it is necessary to get rid of any ".." in the path for the singleton to work.
actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml"))) actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")))
@ -989,30 +987,14 @@ class CuraApplication(QtApplication):
self._camera_animation.setTarget(Selection.getSelectedObject(0).getWorldPosition()) self._camera_animation.setTarget(Selection.getSelectedObject(0).getWorldPosition())
self._camera_animation.start() self._camera_animation.start()
def _onGlobalContainerChanged(self):
if self._global_container_stack is not None:
machine_file_formats = [file_type.strip() for file_type in self._global_container_stack.getMetaDataEntry("file_formats").split(";")]
new_preferred_mimetype = ""
if machine_file_formats:
new_preferred_mimetype = machine_file_formats[0]
if new_preferred_mimetype != self._preferred_mimetype:
self._preferred_mimetype = new_preferred_mimetype
self.preferredOutputMimetypeChanged.emit()
requestAddPrinter = pyqtSignal() requestAddPrinter = pyqtSignal()
activityChanged = pyqtSignal() activityChanged = pyqtSignal()
sceneBoundingBoxChanged = pyqtSignal() sceneBoundingBoxChanged = pyqtSignal()
preferredOutputMimetypeChanged = pyqtSignal()
@pyqtProperty(bool, notify = activityChanged) @pyqtProperty(bool, notify = activityChanged)
def platformActivity(self): def platformActivity(self):
return self._platform_activity return self._platform_activity
@pyqtProperty(str, notify=preferredOutputMimetypeChanged)
def preferredOutputMimetype(self):
return self._preferred_mimetype
@pyqtProperty(str, notify = sceneBoundingBoxChanged) @pyqtProperty(str, notify = sceneBoundingBoxChanged)
def getSceneBoundingBoxString(self): def getSceneBoundingBoxString(self):
return self._i18n_catalog.i18nc("@info 'width', 'depth' and 'height' are variable names that must NOT be translated; just translate the format of ##x##x## mm.", "%(width).1f x %(depth).1f x %(height).1f mm") % {'width' : self._scene_bounding_box.width.item(), 'depth': self._scene_bounding_box.depth.item(), 'height' : self._scene_bounding_box.height.item()} return self._i18n_catalog.i18nc("@info 'width', 'depth' and 'height' are variable names that must NOT be translated; just translate the format of ##x##x## mm.", "%(width).1f x %(depth).1f x %(height).1f mm") % {'width' : self._scene_bounding_box.width.item(), 'depth': self._scene_bounding_box.depth.item(), 'height' : self._scene_bounding_box.height.item()}
@ -1728,4 +1710,15 @@ 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:
self._sidebar_custom_menu_items.append(menu_item)
def getSidebarCustomMenuItems(self) -> list:
return self._sidebar_custom_menu_items

View file

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

View file

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

View file

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

View file

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

View file

@ -4,8 +4,7 @@
from collections import defaultdict, OrderedDict from collections import defaultdict, OrderedDict
import copy import copy
import uuid import uuid
from typing import Dict, cast from typing import Dict, Optional, TYPE_CHECKING, Any, Set, List, cast, Tuple
from typing import Optional, TYPE_CHECKING
from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot
@ -18,10 +17,10 @@ from UM.Util import parseBool
from .MaterialNode import MaterialNode from .MaterialNode import MaterialNode
from .MaterialGroup import MaterialGroup from .MaterialGroup import MaterialGroup
from .VariantType import VariantType
if TYPE_CHECKING: if TYPE_CHECKING:
from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Settings.InstanceContainer import InstanceContainer
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from cura.Settings.ExtruderStack import ExtruderStack from cura.Settings.ExtruderStack import ExtruderStack
@ -39,24 +38,34 @@ if TYPE_CHECKING:
class MaterialManager(QObject): class MaterialManager(QObject):
materialsUpdated = pyqtSignal() # Emitted whenever the material lookup tables are updated. materialsUpdated = pyqtSignal() # Emitted whenever the material lookup tables are updated.
favoritesUpdated = pyqtSignal() # Emitted whenever the favorites are changed
def __init__(self, container_registry, parent = None): def __init__(self, container_registry, parent = None):
super().__init__(parent) super().__init__(parent)
self._application = Application.getInstance() self._application = Application.getInstance()
self._container_registry = container_registry # type: ContainerRegistry self._container_registry = container_registry # type: ContainerRegistry
self._fallback_materials_map = dict() # material_type -> generic material metadata # Material_type -> generic material metadata
self._material_group_map = dict() # root_material_id -> MaterialGroup self._fallback_materials_map = dict() # type: Dict[str, Dict[str, Any]]
self._diameter_machine_variant_material_map = dict() # approximate diameter str -> dict(machine_definition_id -> MaterialNode)
# Root_material_id -> MaterialGroup
self._material_group_map = dict() # type: Dict[str, MaterialGroup]
# Approximate diameter str
self._diameter_machine_nozzle_buildplate_material_map = dict() # type: Dict[str, Dict[str, MaterialNode]]
# We're using these two maps to convert between the specific diameter material id and the generic material id # We're using these two maps to convert between the specific diameter material id and the generic material id
# because the generic material ids are used in qualities and definitions, while the specific diameter material is meant # because the generic material ids are used in qualities and definitions, while the specific diameter material is meant
# i.e. generic_pla -> generic_pla_175 # i.e. generic_pla -> generic_pla_175
self._material_diameter_map = defaultdict(dict) # root_material_id -> approximate diameter str -> root_material_id for that diameter # root_material_id -> approximate diameter str -> root_material_id for that diameter
self._diameter_material_map = dict() # material id including diameter (generic_pla_175) -> material root id (generic_pla) self._material_diameter_map = defaultdict(dict) # type: Dict[str, Dict[str, str]]
# Material id including diameter (generic_pla_175) -> material root id (generic_pla)
self._diameter_material_map = dict() # type: Dict[str, str]
# This is used in Legacy UM3 send material function and the material management page. # This is used in Legacy UM3 send material function and the material management page.
self._guid_material_groups_map = defaultdict(list) # GUID -> a list of material_groups # GUID -> a list of material_groups
self._guid_material_groups_map = defaultdict(list) # type: Dict[str, List[MaterialGroup]]
# The machine definition ID for the non-machine-specific materials. # The machine definition ID for the non-machine-specific materials.
# This is used as the last fallback option if the given machine-specific material(s) cannot be found. # This is used as the last fallback option if the given machine-specific material(s) cannot be found.
@ -75,13 +84,15 @@ class MaterialManager(QObject):
self._container_registry.containerAdded.connect(self._onContainerMetadataChanged) self._container_registry.containerAdded.connect(self._onContainerMetadataChanged)
self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged) self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged)
def initialize(self): self._favorites = set() # type: Set[str]
def initialize(self) -> None:
# Find all materials and put them in a matrix for quick search. # Find all materials and put them in a matrix for quick search.
material_metadatas = {metadata["id"]: metadata for metadata in material_metadatas = {metadata["id"]: metadata for metadata in
self._container_registry.findContainersMetadata(type = "material") if self._container_registry.findContainersMetadata(type = "material") if
metadata.get("GUID")} metadata.get("GUID")} # type: Dict[str, Dict[str, Any]]
self._material_group_map = dict() self._material_group_map = dict() # type: Dict[str, MaterialGroup]
# Map #1 # Map #1
# root_material_id -> MaterialGroup # root_material_id -> MaterialGroup
@ -90,7 +101,7 @@ class MaterialManager(QObject):
if material_id == "empty_material": if material_id == "empty_material":
continue continue
root_material_id = material_metadata.get("base_file") root_material_id = material_metadata.get("base_file", "")
if root_material_id not in self._material_group_map: if root_material_id not in self._material_group_map:
self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id])) self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id]))
self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id) self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id)
@ -106,26 +117,26 @@ class MaterialManager(QObject):
# Map #1.5 # Map #1.5
# GUID -> material group list # GUID -> material group list
self._guid_material_groups_map = defaultdict(list) self._guid_material_groups_map = defaultdict(list) # type: Dict[str, List[MaterialGroup]]
for root_material_id, material_group in self._material_group_map.items(): for root_material_id, material_group in self._material_group_map.items():
guid = material_group.root_material_node.metadata["GUID"] guid = material_group.root_material_node.getMetaDataEntry("GUID", "")
self._guid_material_groups_map[guid].append(material_group) self._guid_material_groups_map[guid].append(material_group)
# Map #2 # Map #2
# Lookup table for material type -> fallback material metadata, only for read-only materials # Lookup table for material type -> fallback material metadata, only for read-only materials
grouped_by_type_dict = dict() grouped_by_type_dict = dict() # type: Dict[str, Any]
material_types_without_fallback = set() material_types_without_fallback = set()
for root_material_id, material_node in self._material_group_map.items(): for root_material_id, material_node in self._material_group_map.items():
material_type = material_node.root_material_node.metadata["material"] material_type = material_node.root_material_node.getMetaDataEntry("material", "")
if material_type not in grouped_by_type_dict: if material_type not in grouped_by_type_dict:
grouped_by_type_dict[material_type] = {"generic": None, grouped_by_type_dict[material_type] = {"generic": None,
"others": []} "others": []}
material_types_without_fallback.add(material_type) material_types_without_fallback.add(material_type)
brand = material_node.root_material_node.metadata["brand"] brand = material_node.root_material_node.getMetaDataEntry("brand", "")
if brand.lower() == "generic": if brand.lower() == "generic":
to_add = True to_add = True
if material_type in grouped_by_type_dict: if material_type in grouped_by_type_dict:
diameter = material_node.root_material_node.metadata.get("approximate_diameter") diameter = material_node.root_material_node.getMetaDataEntry("approximate_diameter", "")
if diameter != self._default_approximate_diameter_for_quality_search: if diameter != self._default_approximate_diameter_for_quality_search:
to_add = False # don't add if it's not the default diameter to_add = False # don't add if it's not the default diameter
@ -134,7 +145,7 @@ class MaterialManager(QObject):
# - if it's in the list, it means that is a new material without fallback # - if it's in the list, it means that is a new material without fallback
# - if it is not, then it is a custom material with a fallback material (parent) # - if it is not, then it is a custom material with a fallback material (parent)
if material_type in material_types_without_fallback: if material_type in material_types_without_fallback:
grouped_by_type_dict[material_type] = material_node.root_material_node.metadata grouped_by_type_dict[material_type] = material_node.root_material_node._metadata
material_types_without_fallback.remove(material_type) material_types_without_fallback.remove(material_type)
# Remove the materials that have no fallback materials # Remove the materials that have no fallback materials
@ -151,15 +162,15 @@ class MaterialManager(QObject):
self._diameter_material_map = dict() self._diameter_material_map = dict()
# Group the material IDs by the same name, material, brand, and color but with different diameters. # Group the material IDs by the same name, material, brand, and color but with different diameters.
material_group_dict = dict() material_group_dict = dict() # type: Dict[Tuple[Any], Dict[str, str]]
keys_to_fetch = ("name", "material", "brand", "color") keys_to_fetch = ("name", "material", "brand", "color")
for root_material_id, machine_node in self._material_group_map.items(): for root_material_id, machine_node in self._material_group_map.items():
root_material_metadata = machine_node.root_material_node.metadata root_material_metadata = machine_node.root_material_node._metadata
key_data = [] key_data_list = [] # type: List[Any]
for key in keys_to_fetch: for key in keys_to_fetch:
key_data.append(root_material_metadata.get(key)) key_data_list.append(machine_node.root_material_node.getMetaDataEntry(key))
key_data = tuple(key_data) key_data = cast(Tuple[Any], tuple(key_data_list)) # type: Tuple[Any]
# If the key_data doesn't exist, it doesn't matter if the material is read only... # If the key_data doesn't exist, it doesn't matter if the material is read only...
if key_data not in material_group_dict: if key_data not in material_group_dict:
@ -168,8 +179,8 @@ class MaterialManager(QObject):
# ...but if key_data exists, we just overwrite it if the material is read only, otherwise we skip it # ...but if key_data exists, we just overwrite it if the material is read only, otherwise we skip it
if not machine_node.is_read_only: if not machine_node.is_read_only:
continue continue
approximate_diameter = root_material_metadata.get("approximate_diameter") approximate_diameter = machine_node.root_material_node.getMetaDataEntry("approximate_diameter", "")
material_group_dict[key_data][approximate_diameter] = root_material_metadata["id"] material_group_dict[key_data][approximate_diameter] = machine_node.root_material_node.getMetaDataEntry("id", "")
# Map [root_material_id][diameter] -> root_material_id for this diameter # Map [root_material_id][diameter] -> root_material_id for this diameter
for data_dict in material_group_dict.values(): for data_dict in material_group_dict.values():
@ -187,52 +198,82 @@ class MaterialManager(QObject):
self._diameter_material_map[root_material_id] = default_root_material_id self._diameter_material_map[root_material_id] = default_root_material_id
# Map #4 # Map #4
# "machine" -> "variant_name" -> "root material ID" -> specific material InstanceContainer # "machine" -> "nozzle name" -> "buildplate name" -> "root material ID" -> specific material InstanceContainer
# Construct the "machine" -> "variant" -> "root material ID" -> specific material InstanceContainer self._diameter_machine_nozzle_buildplate_material_map = dict() # type: Dict[str, Dict[str, MaterialNode]]
self._diameter_machine_variant_material_map = dict()
for material_metadata in material_metadatas.values(): for material_metadata in material_metadatas.values():
# We don't store empty material in the lookup tables self.__addMaterialMetadataIntoLookupTree(material_metadata)
if material_metadata["id"] == "empty_material":
continue
root_material_id = material_metadata["base_file"] favorites = self._application.getPreferences().getValue("cura/favorite_materials")
definition = material_metadata["definition"] for item in favorites.split(";"):
approximate_diameter = material_metadata["approximate_diameter"] self._favorites.add(item)
if approximate_diameter not in self._diameter_machine_variant_material_map:
self._diameter_machine_variant_material_map[approximate_diameter] = {}
machine_variant_material_map = self._diameter_machine_variant_material_map[approximate_diameter]
if definition not in machine_variant_material_map:
machine_variant_material_map[definition] = MaterialNode()
machine_node = machine_variant_material_map[definition]
variant_name = material_metadata.get("variant_name")
if not variant_name:
# if there is no variant, this material is for the machine, so put its metadata in the machine node.
machine_node.material_map[root_material_id] = MaterialNode(material_metadata)
else:
# this material is variant-specific, so we save it in a variant-specific node under the
# machine-specific node
# Check first if the variant exist in the manager
existing_variant = self._application.getVariantManager().getVariantNode(definition, variant_name)
if existing_variant is not None:
if variant_name not in machine_node.children_map:
machine_node.children_map[variant_name] = MaterialNode()
variant_node = machine_node.children_map[variant_name]
if root_material_id in variant_node.material_map: # We shouldn't have duplicated variant-specific materials for the same machine.
ConfigurationErrorMessage.getInstance().addFaultyContainers(root_material_id)
continue
variant_node.material_map[root_material_id] = MaterialNode(material_metadata)
else:
# Add this container id to the wrong containers list in the registry
Logger.log("w", "Not adding {id} to the material manager because the variant does not exist.".format(id = material_metadata["id"]))
self._container_registry.addWrongContainerId(material_metadata["id"])
self.materialsUpdated.emit() self.materialsUpdated.emit()
def __addMaterialMetadataIntoLookupTree(self, material_metadata: Dict[str, Any]) -> None:
material_id = material_metadata["id"]
# We don't store empty material in the lookup tables
if material_id == "empty_material":
return
root_material_id = material_metadata["base_file"]
definition = material_metadata["definition"]
approximate_diameter = material_metadata["approximate_diameter"]
if approximate_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
self._diameter_machine_nozzle_buildplate_material_map[approximate_diameter] = {}
machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[
approximate_diameter]
if definition not in machine_nozzle_buildplate_material_map:
machine_nozzle_buildplate_material_map[definition] = MaterialNode()
# This is a list of information regarding the intermediate nodes:
# nozzle -> buildplate
nozzle_name = material_metadata.get("variant_name")
buildplate_name = material_metadata.get("buildplate_name")
intermediate_node_info_list = [(nozzle_name, VariantType.NOZZLE),
(buildplate_name, VariantType.BUILD_PLATE),
]
variant_manager = self._application.getVariantManager()
machine_node = machine_nozzle_buildplate_material_map[definition]
current_node = machine_node
current_intermediate_node_info_idx = 0
error_message = None # type: Optional[str]
while current_intermediate_node_info_idx < len(intermediate_node_info_list):
variant_name, variant_type = intermediate_node_info_list[current_intermediate_node_info_idx]
if variant_name is not None:
# The new material has a specific variant, so it needs to be added to that specific branch in the tree.
variant = variant_manager.getVariantNode(definition, variant_name, variant_type)
if variant is None:
error_message = "Material {id} contains a variant {name} that does not exist.".format(
id = material_metadata["id"], name = variant_name)
break
# Update the current node to advance to a more specific branch
if variant_name not in current_node.children_map:
current_node.children_map[variant_name] = MaterialNode()
current_node = current_node.children_map[variant_name]
current_intermediate_node_info_idx += 1
if error_message is not None:
Logger.log("e", "%s It will not be added into the material lookup tree.", error_message)
self._container_registry.addWrongContainerId(material_metadata["id"])
return
# Add the material to the current tree node, which is the deepest (the most specific) branch we can find.
# Sanity check: Make sure that there is no duplicated materials.
if root_material_id in current_node.material_map:
Logger.log("e", "Duplicated material [%s] with root ID [%s]. It has already been added.",
material_id, root_material_id)
ConfigurationErrorMessage.getInstance().addFaultyContainers(root_material_id)
return
current_node.material_map[root_material_id] = MaterialNode(material_metadata)
def _updateMaps(self): def _updateMaps(self):
Logger.log("i", "Updating material lookup data ...") Logger.log("i", "Updating material lookup data ...")
self.initialize() self.initialize()
@ -255,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)
@ -263,45 +304,55 @@ class MaterialManager(QObject):
# #
# Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup. # Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup.
# #
def getAvailableMaterials(self, machine_definition: "DefinitionContainer", extruder_variant_name: Optional[str], def getAvailableMaterials(self, machine_definition: "DefinitionContainer", nozzle_name: Optional[str],
diameter: float) -> Dict[str, MaterialNode]: buildplate_name: Optional[str], diameter: float) -> Dict[str, 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_variant_material_map: if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s])", diameter, rounded_diameter) Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s])", diameter, rounded_diameter)
return dict() return dict()
machine_definition_id = machine_definition.getId() machine_definition_id = machine_definition.getId()
# If there are variant materials, get the variant material # If there are nozzle-and-or-buildplate materials, get the nozzle-and-or-buildplate material
machine_variant_material_map = self._diameter_machine_variant_material_map[rounded_diameter] machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter]
machine_node = machine_variant_material_map.get(machine_definition_id) machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id)
default_machine_node = machine_variant_material_map.get(self._default_machine_definition_id) default_machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)
variant_node = None nozzle_node = None
if extruder_variant_name is not None and machine_node is not None: buildplate_node = None
variant_node = machine_node.getChildNode(extruder_variant_name) if nozzle_name is not None and machine_node is not None:
nozzle_node = machine_node.getChildNode(nozzle_name)
# Get buildplate node if possible
if nozzle_node is not None and buildplate_name is not None:
buildplate_node = nozzle_node.getChildNode(buildplate_name)
nodes_to_check = [variant_node, machine_node, default_machine_node] nodes_to_check = [buildplate_node, nozzle_node, machine_node, default_machine_node]
# Fallback mechanism of finding materials: # Fallback mechanism of finding materials:
# 1. variant-specific material # 1. buildplate-specific material
# 2. machine-specific material # 2. nozzle-specific material
# 3. generic material (for fdmprinter) # 3. machine-specific material
# 4. generic material (for fdmprinter)
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]
for node in nodes_to_check: excluded_materials = set()
if node is not None: for current_node in nodes_to_check:
# Only exclude the materials that are explicitly specified in the "exclude_materials" field. if current_node is None:
# Do not exclude other materials that are of the same type. continue
for material_id, node in node.material_map.items():
if material_id in machine_exclude_materials:
Logger.log("d", "Exclude material [%s] for machine [%s]",
material_id, machine_definition.getId())
continue
if material_id not in material_id_metadata_dict: # Only exclude the materials that are explicitly specified in the "exclude_materials" field.
material_id_metadata_dict[material_id] = node # Do not exclude other materials that are of the same type.
for material_id, node in current_node.material_map.items():
if material_id in machine_exclude_materials:
excluded_materials.add(material_id)
continue
if material_id not in material_id_metadata_dict:
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
@ -309,14 +360,15 @@ class MaterialManager(QObject):
# 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]]:
variant_name = None buildplate_name = machine.getBuildplateName()
nozzle_name = None
if extruder_stack.variant.getId() != "empty_variant": if extruder_stack.variant.getId() != "empty_variant":
variant_name = extruder_stack.variant.getName() nozzle_name = extruder_stack.variant.getName()
diameter = extruder_stack.approximateMaterialDiameter diameter = extruder_stack.approximateMaterialDiameter
# Fetch the available materials (ContainerNode) for the current active machine and extruder setup. # Fetch the available materials (ContainerNode) for the current active machine and extruder setup.
return self.getAvailableMaterials(machine.definition, variant_name, diameter) return self.getAvailableMaterials(machine.definition, nozzle_name, buildplate_name, diameter)
# #
# Gets MaterialNode for the given extruder and machine with the given material name. # Gets MaterialNode for the given extruder and machine with the given material name.
@ -324,32 +376,36 @@ class MaterialManager(QObject):
# 1. the given machine doesn't have materials; # 1. the given machine doesn't have materials;
# 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, extruder_variant_name: Optional[str], def getMaterialNode(self, machine_definition_id: str, nozzle_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_variant_material_map: if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s]) for root material id [%s]", Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s]) for root material id [%s]",
diameter, rounded_diameter, root_material_id) diameter, rounded_diameter, root_material_id)
return None return None
# If there are variant materials, get the variant material # If there are nozzle materials, get the nozzle-specific material
machine_variant_material_map = self._diameter_machine_variant_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_variant_material_map.get(machine_definition_id) machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id)
variant_node = None nozzle_node = None
buildplate_node = None
# Fallback for "fdmprinter" if the machine-specific materials cannot be found # Fallback for "fdmprinter" if the machine-specific materials cannot be found
if machine_node is None: if machine_node is None:
machine_node = machine_variant_material_map.get(self._default_machine_definition_id) machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)
if machine_node is not None and extruder_variant_name is not None: if machine_node is not None and nozzle_name is not None:
variant_node = machine_node.getChildNode(extruder_variant_name) nozzle_node = machine_node.getChildNode(nozzle_name)
if nozzle_node is not None and buildplate_name is not None:
buildplate_node = nozzle_node.getChildNode(buildplate_name)
# Fallback mechanism of finding materials: # Fallback mechanism of finding materials:
# 1. variant-specific material # 1. buildplate-specific material
# 2. machine-specific material # 2. nozzle-specific material
# 3. generic material (for fdmprinter) # 3. machine-specific material
nodes_to_check = [variant_node, machine_node, # 4. generic material (for fdmprinter)
machine_variant_material_map.get(self._default_machine_definition_id)] nodes_to_check = [buildplate_node, nozzle_node, machine_node,
machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)]
material_node = None material_node = None
for node in nodes_to_check: for node in nodes_to_check:
@ -366,7 +422,8 @@ class MaterialManager(QObject):
# 1. the given machine doesn't have materials; # 1. the given machine doesn't have materials;
# 2. cannot find any material InstanceContainers with the given settings. # 2. cannot find any material InstanceContainers with the given settings.
# #
def getMaterialNodeByType(self, global_stack: "GlobalStack", position: str, extruder_variant_name: str, material_guid: str) -> Optional["MaterialNode"]: def getMaterialNodeByType(self, global_stack: "GlobalStack", position: str, nozzle_name: str,
buildplate_name: Optional[str], material_guid: str) -> Optional["MaterialNode"]:
node = None node = None
machine_definition = global_stack.definition machine_definition = global_stack.definition
extruder_definition = global_stack.extruders[position].definition extruder_definition = global_stack.extruders[position].definition
@ -378,14 +435,14 @@ 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:
Logger.log("i", "Cannot find materials with guid [%s] ", material_guid) Logger.log("i", "Cannot find materials with guid [%s] ", material_guid)
return None return None
node = self.getMaterialNode(machine_definition.getId(), extruder_variant_name, node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name,
material_diameter, root_material_id) material_diameter, root_material_id)
return node return node
@ -413,13 +470,17 @@ class MaterialManager(QObject):
else: else:
return None return None
## Get default material for given global stack, extruder position and extruder variant name ## Get default material for given global stack, extruder position and extruder nozzle name
# you can provide the extruder_definition and then the position is ignored (useful when building up global stack in CuraStackBuilder) # you can provide the extruder_definition and then the position is ignored (useful when building up global stack in CuraStackBuilder)
def getDefaultMaterial(self, global_stack: "GlobalStack", position: str, extruder_variant_name: Optional[str], extruder_definition: Optional["DefinitionContainer"] = None) -> Optional["MaterialNode"]: def getDefaultMaterial(self, global_stack: "GlobalStack", position: str, nozzle_name: Optional[str],
extruder_definition: Optional["DefinitionContainer"] = None) -> Optional["MaterialNode"]:
node = None node = None
buildplate_name = global_stack.getBuildplateName()
machine_definition = global_stack.definition machine_definition = global_stack.definition
if extruder_definition is None: if extruder_definition is None:
extruder_definition = global_stack.extruders[position].definition extruder_definition = global_stack.extruders[position].definition
if extruder_definition and parseBool(global_stack.getMetaDataEntry("has_materials", False)): if extruder_definition and parseBool(global_stack.getMetaDataEntry("has_materials", False)):
# At this point the extruder_definition is not None # At this point the extruder_definition is not None
material_diameter = extruder_definition.getProperty("material_diameter", "value") material_diameter = extruder_definition.getProperty("material_diameter", "value")
@ -428,7 +489,7 @@ class MaterialManager(QObject):
approximate_material_diameter = str(round(material_diameter)) approximate_material_diameter = str(round(material_diameter))
root_material_id = machine_definition.getMetaDataEntry("preferred_material") root_material_id = machine_definition.getMetaDataEntry("preferred_material")
root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_material_diameter) root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_material_diameter)
node = self.getMaterialNode(machine_definition.getId(), extruder_variant_name, node = self.getMaterialNode(machine_definition.getId(), nozzle_name, buildplate_name,
material_diameter, root_material_id) material_diameter, root_material_id)
return node return node
@ -450,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
@ -468,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)
@ -478,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:
@ -515,8 +576,8 @@ class MaterialManager(QObject):
if container_to_copy.getMetaDataEntry("definition") != "fdmprinter": if container_to_copy.getMetaDataEntry("definition") != "fdmprinter":
new_id += "_" + container_to_copy.getMetaDataEntry("definition") new_id += "_" + container_to_copy.getMetaDataEntry("definition")
if container_to_copy.getMetaDataEntry("variant_name"): if container_to_copy.getMetaDataEntry("variant_name"):
variant_name = container_to_copy.getMetaDataEntry("variant_name") nozzle_name = container_to_copy.getMetaDataEntry("variant_name")
new_id += "_" + variant_name.replace(" ", "_") new_id += "_" + nozzle_name.replace(" ", "_")
new_container = copy.deepcopy(container_to_copy) new_container = copy.deepcopy(container_to_copy)
new_container.getMetaData()["id"] = new_id new_container.getMetaData()["id"] = new_id
@ -530,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
@ -565,3 +632,25 @@ class MaterialManager(QObject):
new_base_id = new_id, new_base_id = new_id,
new_metadata = new_metadata) new_metadata = new_metadata)
return new_id return new_id
@pyqtSlot(str)
def addFavorite(self, root_material_id: str) -> None:
self._favorites.add(root_material_id)
self.materialsUpdated.emit()
# Ensure all settings are saved.
self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites)))
self._application.saveSettings()
@pyqtSlot(str)
def removeFavorite(self, root_material_id: str) -> None:
self._favorites.remove(root_material_id)
self.materialsUpdated.emit()
# Ensure all settings are saved.
self._application.getPreferences().setValue("cura/favorite_materials", ";".join(list(self._favorites)))
self._application.saveSettings()
@pyqtSlot()
def getFavorites(self):
return self._favorites

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@ from UM.Logger import Logger
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
from UM.Util import parseBool from UM.Util import parseBool
from cura.Machines.VariantManager import VariantType from cura.Machines.VariantType import VariantType
class BuildPlateModel(ListModel): class BuildPlateModel(ListModel):

View file

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

View file

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

View file

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

View file

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

View file

@ -8,6 +8,8 @@ from UM.Logger import Logger
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
from UM.Util import parseBool from UM.Util import parseBool
from cura.Machines.VariantType import VariantType
class NozzleModel(ListModel): class NozzleModel(ListModel):
IdRole = Qt.UserRole + 1 IdRole = Qt.UserRole + 1
@ -43,7 +45,6 @@ class NozzleModel(ListModel):
self.setItems([]) self.setItems([])
return return
from cura.Machines.VariantManager import VariantType
variant_node_dict = self._variant_manager.getVariantNodes(global_stack, VariantType.NOZZLE) variant_node_dict = self._variant_manager.getVariantNodes(global_stack, VariantType.NOZZLE)
if not variant_node_dict: if not variant_node_dict:
self.setItems([]) self.setItems([])

View file

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

View file

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

View file

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

View file

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

View file

@ -1,7 +1,7 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING, Optional, cast from typing import TYPE_CHECKING, Optional, cast, Dict, List
from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot
@ -20,6 +20,8 @@ if TYPE_CHECKING:
from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.DefinitionContainer import DefinitionContainer
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from .QualityChangesGroup import QualityChangesGroup from .QualityChangesGroup import QualityChangesGroup
from cura.CuraApplication import CuraApplication
from UM.Settings.ContainerRegistry import ContainerRegistry
# #
@ -36,17 +38,20 @@ class QualityManager(QObject):
qualitiesUpdated = pyqtSignal() qualitiesUpdated = pyqtSignal()
def __init__(self, container_registry, parent = None): def __init__(self, container_registry: "ContainerRegistry", parent = None) -> None:
super().__init__(parent) super().__init__(parent)
self._application = Application.getInstance() self._application = Application.getInstance() # type: CuraApplication
self._material_manager = self._application.getMaterialManager() self._material_manager = self._application.getMaterialManager()
self._container_registry = container_registry self._container_registry = container_registry
self._empty_quality_container = self._application.empty_quality_container self._empty_quality_container = self._application.empty_quality_container
self._empty_quality_changes_container = self._application.empty_quality_changes_container self._empty_quality_changes_container = self._application.empty_quality_changes_container
self._machine_variant_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,12 +67,12 @@ 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> -> <variant> -> <material> # <machine> -> <nozzle> -> <buildplate> -> <material>
# -> <material> # <machine> -> <material>
self._machine_variant_material_quality_type_to_quality_dict = {} # for quality lookup self._machine_nozzle_buildplate_material_quality_type_to_quality_dict = {} # for quality lookup
self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup
quality_metadata_list = self._container_registry.findContainersMetadata(type = "quality") quality_metadata_list = self._container_registry.findContainersMetadata(type = "quality")
@ -79,53 +84,41 @@ class QualityManager(QObject):
quality_type = metadata["quality_type"] quality_type = metadata["quality_type"]
root_material_id = metadata.get("material") root_material_id = metadata.get("material")
variant_name = metadata.get("variant") nozzle_name = metadata.get("variant")
buildplate_name = metadata.get("buildplate")
is_global_quality = metadata.get("global_quality", False) is_global_quality = metadata.get("global_quality", False)
is_global_quality = is_global_quality or (root_material_id is None and variant_name is None) is_global_quality = is_global_quality or (root_material_id is None and nozzle_name is None and buildplate_name is None)
# Sanity check: material+variant and is_global_quality cannot be present at the same time # Sanity check: material+variant and is_global_quality cannot be present at the same time
if is_global_quality and (root_material_id or variant_name): if is_global_quality and (root_material_id or nozzle_name):
ConfigurationErrorMessage.getInstance().addFaultyContainers(metadata["id"]) ConfigurationErrorMessage.getInstance().addFaultyContainers(metadata["id"])
continue continue
if definition_id not in self._machine_variant_material_quality_type_to_quality_dict: if definition_id not in self._machine_nozzle_buildplate_material_quality_type_to_quality_dict:
self._machine_variant_material_quality_type_to_quality_dict[definition_id] = QualityNode() self._machine_nozzle_buildplate_material_quality_type_to_quality_dict[definition_id] = QualityNode()
machine_node = cast(QualityNode, self._machine_variant_material_quality_type_to_quality_dict[definition_id]) machine_node = cast(QualityNode, self._machine_nozzle_buildplate_material_quality_type_to_quality_dict[definition_id])
if is_global_quality: if is_global_quality:
# For global qualities, save data in the machine node # For global qualities, save data in the machine node
machine_node.addQualityMetadata(quality_type, metadata) machine_node.addQualityMetadata(quality_type, metadata)
continue continue
if variant_name is not None: current_node = machine_node
# If variant_name is specified in the quality/quality_changes profile, check if material is specified, intermediate_node_info_list = [nozzle_name, buildplate_name, root_material_id]
# too. current_intermediate_node_info_idx = 0
if variant_name not in machine_node.children_map:
machine_node.children_map[variant_name] = QualityNode()
variant_node = cast(QualityNode, machine_node.children_map[variant_name])
if root_material_id is None: while current_intermediate_node_info_idx < len(intermediate_node_info_list):
# If only variant_name is specified but material is not, add the quality/quality_changes metadata node_name = intermediate_node_info_list[current_intermediate_node_info_idx]
# into the current variant node. if node_name is not None:
variant_node.addQualityMetadata(quality_type, metadata) # There is specific information, update the current node to go deeper so we can add this quality
else: # at the most specific branch in the lookup tree.
# If only variant_name and material are both specified, go one level deeper: create a material node if node_name not in current_node.children_map:
# under the current variant node, and then add the quality/quality_changes metadata into the current_node.children_map[node_name] = QualityNode()
# material node. current_node = cast(QualityNode, current_node.children_map[node_name])
if root_material_id not in variant_node.children_map:
variant_node.children_map[root_material_id] = QualityNode()
material_node = cast(QualityNode, variant_node.children_map[root_material_id])
material_node.addQualityMetadata(quality_type, metadata) current_intermediate_node_info_idx += 1
else: current_node.addQualityMetadata(quality_type, metadata)
# If variant_name is not specified, check if material is specified.
if root_material_id is not None:
if root_material_id not in machine_node.children_map:
machine_node.children_map[root_material_id] = QualityNode()
material_node = cast(QualityNode, machine_node.children_map[root_material_id])
material_node.addQualityMetadata(quality_type, metadata)
# Initialize the lookup tree for quality_changes profiles with following structure: # Initialize the lookup tree for quality_changes profiles with following structure:
# <machine> -> <quality_type> -> <name> # <machine> -> <quality_type> -> <name>
@ -145,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
@ -160,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:
@ -208,46 +201,59 @@ 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_variant_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)
default_machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(self._default_machine_definition_id) # Check if this machine has specific quality profiles for its extruders, if so, when looking up extruder
nodes_to_check = [machine_node, default_machine_node] # 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)
nodes_to_check = [] # type: List[QualityNode]
if machine_node is not None:
nodes_to_check.append(machine_node)
if default_machine_node is not None:
nodes_to_check.append(default_machine_node)
# Iterate over all quality_types in the machine node # Iterate over all quality_types in the machine node
quality_group_dict = {} quality_group_dict = {}
for node in nodes_to_check: for node in nodes_to_check:
if node and node.quality_type_map: if node and node.quality_type_map:
# Only include global qualities quality_node = list(node.quality_type_map.values())[0]
if has_variant_materials: is_global_quality = parseBool(quality_node.getMetaDataEntry("global_quality", False))
quality_node = list(node.quality_type_map.values())[0] if not is_global_quality:
is_global_quality = parseBool(quality_node.metadata.get("global_quality", False)) continue
if not is_global_quality:
continue
for quality_type, quality_node in node.quality_type_map.items(): for quality_type, quality_node in node.quality_type_map.items():
quality_group = QualityGroup(quality_node.metadata["name"], quality_type) quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
quality_group.node_for_global = quality_node quality_group.node_for_global = quality_node
quality_group_dict[quality_type] = quality_group quality_group_dict[quality_type] = quality_group
break break
buildplate_name = machine.getBuildplateName()
# Iterate over all extruders to find quality containers for each extruder # Iterate over all extruders to find quality containers for each extruder
for position, extruder in machine.extruders.items(): for position, extruder in machine.extruders.items():
variant_name = None nozzle_name = None
if extruder.variant.getId() != "empty_variant": if extruder.variant.getId() != "empty_variant":
variant_name = extruder.variant.getName() nozzle_name = extruder.variant.getName()
# This is a list of root material IDs to use for searching for suitable quality profiles. # This is a list of root material IDs to use for searching for suitable quality profiles.
# The root material IDs in this list are in prioritized order. # The root material IDs in this list are in prioritized order.
root_material_id_list = [] root_material_id_list = []
has_material = False # flag indicating whether this extruder has a material assigned has_material = False # flag indicating whether this extruder has a material assigned
root_material_id = None
if extruder.material.getId() != "empty_material": if extruder.material.getId() != "empty_material":
has_material = True has_material = True
root_material_id = extruder.material.getMetaDataEntry("base_file") root_material_id = extruder.material.getMetaDataEntry("base_file")
@ -264,67 +270,92 @@ class QualityManager(QObject):
# Here we construct a list of nodes we want to look for qualities with the highest priority first. # Here we construct a list of nodes we want to look for qualities with the highest priority first.
# The use case is that, when we look for qualities for a machine, we first want to search in the following # The use case is that, when we look for qualities for a machine, we first want to search in the following
# order: # order:
# 1. machine-variant-and-material-specific qualities if exist # 1. machine-nozzle-buildplate-and-material-specific qualities if exist
# 2. machine-variant-specific qualities if exist # 2. machine-nozzle-and-material-specific qualities if exist
# 3. machine-material-specific qualities if exist # 3. machine-nozzle-specific qualities if exist
# 4. machine-specific qualities if exist # 4. machine-material-specific qualities if exist
# 5. generic qualities if exist # 5. machine-specific global qualities if exist, otherwise generic global qualities
# NOTE: We DO NOT fail back to generic global qualities if machine-specific global qualities exist.
# This is because when a machine defines its own global qualities such as Normal, Fine, etc.,
# it is intended to maintain those specific qualities ONLY. If we still fail back to the generic
# global qualities, there can be unimplemented quality types e.g. "coarse", and this is not
# correct.
# Each points above can be represented as a node in the lookup tree, so here we simply put those nodes into # Each points above can be represented as a node in the lookup tree, so here we simply put those nodes into
# the list with priorities as the order. Later, we just need to loop over each node in this list and fetch # the list with priorities as the order. Later, we just need to loop over each node in this list and fetch
# qualities from there. # qualities from there.
node_info_list_0 = [nozzle_name, buildplate_name, root_material_id] # type: List[Optional[str]]
nodes_to_check = [] nodes_to_check = []
if variant_name: # This function tries to recursively find the deepest (the most specific) branch and add those nodes to
# In this case, we have both a specific variant and a specific material # the search list in the order described above. So, by iterating over that search node list, we first look
variant_node = machine_node.getChildNode(variant_name) # in the more specific branches and then the less specific (generic) ones.
if variant_node and has_material: def addNodesToCheck(node: Optional[QualityNode], nodes_to_check_list: List[QualityNode], node_info_list, node_info_idx: int) -> None:
for root_material_id in root_material_id_list: if node is None:
material_node = variant_node.getChildNode(root_material_id) return
if node_info_idx < len(node_info_list):
node_name = node_info_list[node_info_idx]
if node_name is not None:
current_node = node.getChildNode(node_name)
if current_node is not None and has_material:
addNodesToCheck(current_node, nodes_to_check_list, node_info_list, node_info_idx + 1)
if has_material:
for rmid in root_material_id_list:
material_node = node.getChildNode(rmid)
if material_node: if material_node:
nodes_to_check.append(material_node) nodes_to_check_list.append(material_node)
break break
nodes_to_check.append(variant_node)
# In this case, we only have a specific material but NOT a variant nodes_to_check_list.append(node)
if has_material:
for root_material_id in root_material_id_list:
material_node = machine_node.getChildNode(root_material_id)
if material_node:
nodes_to_check.append(material_node)
break
nodes_to_check += [machine_node, default_machine_node] addNodesToCheck(machine_node, nodes_to_check, node_info_list_0, 0)
for node in nodes_to_check:
# 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.
if machine_node is not None and machine_node.quality_type_map:
nodes_to_check += [machine_node]
elif default_machine_node is not None:
nodes_to_check += [default_machine_node]
for node_idx, node in enumerate(nodes_to_check):
if node and node.quality_type_map: if node and node.quality_type_map:
if has_variant_materials: if has_extruder_specific_qualities:
# Only include variant qualities; skip non global qualities # Only include variant qualities; skip non global qualities
quality_node = list(node.quality_type_map.values())[0] quality_node = list(node.quality_type_map.values())[0]
is_global_quality = parseBool(quality_node.metadata.get("global_quality", False)) is_global_quality = parseBool(quality_node.getMetaDataEntry("global_quality", False))
if is_global_quality: if is_global_quality:
continue continue
for quality_type, quality_node in node.quality_type_map.items(): for quality_type, quality_node in node.quality_type_map.items():
if quality_type not in quality_group_dict: if quality_type not in quality_group_dict:
quality_group = QualityGroup(quality_node.metadata["name"], quality_type) quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
quality_group_dict[quality_type] = quality_group quality_group_dict[quality_type] = quality_group
quality_group = quality_group_dict[quality_type] quality_group = quality_group_dict[quality_type]
quality_group.nodes_for_extruders[position] = quality_node if position not in quality_group.nodes_for_extruders:
break 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:
# (1) the machine-specific node # (1) the machine-specific node
# (2) the generic node # (2) the generic node
machine_node = self._machine_variant_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)
default_machine_node = self._machine_variant_material_quality_type_to_quality_dict.get( default_machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(
self._default_machine_definition_id) self._default_machine_definition_id)
nodes_to_check = [machine_node, default_machine_node] nodes_to_check = [machine_node, default_machine_node]
@ -333,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
@ -355,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.
@ -387,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.")
@ -415,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

@ -1,9 +1,8 @@
# 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 enum import Enum
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
@ -11,20 +10,13 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Util import parseBool from UM.Util import parseBool
from cura.Machines.ContainerNode import ContainerNode from cura.Machines.ContainerNode import ContainerNode
from cura.Machines.VariantType import VariantType, ALL_VARIANT_TYPES
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
if TYPE_CHECKING: if TYPE_CHECKING:
from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.DefinitionContainer import DefinitionContainer
class VariantType(Enum):
BUILD_PLATE = "buildplate"
NOZZLE = "nozzle"
ALL_VARIANT_TYPES = (VariantType.BUILD_PLATE, VariantType.NOZZLE)
# #
# VariantManager is THE place to look for a specific variant. It maintains two variant lookup tables with the following # VariantManager is THE place to look for a specific variant. It maintains two variant lookup tables with the following
# structure: # structure:
@ -44,11 +36,11 @@ ALL_VARIANT_TYPES = (VariantType.BUILD_PLATE, VariantType.NOZZLE)
# #
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"]
@ -56,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()
@ -114,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,15 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from enum import Enum
class VariantType(Enum):
BUILD_PLATE = "buildplate"
NOZZLE = "nozzle"
ALL_VARIANT_TYPES = (VariantType.BUILD_PLATE, VariantType.NOZZLE)
__all__ = ["VariantType", "ALL_VARIANT_TYPES"]

View file

@ -9,6 +9,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection from UM.Scene.Selection import Selection
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from collections import defaultdict
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
@ -40,6 +41,8 @@ class ObjectsModel(ListModel):
filter_current_build_plate = Application.getInstance().getPreferences().getValue("view/filter_current_build_plate") filter_current_build_plate = Application.getInstance().getPreferences().getValue("view/filter_current_build_plate")
active_build_plate_number = self._build_plate_number active_build_plate_number = self._build_plate_number
group_nr = 1 group_nr = 1
name_count_dict = defaultdict(int)
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
if not isinstance(node, SceneNode): if not isinstance(node, SceneNode):
continue continue
@ -55,6 +58,7 @@ class ObjectsModel(ListModel):
if not node.callDecoration("isGroup"): if not node.callDecoration("isGroup"):
name = node.getName() name = node.getName()
else: else:
name = catalog.i18nc("@label", "Group #{group_nr}").format(group_nr = str(group_nr)) name = catalog.i18nc("@label", "Group #{group_nr}").format(group_nr = str(group_nr))
group_nr += 1 group_nr += 1
@ -63,6 +67,14 @@ class ObjectsModel(ListModel):
is_outside_build_area = node.isOutsideBuildArea() is_outside_build_area = node.isOutsideBuildArea()
else: else:
is_outside_build_area = False is_outside_build_area = False
#check if we already have an instance of the object based on name
name_count_dict[name] += 1
name_count = name_count_dict[name]
if name_count > 1:
name = "{0}({1})".format(name, name_count-1)
node.setName(name)
nodes.append({ nodes.append({
"name": name, "name": name,
@ -71,6 +83,7 @@ class ObjectsModel(ListModel):
"buildPlateNumber": node_build_plate_number, "buildPlateNumber": node_build_plate_number,
"node": node "node": node
}) })
nodes = sorted(nodes, key=lambda n: n["name"]) nodes = sorted(nodes, key=lambda n: n["name"])
self.setItems(nodes) self.setItems(nodes)

View file

@ -1,112 +1,149 @@
# Copyright (c) 2015 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 UM.Scene.Iterator import Iterator import sys
from UM.Scene.SceneNode import SceneNode
from functools import cmp_to_key from shapely import affinity
from UM.Application import Application from shapely.geometry import Polygon
from UM.Scene.Iterator.Iterator import Iterator
from UM.Scene.SceneNode import SceneNode
# Iterator that determines the object print order when one-at a time mode is enabled.
#
# In one-at-a-time mode, only one extruder can be enabled to print. In order to maximize the number of objects we can
# print, we need to print from the corner that's closest to the extruder that's being used. Here is an illustration:
#
# +--------------------------------+
# | |
# | |
# | | - Rectangle represents the complete print head including fans, etc.
# | X X | y - X's are the nozzles
# | (1) (2) | ^
# | | |
# +--------------------------------+ +--> x
#
# In this case, the nozzles are symmetric, nozzle (1) is closer to the bottom left corner while (2) is closer to the
# bottom right. If we use nozzle (1) to print, then we better off printing from the bottom left corner so the print
# head will not collide into an object on its top-right side, which is a very large unused area. Following the same
# logic, if we are printing with nozzle (2), then it's better to print from the bottom-right side.
#
# This iterator determines the print order following the rules above.
#
class OneAtATimeIterator(Iterator):
## Iterator that returns a list of nodes in the order that they need to be printed
# If there is no solution an empty list is returned.
# Take note that the list of nodes can have children (that may or may not contain mesh data)
class OneAtATimeIterator(Iterator.Iterator):
def __init__(self, scene_node): def __init__(self, scene_node):
super().__init__(scene_node) # Call super to make multiple inheritence work. from cura.CuraApplication import CuraApplication
self._hit_map = [[]] self._global_stack = CuraApplication.getInstance().getGlobalContainerStack()
self._original_node_list = [] self._original_node_list = []
super().__init__(scene_node) # Call super to make multiple inheritance work.
def getMachineNearestCornerToExtruder(self, global_stack):
head_and_fans_coordinates = global_stack.getHeadAndFansCoordinates()
used_extruder = None
for extruder in global_stack.extruders.values():
if extruder.isEnabled:
used_extruder = extruder
break
extruder_offsets = [used_extruder.getProperty("machine_nozzle_offset_x", "value"),
used_extruder.getProperty("machine_nozzle_offset_y", "value")]
# find the corner that's closest to the origin
min_distance2 = sys.maxsize
min_coord = None
for coord in head_and_fans_coordinates:
x = coord[0] - extruder_offsets[0]
y = coord[1] - extruder_offsets[1]
distance2 = x**2 + y**2
if distance2 <= min_distance2:
min_distance2 = distance2
min_coord = coord
return min_coord
def _checkForCollisions(self) -> bool:
all_nodes = []
for node in self._scene_node.getChildren():
if not issubclass(type(node), SceneNode):
continue
convex_hull = node.callDecoration("getConvexHullHead")
if not convex_hull:
continue
bounding_box = node.getBoundingBox()
if not bounding_box:
continue
from UM.Math.Polygon import Polygon
bounding_box_polygon = Polygon([[bounding_box.left, bounding_box.front],
[bounding_box.left, bounding_box.back],
[bounding_box.right, bounding_box.back],
[bounding_box.right, bounding_box.front]])
all_nodes.append({"node": node,
"bounding_box": bounding_box_polygon,
"convex_hull": convex_hull})
has_collisions = False
for i, node_dict in enumerate(all_nodes):
for j, other_node_dict in enumerate(all_nodes):
if i == j:
continue
if node_dict["bounding_box"].intersectsPolygon(other_node_dict["convex_hull"]):
has_collisions = True
break
if has_collisions:
break
return has_collisions
def _fillStack(self): def _fillStack(self):
min_coord = self.getMachineNearestCornerToExtruder(self._global_stack)
transform_x = -int(round(min_coord[0] / abs(min_coord[0])))
transform_y = -int(round(min_coord[1] / abs(min_coord[1])))
machine_size = [self._global_stack.getProperty("machine_width", "value"),
self._global_stack.getProperty("machine_depth", "value")]
def flip_x(polygon):
tm2 = [-1, 0, 0, 1, 0, 0]
return affinity.affine_transform(affinity.translate(polygon, xoff = -machine_size[0]), tm2)
def flip_y(polygon):
tm2 = [1, 0, 0, -1, 0, 0]
return affinity.affine_transform(affinity.translate(polygon, yoff = -machine_size[1]), tm2)
if self._checkForCollisions():
self._node_stack = []
return
node_list = [] node_list = []
for node in self._scene_node.getChildren(): for node in self._scene_node.getChildren():
if not issubclass(type(node), SceneNode): if not issubclass(type(node), SceneNode):
continue continue
if node.callDecoration("getConvexHull"): convex_hull = node.callDecoration("getConvexHull")
node_list.append(node) if convex_hull:
xmin = min(x for x, _ in convex_hull._points)
xmax = max(x for x, _ in convex_hull._points)
ymin = min(y for _, y in convex_hull._points)
ymax = max(y for _, y in convex_hull._points)
convex_hull_polygon = Polygon.from_bounds(xmin, ymin, xmax, ymax)
if transform_x < 0:
convex_hull_polygon = flip_x(convex_hull_polygon)
if transform_y < 0:
convex_hull_polygon = flip_y(convex_hull_polygon)
if len(node_list) < 2: node_list.append({"node": node,
self._node_stack = node_list[:] "min_coord": [convex_hull_polygon.bounds[0], convex_hull_polygon.bounds[1]],
return })
# Copy the list node_list = sorted(node_list, key = lambda d: d["min_coord"])
self._original_node_list = node_list[:]
## Initialise the hit map (pre-compute all hits between all objects)
self._hit_map = [[self._checkHit(i,j) for i in node_list] for j in node_list]
# Check if we have to files that block eachother. If this is the case, there is no solution!
for a in range(0,len(node_list)):
for b in range(0,len(node_list)):
if a != b and self._hit_map[a][b] and self._hit_map[b][a]:
return
# Sort the original list so that items that block the most other objects are at the beginning.
# This does not decrease the worst case running time, but should improve it in most cases.
sorted(node_list, key = cmp_to_key(self._calculateScore))
todo_node_list = [_ObjectOrder([], node_list)]
while len(todo_node_list) > 0:
current = todo_node_list.pop()
for node in current.todo:
# Check if the object can be placed with what we have and still allows for a solution in the future
if not self._checkHitMultiple(node, current.order) and not self._checkBlockMultiple(node, current.todo):
# We found a possible result. Create new todo & order list.
new_todo_list = current.todo[:]
new_todo_list.remove(node)
new_order = current.order[:] + [node]
if len(new_todo_list) == 0:
# We have no more nodes to check, so quit looking.
todo_node_list = None
self._node_stack = new_order
return
todo_node_list.append(_ObjectOrder(new_order, new_todo_list))
self._node_stack = [] #No result found!
# Check if first object can be printed before the provided list (using the hit map)
def _checkHitMultiple(self, node, other_nodes):
node_index = self._original_node_list.index(node)
for other_node in other_nodes:
other_node_index = self._original_node_list.index(other_node)
if self._hit_map[node_index][other_node_index]:
return True
return False
def _checkBlockMultiple(self, node, other_nodes):
node_index = self._original_node_list.index(node)
for other_node in other_nodes:
other_node_index = self._original_node_list.index(other_node)
if self._hit_map[other_node_index][node_index] and node_index != other_node_index:
return True
return False
## Calculate score simply sums the number of other objects it 'blocks'
def _calculateScore(self, a, b):
score_a = sum(self._hit_map[self._original_node_list.index(a)])
score_b = sum(self._hit_map[self._original_node_list.index(b)])
return score_a - score_b
# Checks if A can be printed before B
def _checkHit(self, a, b):
if a == b:
return False
overlap = a.callDecoration("getConvexHullBoundary").intersectsPolygon(b.callDecoration("getConvexHullHeadFull"))
if overlap:
return True
else:
return False
## Internal object used to keep track of a possible order in which to print objects.
class _ObjectOrder():
def __init__(self, order, todo):
"""
:param order: List of indexes in which to print objects, ordered by printing order.
:param todo: List of indexes which are not yet inserted into the order list.
"""
self.order = order
self.todo = todo
self._node_stack = [d["node"] for d in node_list]

View file

@ -10,7 +10,6 @@ from typing import Dict
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
from UM.i18n import i18nCatalog
from UM.Logger import Logger from UM.Logger import Logger
from UM.Qt.Duration import Duration from UM.Qt.Duration import Duration
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
@ -52,6 +51,8 @@ class PrintInformation(QObject):
super().__init__(parent) super().__init__(parent)
self._application = application self._application = application
self.UNTITLED_JOB_NAME = "Untitled"
self.initializeCuraMessagePrintTimeProperties() self.initializeCuraMessagePrintTimeProperties()
self._material_lengths = {} # indexed by build plate number self._material_lengths = {} # indexed by build plate number
@ -70,12 +71,13 @@ class PrintInformation(QObject):
self._base_name = "" self._base_name = ""
self._abbr_machine = "" self._abbr_machine = ""
self._job_name = "" self._job_name = ""
self._project_name = ""
self._active_build_plate = 0 self._active_build_plate = 0
self._initVariablesWithBuildPlate(self._active_build_plate) self._initVariablesWithBuildPlate(self._active_build_plate)
self._multi_build_plate_model = self._application.getMultiBuildPlateModel() self._multi_build_plate_model = self._application.getMultiBuildPlateModel()
ss = self._multi_build_plate_model.maxBuildPlate
self._application.globalContainerStackChanged.connect(self._updateJobName) self._application.globalContainerStackChanged.connect(self._updateJobName)
self._application.globalContainerStackChanged.connect(self.setToZeroPrintInformation) self._application.globalContainerStackChanged.connect(self.setToZeroPrintInformation)
self._application.fileLoaded.connect(self.setBaseName) self._application.fileLoaded.connect(self.setBaseName)
@ -300,13 +302,13 @@ class PrintInformation(QObject):
def _updateJobName(self): def _updateJobName(self):
if self._base_name == "": if self._base_name == "":
self._job_name = "unnamed" self._job_name = self.UNTITLED_JOB_NAME
self._is_user_specified_job_name = False self._is_user_specified_job_name = False
self.jobNameChanged.emit() self.jobNameChanged.emit()
return return
base_name = self._stripAccents(self._base_name) base_name = self._stripAccents(self._base_name)
self._setAbbreviatedMachineName() self._defineAbbreviatedMachineName()
# Only update the job name when it's not user-specified. # Only update the job name when it's not user-specified.
if not self._is_user_specified_job_name: if not self._is_user_specified_job_name:
@ -382,7 +384,7 @@ class PrintInformation(QObject):
## Created an acronym-like abbreviated machine name from the currently ## Created an acronym-like abbreviated machine name from the currently
# active machine name. # active machine name.
# Called each time the global stack is switched. # Called each time the global stack is switched.
def _setAbbreviatedMachineName(self): def _defineAbbreviatedMachineName(self):
global_container_stack = self._application.getGlobalContainerStack() global_container_stack = self._application.getGlobalContainerStack()
if not global_container_stack: if not global_container_stack:
self._abbr_machine = "" self._abbr_machine = ""

View file

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

View file

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

View file

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

View file

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

View file

@ -66,7 +66,7 @@ class GenericOutputController(PrinterOutputController):
self._output_device.sendCommand("G28 Z") self._output_device.sendCommand("G28 Z")
def sendRawCommand(self, printer: "PrinterOutputModel", command: str): def sendRawCommand(self, printer: "PrinterOutputModel", command: str):
self._output_device.sendCommand(command) self._output_device.sendCommand(command.upper()) #Most printers only understand uppercase g-code commands.
def setJobState(self, job: "PrintJobOutputModel", state: str): def setJobState(self, job: "PrintJobOutputModel", state: str):
if state == "pause": if state == "pause":

View file

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

View file

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

View file

@ -35,7 +35,7 @@ class PrinterOutputModel(QObject):
self._key = "" # Unique identifier self._key = "" # Unique identifier
self._controller = output_controller self._controller = output_controller
self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)] self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)]
self._printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer self._printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer
self._head_position = Vector(0, 0, 0) self._head_position = Vector(0, 0, 0)
self._active_print_job = None # type: Optional[PrintJobOutputModel] self._active_print_job = None # type: Optional[PrintJobOutputModel]
self._firmware_version = firmware_version self._firmware_version = firmware_version
@ -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
@ -120,7 +120,7 @@ class PrinterOutputModel(QObject):
@pyqtProperty(QVariant, notify = headPositionChanged) @pyqtProperty(QVariant, notify = headPositionChanged)
def headPosition(self): def headPosition(self):
return {"x": self._head_position.x, "y": self._head_position.y, "z": self.head_position_z} return {"x": self._head_position.x, "y": self._head_position.y, "z": self.head_position.z}
def updateHeadPosition(self, x, y, z): def updateHeadPosition(self, x, y, z):
if self._head_position.x != x or self._head_position.y != y or self._head_position.z != z: if self._head_position.x != x or self._head_position.y != y or self._head_position.z != z:
@ -282,8 +282,4 @@ class PrinterOutputModel(QObject):
def printerConfiguration(self): def printerConfiguration(self):
if self._printer_configuration.isValid(): if self._printer_configuration.isValid():
return self._printer_configuration return self._printer_configuration
return None return None
def _updateExtruderConfiguration(self):
self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in self._extruders]
self.configurationChanged.emit()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 Any, cast, List, Optional, Union from typing import Any, cast, List, Optional
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject
from UM.Application import Application from UM.Application import Application
@ -13,6 +13,7 @@ from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.Interfaces import ContainerInterface, DefinitionContainerInterface from UM.Settings.Interfaces import ContainerInterface, DefinitionContainerInterface
from cura.Settings import cura_empty_instance_containers
from . import Exceptions from . import Exceptions
@ -39,14 +40,12 @@ class CuraContainerStack(ContainerStack):
def __init__(self, container_id: str) -> None: def __init__(self, container_id: str) -> None:
super().__init__(container_id) super().__init__(container_id)
self._container_registry = ContainerRegistry.getInstance() #type: ContainerRegistry self._empty_instance_container = cura_empty_instance_containers.empty_container #type: InstanceContainer
self._empty_instance_container = self._container_registry.getEmptyInstanceContainer() #type: InstanceContainer self._empty_quality_changes = cura_empty_instance_containers.empty_quality_changes_container #type: InstanceContainer
self._empty_quality = cura_empty_instance_containers.empty_quality_container #type: InstanceContainer
self._empty_quality_changes = self._container_registry.findInstanceContainers(id = "empty_quality_changes")[0] #type: InstanceContainer self._empty_material = cura_empty_instance_containers.empty_material_container #type: InstanceContainer
self._empty_quality = self._container_registry.findInstanceContainers(id = "empty_quality")[0] #type: InstanceContainer self._empty_variant = cura_empty_instance_containers.empty_variant_container #type: InstanceContainer
self._empty_material = self._container_registry.findInstanceContainers(id = "empty_material")[0] #type: InstanceContainer
self._empty_variant = self._container_registry.findInstanceContainers(id = "empty_variant")[0] #type: InstanceContainer
self._containers = [self._empty_instance_container for i in range(len(_ContainerIndexes.IndexTypeMap))] #type: List[ContainerInterface] self._containers = [self._empty_instance_container for i in range(len(_ContainerIndexes.IndexTypeMap))] #type: List[ContainerInterface]
self._containers[_ContainerIndexes.QualityChanges] = self._empty_quality_changes self._containers[_ContainerIndexes.QualityChanges] = self._empty_quality_changes
@ -292,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

@ -8,13 +8,14 @@ from UM.Logger import Logger
from UM.Settings.Interfaces import DefinitionContainerInterface from UM.Settings.Interfaces import DefinitionContainerInterface
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
from cura.Machines.VariantManager import VariantType from cura.Machines.VariantType import VariantType
from .GlobalStack import GlobalStack from .GlobalStack import GlobalStack
from .ExtruderStack import ExtruderStack 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,50 +64,35 @@ 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)
preferred_quality_type = machine_definition.getMetaDataEntry("preferred_quality_type") preferred_quality_type = machine_definition.getMetaDataEntry("preferred_quality_type")
quality_group_dict = quality_manager.getQualityGroups(new_global_stack) quality_group_dict = quality_manager.getQualityGroups(new_global_stack)
quality_group = quality_group_dict.get(preferred_quality_type) if not quality_group_dict:
# There is no available quality group, set all quality containers to empty.
new_global_stack.quality = quality_group.node_for_global.getContainer()
if not new_global_stack.quality:
new_global_stack.quality = application.empty_quality_container new_global_stack.quality = application.empty_quality_container
for position, extruder_stack in new_global_stack.extruders.items(): for extruder_stack in new_global_stack.extruders.values():
if position in quality_group.nodes_for_extruders and quality_group.nodes_for_extruders[position].getContainer():
extruder_stack.quality = quality_group.nodes_for_extruders[position].getContainer()
else:
extruder_stack.quality = application.empty_quality_container extruder_stack.quality = application.empty_quality_container
else:
# Set the quality containers to the preferred quality type if available, otherwise use the first quality
# type that's available.
if preferred_quality_type not in quality_group_dict:
Logger.log("w", "The preferred quality {quality_type} doesn't exist for this set-up. Choosing a random one.".format(quality_type = preferred_quality_type))
preferred_quality_type = next(iter(quality_group_dict))
quality_group = quality_group_dict.get(preferred_quality_type)
new_global_stack.quality = quality_group.node_for_global.getContainer()
if not new_global_stack.quality:
new_global_stack.quality = application.empty_quality_container
for position, extruder_stack in new_global_stack.extruders.items():
if position in quality_group.nodes_for_extruders and quality_group.nodes_for_extruders[position].getContainer():
extruder_stack.quality = quality_group.nodes_for_extruders[position].getContainer()
else:
extruder_stack.quality = application.empty_quality_container
# Register the global stack after the extruder stacks are created. This prevents the registry from adding another # Register the global stack after the extruder stacks are created. This prevents the registry from adding another
# extruder stack because the global stack didn't have one yet (which is enforced since Cura 3.1). # extruder stack because the global stack didn't have one yet (which is enforced since Cura 3.1).
@ -125,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()
@ -146,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)
@ -172,9 +201,22 @@ class CuraStackBuilder:
# \param kwargs You can add keyword arguments to specify IDs of containers to use for a specific type, for example "variant": "0.4mm" # \param kwargs You can add keyword arguments to specify IDs of containers to use for a specific type, for example "variant": "0.4mm"
# #
# \return A new Global stack instance with the specified parameters. # \return A new Global stack instance with the specified parameters.
## Create a new Global stack
#
# \param new_stack_id The ID of the new stack.
# \param definition The definition to base the new stack on.
# \param variant_container The variant selected for the current stack.
# \param material_container The material selected for the current stack.
# \param quality_container The quality selected for the current stack.
#
# \return A new Global stack instance with the specified parameters.
@classmethod @classmethod
def createGlobalStack(cls, new_stack_id: str, definition: DefinitionContainerInterface, def createGlobalStack(cls, new_stack_id: str, definition: DefinitionContainerInterface,
variant_container, material_container, quality_container) -> GlobalStack: variant_container: "InstanceContainer",
material_container: "InstanceContainer",
quality_container: "InstanceContainer") -> GlobalStack:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
application = CuraApplication.getInstance() application = CuraApplication.getInstance()
registry = application.getContainerRegistry() registry = application.getContainerRegistry()

View file

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

View file

@ -139,9 +139,6 @@ class ExtruderStack(CuraContainerStack):
super().deserialize(contents, file_name) super().deserialize(contents, file_name)
if "enabled" not in self.getMetaData(): if "enabled" not in self.getMetaData():
self.setMetaDataEntry("enabled", "True") self.setMetaDataEntry("enabled", "True")
stacks = ContainerRegistry.getInstance().findContainerStacks(id=self.getMetaDataEntry("machine", ""))
if stacks:
self.setNextStack(stacks[0])
def _onPropertiesChanged(self, key: str, properties: Dict[str, Any]) -> None: def _onPropertiesChanged(self, key: str, properties: Dict[str, Any]) -> None:
# When there is a setting that is not settable per extruder that depends on a value from a setting that is, # When there is a setting that is not settable per extruder that depends on a value from a setting that is,

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):
@ -55,6 +58,16 @@ class GlobalStack(CuraContainerStack):
return "machine_stack" return "machine_stack"
return configuration_type return configuration_type
def getBuildplateName(self) -> Optional[str]:
name = None
if self.variant.getId() != "empty_variant":
name = self.variant.getName()
return name
@pyqtProperty(str, constant = True)
def preferred_output_file_formats(self) -> str:
return self.getMetaDataEntry("file_formats")
## Add an extruder to the list of extruders of this stack. ## Add an extruder to the list of extruders of this stack.
# #
# \param extruder The extruder to add. # \param extruder The extruder to add.
@ -96,6 +109,9 @@ class GlobalStack(CuraContainerStack):
# Handle the "resolve" property. # Handle the "resolve" property.
#TODO: Why the hell does this involve threading? #TODO: Why the hell does this involve threading?
# Answer: Because if multiple threads start resolving properties that have the same underlying properties that's
# related, without taking a note of which thread a resolve paths belongs to, they can bump into each other and
# generate unexpected behaviours.
if self._shouldResolve(key, property_name, context): if self._shouldResolve(key, property_name, context):
current_thread = threading.current_thread() current_thread = threading.current_thread()
self._resolving_settings[current_thread.name].add(key) self._resolving_settings[current_thread.name].add(key)
@ -172,6 +188,18 @@ class GlobalStack(CuraContainerStack):
return False return False
return True return True
def getHeadAndFansCoordinates(self):
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

@ -21,9 +21,6 @@ from UM.Settings.SettingFunction import SettingFunction
from UM.Signal import postponeSignals, CompressTechnique from UM.Signal import postponeSignals, CompressTechnique
import cura.CuraApplication import cura.CuraApplication
from cura.Machines.ContainerNode import ContainerNode #For typing.
from cura.Machines.QualityChangesGroup import QualityChangesGroup #For typing.
from cura.Machines.QualityGroup import QualityGroup #For typing.
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
from cura.PrinterOutputDevice import PrinterOutputDevice from cura.PrinterOutputDevice import PrinterOutputDevice
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
@ -44,12 +41,16 @@ if TYPE_CHECKING:
from cura.Machines.MaterialManager import MaterialManager from cura.Machines.MaterialManager import MaterialManager
from cura.Machines.QualityManager import QualityManager from cura.Machines.QualityManager import QualityManager
from cura.Machines.VariantManager import VariantManager from cura.Machines.VariantManager import VariantManager
from cura.Machines.ContainerNode import ContainerNode
from cura.Machines.QualityChangesGroup import QualityChangesGroup
from cura.Machines.QualityGroup import QualityGroup
class MachineManager(QObject): class MachineManager(QObject):
def __init__(self, parent: QObject = None) -> None: def __init__(self, parent: QObject = None) -> None:
super().__init__(parent) super().__init__(parent)
self._active_container_stack = None # type: Optional[ExtruderManager] self._active_container_stack = None # type: Optional[ExtruderStack]
self._global_container_stack = None # type: Optional[GlobalStack] self._global_container_stack = None # type: Optional[GlobalStack]
self._current_root_material_id = {} # type: Dict[str, str] self._current_root_material_id = {} # type: Dict[str, str]
@ -366,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())
@ -374,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()
@ -384,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:
@ -411,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()
@ -434,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
@ -447,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
@ -472,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:
@ -637,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:
@ -889,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)
@ -1087,7 +1095,7 @@ class MachineManager(QObject):
self.activeQualityGroupChanged.emit() self.activeQualityGroupChanged.emit()
self.activeQualityChangesGroupChanged.emit() self.activeQualityChangesGroupChanged.emit()
def _setQualityGroup(self, quality_group: Optional[QualityGroup], empty_quality_changes: bool = True) -> None: def _setQualityGroup(self, quality_group: Optional["QualityGroup"], empty_quality_changes: bool = True) -> None:
if self._global_container_stack is None: if self._global_container_stack is None:
return return
if quality_group is None: if quality_group is None:
@ -1118,7 +1126,7 @@ class MachineManager(QObject):
self.activeQualityGroupChanged.emit() self.activeQualityGroupChanged.emit()
self.activeQualityChangesGroupChanged.emit() self.activeQualityChangesGroupChanged.emit()
def _fixQualityChangesGroupToNotSupported(self, quality_changes_group: QualityChangesGroup) -> None: def _fixQualityChangesGroupToNotSupported(self, quality_changes_group: "QualityChangesGroup") -> None:
nodes = [quality_changes_group.node_for_global] + list(quality_changes_group.nodes_for_extruders.values()) nodes = [quality_changes_group.node_for_global] + list(quality_changes_group.nodes_for_extruders.values())
containers = [n.getContainer() for n in nodes if n is not None] containers = [n.getContainer() for n in nodes if n is not None]
for container in containers: for container in containers:
@ -1126,7 +1134,7 @@ class MachineManager(QObject):
container.setMetaDataEntry("quality_type", "not_supported") container.setMetaDataEntry("quality_type", "not_supported")
quality_changes_group.quality_type = "not_supported" quality_changes_group.quality_type = "not_supported"
def _setQualityChangesGroup(self, quality_changes_group: QualityChangesGroup) -> None: def _setQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None:
if self._global_container_stack is None: if self._global_container_stack is None:
return #Can't change that. return #Can't change that.
quality_type = quality_changes_group.quality_type quality_type = quality_changes_group.quality_type
@ -1170,20 +1178,20 @@ class MachineManager(QObject):
self.activeQualityGroupChanged.emit() self.activeQualityGroupChanged.emit()
self.activeQualityChangesGroupChanged.emit() self.activeQualityChangesGroupChanged.emit()
def _setVariantNode(self, position: str, container_node: ContainerNode) -> None: def _setVariantNode(self, position: str, container_node: "ContainerNode") -> None:
if container_node.getContainer() is None or self._global_container_stack is None: if container_node.getContainer() is None or self._global_container_stack is None:
return return
self._global_container_stack.extruders[position].variant = container_node.getContainer() self._global_container_stack.extruders[position].variant = container_node.getContainer()
self.activeVariantChanged.emit() self.activeVariantChanged.emit()
def _setGlobalVariant(self, container_node: ContainerNode) -> None: def _setGlobalVariant(self, container_node: "ContainerNode") -> None:
if self._global_container_stack is None: if self._global_container_stack is None:
return return
self._global_container_stack.variant = container_node.getContainer() self._global_container_stack.variant = container_node.getContainer()
if not self._global_container_stack.variant: if not self._global_container_stack.variant:
self._global_container_stack.variant = self._application.empty_variant_container self._global_container_stack.variant = self._application.empty_variant_container
def _setMaterial(self, position: str, container_node: ContainerNode = None) -> None: def _setMaterial(self, position: str, container_node: Optional["ContainerNode"] = None) -> None:
if self._global_container_stack is None: if self._global_container_stack is None:
return return
if container_node and container_node.getContainer(): if container_node and container_node.getContainer():
@ -1256,13 +1264,17 @@ class MachineManager(QObject):
else: else:
position_list = [position] position_list = [position]
buildplate_name = None
if self._global_container_stack.variant.getId() != "empty_variant":
buildplate_name = self._global_container_stack.variant.getName()
for position_item in position_list: for position_item in position_list:
extruder = self._global_container_stack.extruders[position_item] extruder = self._global_container_stack.extruders[position_item]
current_material_base_name = extruder.material.getMetaDataEntry("base_file") current_material_base_name = extruder.material.getMetaDataEntry("base_file")
current_variant_name = None current_nozzle_name = None
if extruder.variant.getId() != self._empty_variant_container.getId(): if extruder.variant.getId() != self._empty_variant_container.getId():
current_variant_name = extruder.variant.getMetaDataEntry("name") current_nozzle_name = extruder.variant.getMetaDataEntry("name")
from UM.Settings.Interfaces import PropertyEvaluationContext from UM.Settings.Interfaces import PropertyEvaluationContext
from cura.Settings.CuraContainerStack import _ContainerIndexes from cura.Settings.CuraContainerStack import _ContainerIndexes
@ -1271,7 +1283,8 @@ class MachineManager(QObject):
material_diameter = extruder.getProperty("material_diameter", "value", context) material_diameter = extruder.getProperty("material_diameter", "value", context)
candidate_materials = self._material_manager.getAvailableMaterials( candidate_materials = self._material_manager.getAvailableMaterials(
self._global_container_stack.definition, self._global_container_stack.definition,
current_variant_name, current_nozzle_name,
buildplate_name,
material_diameter) material_diameter)
if not candidate_materials: if not candidate_materials:
@ -1284,7 +1297,7 @@ class MachineManager(QObject):
continue continue
# The current material is not available, find the preferred one # The current material is not available, find the preferred one
material_node = self._material_manager.getDefaultMaterial(self._global_container_stack, position_item, current_variant_name) material_node = self._material_manager.getDefaultMaterial(self._global_container_stack, position_item, current_nozzle_name)
if material_node is not None: if material_node is not None:
self._setMaterial(position_item, material_node) self._setMaterial(position_item, material_node)
@ -1326,7 +1339,12 @@ class MachineManager(QObject):
for extruder_configuration in configuration.extruderConfigurations: for extruder_configuration in configuration.extruderConfigurations:
position = str(extruder_configuration.position) position = str(extruder_configuration.position)
variant_container_node = self._variant_manager.getVariantNode(self._global_container_stack.definition.getId(), extruder_configuration.hotendID) variant_container_node = self._variant_manager.getVariantNode(self._global_container_stack.definition.getId(), extruder_configuration.hotendID)
material_container_node = self._material_manager.getMaterialNodeByType(self._global_container_stack, position, extruder_configuration.hotendID, extruder_configuration.material.guid) material_container_node = self._material_manager.getMaterialNodeByType(self._global_container_stack,
position,
extruder_configuration.hotendID,
configuration.buildplateConfiguration,
extruder_configuration.material.guid)
if variant_container_node: if variant_container_node:
self._setVariantNode(position, variant_container_node) self._setVariantNode(position, variant_container_node)
else: else:
@ -1378,7 +1396,7 @@ class MachineManager(QObject):
return bool(containers) return bool(containers)
@pyqtSlot("QVariant") @pyqtSlot("QVariant")
def setGlobalVariant(self, container_node: ContainerNode) -> None: def setGlobalVariant(self, container_node: "ContainerNode") -> None:
self.blurSettings.emit() self.blurSettings.emit()
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
self._setGlobalVariant(container_node) self._setGlobalVariant(container_node)
@ -1389,12 +1407,17 @@ class MachineManager(QObject):
def setMaterialById(self, position: str, root_material_id: str) -> None: def setMaterialById(self, position: str, root_material_id: str) -> None:
if self._global_container_stack is None: if self._global_container_stack is None:
return return
buildplate_name = None
if self._global_container_stack.variant.getId() != "empty_variant":
buildplate_name = self._global_container_stack.variant.getName()
machine_definition_id = self._global_container_stack.definition.id machine_definition_id = self._global_container_stack.definition.id
position = str(position) position = str(position)
extruder_stack = self._global_container_stack.extruders[position] extruder_stack = self._global_container_stack.extruders[position]
variant_name = extruder_stack.variant.getName() nozzle_name = extruder_stack.variant.getName()
material_diameter = extruder_stack.approximateMaterialDiameter material_diameter = extruder_stack.approximateMaterialDiameter
material_node = self._material_manager.getMaterialNode(machine_definition_id, variant_name, material_diameter, root_material_id) material_node = self._material_manager.getMaterialNode(machine_definition_id, nozzle_name, buildplate_name,
material_diameter, root_material_id)
self.setMaterial(position, material_node) self.setMaterial(position, material_node)
## global_stack: if you want to provide your own global_stack instead of the current active one ## global_stack: if you want to provide your own global_stack instead of the current active one
@ -1423,7 +1446,7 @@ class MachineManager(QObject):
self.setVariant(position, variant_node) self.setVariant(position, variant_node)
@pyqtSlot(str, "QVariant") @pyqtSlot(str, "QVariant")
def setVariant(self, position: str, container_node: ContainerNode) -> None: def setVariant(self, position: str, container_node: "ContainerNode") -> None:
position = str(position) position = str(position)
self.blurSettings.emit() self.blurSettings.emit()
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
@ -1447,7 +1470,7 @@ class MachineManager(QObject):
## Optionally provide global_stack if you want to use your own ## Optionally provide global_stack if you want to use your own
# The active global_stack is treated differently. # The active global_stack is treated differently.
@pyqtSlot(QObject) @pyqtSlot(QObject)
def setQualityGroup(self, quality_group: QualityGroup, no_dialog: bool = False, global_stack: Optional["GlobalStack"] = None) -> None: def setQualityGroup(self, quality_group: "QualityGroup", no_dialog: bool = False, global_stack: Optional["GlobalStack"] = None) -> None:
if global_stack is not None and global_stack != self._global_container_stack: if global_stack is not None and global_stack != self._global_container_stack:
if quality_group is None: if quality_group is None:
Logger.log("e", "Could not set quality group because quality group is None") Logger.log("e", "Could not set quality group because quality group is None")
@ -1455,9 +1478,14 @@ class MachineManager(QObject):
if quality_group.node_for_global is None: if quality_group.node_for_global is None:
Logger.log("e", "Could not set quality group [%s] because it has no node_for_global", str(quality_group)) Logger.log("e", "Could not set quality group [%s] because it has no node_for_global", str(quality_group))
return return
# This is not changing the quality for the active machine !!!!!!!!
global_stack.quality = quality_group.node_for_global.getContainer() global_stack.quality = quality_group.node_for_global.getContainer()
for extruder_nr, extruder_stack in global_stack.extruders.items(): for extruder_nr, extruder_stack in global_stack.extruders.items():
extruder_stack.quality = quality_group.nodes_for_extruders[extruder_nr].getContainer() quality_container = self._empty_quality_container
if extruder_nr in quality_group.nodes_for_extruders:
container = quality_group.nodes_for_extruders[extruder_nr].getContainer()
quality_container = container if container is not None else quality_container
extruder_stack.quality = quality_container
return return
self.blurSettings.emit() self.blurSettings.emit()
@ -1469,11 +1497,11 @@ class MachineManager(QObject):
self._application.discardOrKeepProfileChanges() self._application.discardOrKeepProfileChanges()
@pyqtProperty(QObject, fset = setQualityGroup, notify = activeQualityGroupChanged) @pyqtProperty(QObject, fset = setQualityGroup, notify = activeQualityGroupChanged)
def activeQualityGroup(self) -> Optional[QualityGroup]: def activeQualityGroup(self) -> Optional["QualityGroup"]:
return self._current_quality_group return self._current_quality_group
@pyqtSlot(QObject) @pyqtSlot(QObject)
def setQualityChangesGroup(self, quality_changes_group: QualityChangesGroup, no_dialog: bool = False) -> None: def setQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", no_dialog: bool = False) -> None:
self.blurSettings.emit() self.blurSettings.emit()
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
self._setQualityChangesGroup(quality_changes_group) self._setQualityChangesGroup(quality_changes_group)
@ -1492,7 +1520,7 @@ class MachineManager(QObject):
stack.userChanges.clear() stack.userChanges.clear()
@pyqtProperty(QObject, fset = setQualityChangesGroup, notify = activeQualityChangesGroupChanged) @pyqtProperty(QObject, fset = setQualityChangesGroup, notify = activeQualityChangesGroupChanged)
def activeQualityChangesGroup(self) -> Optional[QualityChangesGroup]: def activeQualityChangesGroup(self) -> Optional["QualityChangesGroup"]:
return self._current_quality_changes_group return self._current_quality_changes_group
@pyqtProperty(str, notify = activeQualityGroupChanged) @pyqtProperty(str, notify = activeQualityGroupChanged)

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

@ -0,0 +1,41 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Any
from UM.Qt.ListModel import ListModel
from PyQt5.QtCore import pyqtSlot, Qt
class SidebarCustomMenuItemsModel(ListModel):
name_role = Qt.UserRole + 1
actions_role = Qt.UserRole + 2
menu_item_role = Qt.UserRole + 3
menu_item_icon_name_role = Qt.UserRole + 5
def __init__(self, parent=None):
super().__init__(parent)
self.addRoleName(self.name_role, "name")
self.addRoleName(self.actions_role, "actions")
self.addRoleName(self.menu_item_role, "menu_item")
self.addRoleName(self.menu_item_icon_name_role, "icon_name")
self._updateExtensionList()
def _updateExtensionList(self)-> None:
from cura.CuraApplication import CuraApplication
for menu_item in CuraApplication.getInstance().getSidebarCustomMenuItems():
self.appendItem({
"name": menu_item["name"],
"icon_name": menu_item["icon_name"],
"actions": menu_item["actions"],
"menu_item": menu_item["menu_item"]
})
@pyqtSlot(str, "QVariantList", "QVariantMap")
def callMenuItemMethod(self, menu_item_name: str, menu_item_actions: list, kwargs: Any) -> None:
for item in self._items:
if menu_item_name == item["name"]:
for method in menu_item_actions:
getattr(item["menu_item"], method)(kwargs)
break

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

@ -0,0 +1,56 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import copy
from UM.Settings.constant_instance_containers import EMPTY_CONTAINER_ID, empty_container
# Empty definition changes
EMPTY_DEFINITION_CHANGES_CONTAINER_ID = "empty_definition_changes"
empty_definition_changes_container = copy.deepcopy(empty_container)
empty_definition_changes_container.setMetaDataEntry("id", EMPTY_DEFINITION_CHANGES_CONTAINER_ID)
empty_definition_changes_container.setMetaDataEntry("type", "definition_changes")
# Empty variant
EMPTY_VARIANT_CONTAINER_ID = "empty_variant"
empty_variant_container = copy.deepcopy(empty_container)
empty_variant_container.setMetaDataEntry("id", EMPTY_VARIANT_CONTAINER_ID)
empty_variant_container.setMetaDataEntry("type", "variant")
# Empty material
EMPTY_MATERIAL_CONTAINER_ID = "empty_material"
empty_material_container = copy.deepcopy(empty_container)
empty_material_container.setMetaDataEntry("id", EMPTY_MATERIAL_CONTAINER_ID)
empty_material_container.setMetaDataEntry("type", "material")
# Empty quality
EMPTY_QUALITY_CONTAINER_ID = "empty_quality"
empty_quality_container = copy.deepcopy(empty_container)
empty_quality_container.setMetaDataEntry("id", EMPTY_QUALITY_CONTAINER_ID)
empty_quality_container.setName("Not Supported")
empty_quality_container.setMetaDataEntry("quality_type", "not_supported")
empty_quality_container.setMetaDataEntry("type", "quality")
empty_quality_container.setMetaDataEntry("supported", False)
# Empty quality changes
EMPTY_QUALITY_CHANGES_CONTAINER_ID = "empty_quality_changes"
empty_quality_changes_container = copy.deepcopy(empty_container)
empty_quality_changes_container.setMetaDataEntry("id", EMPTY_QUALITY_CHANGES_CONTAINER_ID)
empty_quality_changes_container.setMetaDataEntry("type", "quality_changes")
empty_quality_changes_container.setMetaDataEntry("quality_type", "not_supported")
__all__ = ["EMPTY_CONTAINER_ID",
"empty_container", # For convenience
"EMPTY_DEFINITION_CHANGES_CONTAINER_ID",
"empty_definition_changes_container",
"EMPTY_VARIANT_CONTAINER_ID",
"empty_variant_container",
"EMPTY_MATERIAL_CONTAINER_ID",
"empty_material_container",
"EMPTY_QUALITY_CHANGES_CONTAINER_ID",
"empty_quality_changes_container",
"EMPTY_QUALITY_CONTAINER_ID",
"empty_quality_container"
]

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

@ -24,6 +24,7 @@ from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
from UM.Job import Job from UM.Job import Job
from UM.Preferences import Preferences from UM.Preferences import Preferences
from cura.Machines.VariantType import VariantType
from cura.Settings.CuraStackBuilder import CuraStackBuilder from cura.Settings.CuraStackBuilder import CuraStackBuilder
from cura.Settings.ExtruderStack import ExtruderStack from cura.Settings.ExtruderStack import ExtruderStack
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
@ -84,14 +85,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
MimeTypeDatabase.addMimeType(
MimeType(
name="application/x-curaproject+xml",
comment="Cura Project File",
suffixes=["curaproject.3mf"]
)
)
self._supported_extensions = [".3mf"] self._supported_extensions = [".3mf"]
self._dialog = WorkspaceDialog() self._dialog = WorkspaceDialog()
self._3mf_mesh_reader = None self._3mf_mesh_reader = None
@ -629,6 +622,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
type = "extruder_train") type = "extruder_train")
extruder_stack_dict = {stack.getMetaDataEntry("position"): stack for stack in extruder_stacks} extruder_stack_dict = {stack.getMetaDataEntry("position"): stack for stack in extruder_stacks}
# Make sure that those extruders have the global stack as the next stack or later some value evaluation
# will fail.
for stack in extruder_stacks:
stack.setNextStack(global_stack, connect_signals = False)
Logger.log("d", "Workspace loading is checking definitions...") Logger.log("d", "Workspace loading is checking definitions...")
# Get all the definition files & check if they exist. If not, add them. # Get all the definition files & check if they exist. If not, add them.
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)] definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
@ -720,8 +718,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
nodes = [] nodes = []
base_file_name = os.path.basename(file_name) base_file_name = os.path.basename(file_name)
if base_file_name.endswith(".curaproject.3mf"):
base_file_name = base_file_name[:base_file_name.rfind(".curaproject.3mf")]
self.setWorkspaceName(base_file_name) self.setWorkspaceName(base_file_name)
return nodes return nodes
@ -889,7 +885,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
parser = self._machine_info.variant_info.parser parser = self._machine_info.variant_info.parser
variant_name = parser["general"]["name"] variant_name = parser["general"]["name"]
from cura.Machines.VariantManager import VariantType
variant_type = VariantType.BUILD_PLATE variant_type = VariantType.BUILD_PLATE
node = variant_manager.getVariantNode(global_stack.definition.getId(), variant_name, variant_type) node = variant_manager.getVariantNode(global_stack.definition.getId(), variant_name, variant_type)
@ -905,7 +900,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
parser = extruder_info.variant_info.parser parser = extruder_info.variant_info.parser
variant_name = parser["general"]["name"] variant_name = parser["general"]["name"]
from cura.Machines.VariantManager import VariantType
variant_type = VariantType.NOZZLE variant_type = VariantType.NOZZLE
node = variant_manager.getVariantNode(global_stack.definition.getId(), variant_name, variant_type) node = variant_manager.getVariantNode(global_stack.definition.getId(), variant_name, variant_type)
@ -929,14 +923,18 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
root_material_id = extruder_info.root_material_id root_material_id = extruder_info.root_material_id
root_material_id = self._old_new_materials.get(root_material_id, root_material_id) root_material_id = self._old_new_materials.get(root_material_id, root_material_id)
build_plate_id = global_stack.variant.getId()
# get material diameter of this extruder # get material diameter of this extruder
machine_material_diameter = extruder_stack.materialDiameter machine_material_diameter = extruder_stack.materialDiameter
material_node = material_manager.getMaterialNode(global_stack.definition.getId(), material_node = material_manager.getMaterialNode(global_stack.definition.getId(),
extruder_stack.variant.getName(), extruder_stack.variant.getName(),
build_plate_id,
machine_material_diameter, machine_material_diameter,
root_material_id) root_material_id)
if material_node is not None and material_node.getContainer() is not None: if material_node is not None and material_node.getContainer() is not None:
extruder_stack.material = material_node.getContainer() extruder_stack.material = material_node.getContainer() # type: InstanceContainer
def _applyChangesToMachine(self, global_stack, extruder_stack_dict): def _applyChangesToMachine(self, global_stack, extruder_stack_dict):
# Clear all first # Clear all first

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -29,6 +29,7 @@ message Object
bytes normals = 3; //An array of 3 floats. bytes normals = 3; //An array of 3 floats.
bytes indices = 4; //An array of ints. bytes indices = 4; //An array of ints.
repeated Setting settings = 5; // Setting override per object, overruling the global settings. repeated Setting settings = 5; // Setting override per object, overruling the global settings.
string name = 6;
} }
message Progress message Progress

View file

@ -1,6 +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.
import argparse #To run the engine in debug mode if the front-end is in debug mode.
from collections import defaultdict from collections import defaultdict
import os import os
from PyQt5.QtCore import QObject, QTimer, pyqtSlot from PyQt5.QtCore import QObject, QTimer, pyqtSlot
@ -178,8 +179,15 @@ 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), ""]
return [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.add_argument("--debug", action = "store_true", default = False, help = "Turn on the debug mode by setting this option.")
known_args = vars(parser.parse_known_args()[0])
if known_args["debug"]:
command.append("-vvv")
return command
## Emitted when we get a message containing print duration and material amount. ## Emitted when we get a message containing print duration and material amount.
# This also implies the slicing has finished. # This also implies the slicing has finished.
@ -334,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

@ -41,7 +41,7 @@ class StartJobResult(IntEnum):
## Formatter class that handles token expansion in start/end gcode ## Formatter class that handles token expansion in start/end gcode
class GcodeStartEndFormatter(Formatter): class GcodeStartEndFormatter(Formatter):
def get_value(self, key: str, *args: str, default_extruder_nr: str = "-1", **kwargs) -> str: #type: ignore # [CodeStyle: get_value is an overridden function from the Formatter class] def get_value(self, key: str, args: str, kwargs: dict, default_extruder_nr: str = "-1") -> str: #type: ignore # [CodeStyle: get_value is an overridden function from the Formatter class]
# The kwargs dictionary contains a dictionary for each stack (with a string of the extruder_nr as their key), # The kwargs dictionary contains a dictionary for each stack (with a string of the extruder_nr as their key),
# and a default_extruder_nr to use when no extruder_nr is specified # and a default_extruder_nr to use when no extruder_nr is specified
@ -220,8 +220,10 @@ class StartSliceJob(Job):
stack = global_stack stack = global_stack
skip_group = False skip_group = False
for node in group: for node in group:
# Only check if the printing extruder is enabled for printing meshes
is_non_printing_mesh = node.callDecoration("evaluateIsNonPrintingMesh")
extruder_position = node.callDecoration("getActiveExtruderPosition") extruder_position = node.callDecoration("getActiveExtruderPosition")
if not extruders_enabled[extruder_position]: if not is_non_printing_mesh and not extruders_enabled[extruder_position]:
skip_group = True skip_group = True
has_model_with_disabled_extruders = True has_model_with_disabled_extruders = True
associated_disabled_extruders.add(extruder_position) associated_disabled_extruders.add(extruder_position)
@ -268,7 +270,7 @@ class StartSliceJob(Job):
obj = group_message.addRepeatedMessage("objects") obj = group_message.addRepeatedMessage("objects")
obj.id = id(object) obj.id = id(object)
obj.name = object.getName()
indices = mesh_data.getIndices() indices = mesh_data.getIndices()
if indices is not None: if indices is not None:
flat_verts = numpy.take(verts, indices.flatten(), axis=0) flat_verts = numpy.take(verts, indices.flatten(), axis=0)
@ -331,7 +333,7 @@ class StartSliceJob(Job):
"-1": self._buildReplacementTokens(global_stack) "-1": self._buildReplacementTokens(global_stack)
} }
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()): for extruder_stack in ExtruderManager.getInstance().getActiveExtruderStacks():
extruder_nr = extruder_stack.getProperty("extruder_nr", "value") extruder_nr = extruder_stack.getProperty("extruder_nr", "value")
self._all_extruders_settings[str(extruder_nr)] = self._buildReplacementTokens(extruder_stack) self._all_extruders_settings[str(extruder_nr)] = self._buildReplacementTokens(extruder_stack)
@ -438,8 +440,7 @@ class StartSliceJob(Job):
Job.yieldThread() Job.yieldThread()
# Ensure that the engine is aware what the build extruder is. # Ensure that the engine is aware what the build extruder is.
if stack.getProperty("machine_extruder_count", "value") > 1: changed_setting_keys.add("extruder_nr")
changed_setting_keys.add("extruder_nr")
# Get values for all changed settings # Get values for all changed settings
for key in changed_setting_keys: for key in changed_setting_keys:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -47,7 +47,7 @@ class GCodeWriter(MeshWriter):
_setting_keyword = ";SETTING_" _setting_keyword = ";SETTING_"
def __init__(self): def __init__(self):
super().__init__() super().__init__(add_to_recent_files = False)
self._application = Application.getInstance() self._application = Application.getInstance()
@ -70,7 +70,7 @@ class GCodeWriter(MeshWriter):
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
scene = Application.getInstance().getController().getScene() scene = Application.getInstance().getController().getScene()
if not hasattr(scene, "gcode_dict"): if not hasattr(scene, "gcode_dict"):
self.setInformation(catalog.i18nc("@warning:status", "Please generate G-code before saving.")) self.setInformation(catalog.i18nc("@warning:status", "Please prepare G-code before exporting."))
return False return False
gcode_dict = getattr(scene, "gcode_dict") gcode_dict = getattr(scene, "gcode_dict")
gcode_list = gcode_dict.get(active_build_plate, None) gcode_list = gcode_dict.get(active_build_plate, None)
@ -86,7 +86,7 @@ class GCodeWriter(MeshWriter):
stream.write(settings) stream.write(settings)
return True return True
self.setInformation(catalog.i18nc("@warning:status", "Please generate G-code before saving.")) self.setInformation(catalog.i18nc("@warning:status", "Please prepare G-code before exporting."))
return False return False
## Create a new container with container 2 as base and container 1 written over it. ## Create a new container with container 2 as base and container 1 written over it.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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