mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-07-25 01:54:24 +08:00
Merge branch 'master' into top_bottom_settings_enabled_function
This commit is contained in:
commit
d81cdee9b9
2
.gitignore
vendored
2
.gitignore
vendored
@ -15,6 +15,7 @@ LC_MESSAGES
|
||||
.cache
|
||||
*.qmlc
|
||||
.mypy_cache
|
||||
.pytest_cache
|
||||
|
||||
#MacOS
|
||||
.DS_Store
|
||||
@ -25,6 +26,7 @@ LC_MESSAGES
|
||||
*.lprof
|
||||
*~
|
||||
*.qm
|
||||
.directory
|
||||
.idea
|
||||
cura.desktop
|
||||
|
||||
|
75
Jenkinsfile
vendored
75
Jenkinsfile
vendored
@ -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.
|
||||
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.
|
||||
dir('build') {
|
||||
// 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".
|
||||
stage('Unit Test') {
|
||||
try {
|
||||
make('test')
|
||||
} catch(e) {
|
||||
currentBuild.result = "UNSTABLE"
|
||||
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/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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ function(cura_add_test)
|
||||
if (NOT ${test_exists})
|
||||
add_test(
|
||||
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 "PYTHONPATH=${_PYTHONPATH}")
|
||||
|
@ -13,6 +13,6 @@ TryExec=@CMAKE_INSTALL_FULL_BINDIR@/cura
|
||||
Icon=cura-icon
|
||||
Terminal=false
|
||||
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;
|
||||
Keywords=3D;Printing;Slicer;
|
||||
|
@ -6,7 +6,7 @@
|
||||
<glob-deleteall/>
|
||||
<glob pattern="*.3mf"/>
|
||||
</mime-type>
|
||||
<mime-type type="application/sla">
|
||||
<mime-type type="model/stl">
|
||||
<comment>Computer-aided design and manufacturing format</comment>
|
||||
<icon name="unknown"/>
|
||||
<glob-deleteall/>
|
||||
|
@ -13,6 +13,7 @@ from cura.Backups.BackupsManager import BackupsManager
|
||||
# api = CuraAPI()
|
||||
# api.backups.createBackup()
|
||||
# api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})``
|
||||
|
||||
class Backups:
|
||||
manager = BackupsManager() # Re-used instance of the backups manager.
|
||||
|
||||
|
33
cura/API/Interface/Settings.py
Normal file
33
cura/API/Interface/Settings.py
Normal 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()
|
24
cura/API/Interface/__init__.py
Normal file
24
cura/API/Interface/__init__.py
Normal 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()
|
@ -2,6 +2,7 @@
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
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.
|
||||
#
|
||||
@ -9,10 +10,14 @@ from cura.API.Backups import Backups
|
||||
# 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
|
||||
# plug-ins to be unstable.
|
||||
|
||||
class CuraAPI:
|
||||
|
||||
# For now we use the same API version to be consistent.
|
||||
VERSION = PluginRegistry.APIVersion
|
||||
|
||||
# Backups API.
|
||||
# Backups API
|
||||
backups = Backups()
|
||||
|
||||
# Interface API
|
||||
interface = Interface()
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from UM.Application import Application #To modify the maximum zoom level.
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Scene.Platform import Platform
|
||||
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||
@ -27,7 +28,7 @@ import copy
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -170,6 +171,12 @@ class BuildVolume(SceneNode):
|
||||
if 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]:
|
||||
return self._disallowed_areas
|
||||
|
||||
@ -235,6 +242,8 @@ class BuildVolume(SceneNode):
|
||||
|
||||
# Mark the node as outside build volume if the set extruder is disabled
|
||||
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:
|
||||
node.setOutsideBuildArea(True)
|
||||
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)
|
||||
)
|
||||
|
||||
self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds
|
||||
|
||||
self.updateNodeBoundaryCheck()
|
||||
|
||||
def getBoundingBox(self) -> AxisAlignedBox:
|
||||
@ -519,7 +526,7 @@ class BuildVolume(SceneNode):
|
||||
def _onStackChanged(self):
|
||||
if self._global_container_stack:
|
||||
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:
|
||||
extruder.propertyChanged.disconnect(self._onSettingPropertyChanged)
|
||||
|
||||
@ -527,7 +534,7 @@ class BuildVolume(SceneNode):
|
||||
|
||||
if self._global_container_stack:
|
||||
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:
|
||||
extruder.propertyChanged.connect(self._onSettingPropertyChanged)
|
||||
|
||||
@ -552,6 +559,12 @@ class BuildVolume(SceneNode):
|
||||
if self._engine_ready:
|
||||
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):
|
||||
self._engine_ready = True
|
||||
self.rebuild()
|
||||
|
@ -19,5 +19,11 @@ class CameraImageProvider(QQuickImageProvider):
|
||||
|
||||
return image, QSize(15, 15)
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
image = output_device.activeCamera.getImage()
|
||||
|
||||
return image, QSize(15, 15)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return QImage(), QSize(15, 15)
|
||||
|
@ -50,7 +50,8 @@ class CuraActions(QObject):
|
||||
scene = cura.CuraApplication.CuraApplication.getInstance().getController().getScene()
|
||||
camera = scene.getActiveCamera()
|
||||
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.lookAt(Vector(0, 0, 0))
|
||||
|
||||
|
@ -1,11 +1,10 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import copy
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from typing import cast, TYPE_CHECKING, Optional
|
||||
from typing import cast, TYPE_CHECKING
|
||||
|
||||
import numpy
|
||||
|
||||
@ -14,6 +13,7 @@ from PyQt5.QtGui import QColor, QIcon
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
|
||||
|
||||
from UM.PluginError import PluginNotFoundError
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.Camera import Camera
|
||||
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.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
|
||||
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.BrandMaterialsModel import BrandMaterialsModel
|
||||
from cura.Machines.Models.MaterialBrandsModel import MaterialBrandsModel
|
||||
from cura.Machines.Models.QualityManagementModel import QualityManagementModel
|
||||
from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
|
||||
from cura.Machines.Models.MachineManagementModel import MachineManagementModel
|
||||
@ -94,6 +94,7 @@ from . import CuraActions
|
||||
from cura.Scene import ZOffsetDecorator
|
||||
from . import CuraSplashScreen
|
||||
from . import CameraImageProvider
|
||||
from . import PrintJobPreviewImageProvider
|
||||
from . import MachineActionManager
|
||||
|
||||
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.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
|
||||
from cura.Settings.ContainerManager import ContainerManager
|
||||
from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
|
||||
import cura.Settings.cura_empty_instance_containers
|
||||
|
||||
from cura.ObjectsModel import ObjectsModel
|
||||
|
||||
@ -111,17 +114,20 @@ from UM.FlameProfiler import pyqtSlot
|
||||
|
||||
|
||||
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")
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraVersion, CuraBuildType, CuraDebugMode
|
||||
from cura.CuraVersion import CuraVersion, CuraBuildType, CuraDebugMode, CuraSDKVersion
|
||||
except ImportError:
|
||||
CuraVersion = "master" # [CodeStyle: Reflecting imported value]
|
||||
CuraBuildType = ""
|
||||
CuraDebugMode = False
|
||||
CuraSDKVersion = ""
|
||||
|
||||
|
||||
class CuraApplication(QtApplication):
|
||||
@ -172,12 +178,12 @@ class CuraApplication(QtApplication):
|
||||
|
||||
self._machine_action_manager = None
|
||||
|
||||
self.empty_container = None
|
||||
self.empty_definition_changes_container = None
|
||||
self.empty_variant_container = None
|
||||
self.empty_material_container = None
|
||||
self.empty_quality_container = None
|
||||
self.empty_quality_changes_container = None
|
||||
self.empty_container = None # type: EmptyInstanceContainer
|
||||
self.empty_definition_changes_container = None # type: EmptyInstanceContainer
|
||||
self.empty_variant_container = None # type: EmptyInstanceContainer
|
||||
self.empty_material_container = None # type: EmptyInstanceContainer
|
||||
self.empty_quality_container = None # type: EmptyInstanceContainer
|
||||
self.empty_quality_changes_container = None # type: EmptyInstanceContainer
|
||||
|
||||
self._variant_manager = None
|
||||
self._material_manager = None
|
||||
@ -213,7 +219,6 @@ class CuraApplication(QtApplication):
|
||||
|
||||
self._message_box_callback = None
|
||||
self._message_box_callback_arguments = []
|
||||
self._preferred_mimetype = ""
|
||||
self._i18n_catalog = None
|
||||
|
||||
self._currently_loading_files = []
|
||||
@ -226,6 +231,10 @@ class CuraApplication(QtApplication):
|
||||
|
||||
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
|
||||
self._auto_save = None
|
||||
self._save_data_enabled = True
|
||||
@ -362,42 +371,23 @@ class CuraApplication(QtApplication):
|
||||
# Add empty variant, material and quality containers.
|
||||
# Since they are empty, they should never be serialized and instead just programmatically created.
|
||||
# We need them to simplify the switching between materials.
|
||||
empty_container = self._container_registry.getEmptyInstanceContainer()
|
||||
self.empty_container = empty_container
|
||||
self.empty_container = cura.Settings.cura_empty_instance_containers.empty_container # type: EmptyInstanceContainer
|
||||
|
||||
empty_definition_changes_container = copy.deepcopy(empty_container)
|
||||
empty_definition_changes_container.setMetaDataEntry("id", "empty_definition_changes")
|
||||
empty_definition_changes_container.setMetaDataEntry("type", "definition_changes")
|
||||
self._container_registry.addContainer(empty_definition_changes_container)
|
||||
self.empty_definition_changes_container = empty_definition_changes_container
|
||||
self._container_registry.addContainer(
|
||||
cura.Settings.cura_empty_instance_containers.empty_definition_changes_container)
|
||||
self.empty_definition_changes_container = cura.Settings.cura_empty_instance_containers.empty_definition_changes_container
|
||||
|
||||
empty_variant_container = copy.deepcopy(empty_container)
|
||||
empty_variant_container.setMetaDataEntry("id", "empty_variant")
|
||||
empty_variant_container.setMetaDataEntry("type", "variant")
|
||||
self._container_registry.addContainer(empty_variant_container)
|
||||
self.empty_variant_container = empty_variant_container
|
||||
self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_variant_container)
|
||||
self.empty_variant_container = cura.Settings.cura_empty_instance_containers.empty_variant_container
|
||||
|
||||
empty_material_container = copy.deepcopy(empty_container)
|
||||
empty_material_container.setMetaDataEntry("id", "empty_material")
|
||||
empty_material_container.setMetaDataEntry("type", "material")
|
||||
self._container_registry.addContainer(empty_material_container)
|
||||
self.empty_material_container = empty_material_container
|
||||
self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_material_container)
|
||||
self.empty_material_container = cura.Settings.cura_empty_instance_containers.empty_material_container
|
||||
|
||||
empty_quality_container = copy.deepcopy(empty_container)
|
||||
empty_quality_container.setMetaDataEntry("id", "empty_quality")
|
||||
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
|
||||
self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_quality_container)
|
||||
self.empty_quality_container = cura.Settings.cura_empty_instance_containers.empty_quality_container
|
||||
|
||||
empty_quality_changes_container = copy.deepcopy(empty_container)
|
||||
empty_quality_changes_container.setMetaDataEntry("id", "empty_quality_changes")
|
||||
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
|
||||
self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_quality_changes_container)
|
||||
self.empty_quality_changes_container = cura.Settings.cura_empty_instance_containers.empty_quality_changes_container
|
||||
|
||||
# Initializes the version upgrade manager with by providing the paths for each resource type and the latest
|
||||
# versions.
|
||||
@ -442,6 +432,7 @@ class CuraApplication(QtApplication):
|
||||
# Readers & Writers:
|
||||
"GCodeWriter",
|
||||
"STLReader",
|
||||
"3MFWriter",
|
||||
|
||||
# Tools:
|
||||
"CameraTool",
|
||||
@ -494,7 +485,11 @@ class CuraApplication(QtApplication):
|
||||
preferences.addPreference("view/filter_current_build_plate", 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 [
|
||||
"dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin
|
||||
@ -508,15 +503,13 @@ class CuraApplication(QtApplication):
|
||||
self.applicationShuttingDown.connect(self.saveSettings)
|
||||
self.engineCreatedSignal.connect(self._onEngineCreated)
|
||||
|
||||
self.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||
self._onGlobalContainerChanged()
|
||||
|
||||
self.getCuraSceneController().setActiveBuildPlate(0) # Initialize
|
||||
|
||||
CuraApplication.Created = True
|
||||
|
||||
def _onEngineCreated(self):
|
||||
self._qml_engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
|
||||
self._qml_engine.addImageProvider("print_job_preview", PrintJobPreviewImageProvider.PrintJobPreviewImageProvider())
|
||||
|
||||
@pyqtProperty(bool)
|
||||
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.
|
||||
def saveSettings(self):
|
||||
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
|
||||
ContainerRegistry.getInstance().saveDirtyContainers()
|
||||
self.savePreferences()
|
||||
@ -774,7 +767,10 @@ class CuraApplication(QtApplication):
|
||||
# Initialize camera
|
||||
root = controller.getScene().getRoot()
|
||||
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.lookAt(Vector(0, 0, 0))
|
||||
controller.getScene().setActiveCamera("3d")
|
||||
@ -816,20 +812,20 @@ class CuraApplication(QtApplication):
|
||||
self._machine_manager = MachineManager(self)
|
||||
return self._machine_manager
|
||||
|
||||
def getExtruderManager(self, *args):
|
||||
def getExtruderManager(self, *args) -> ExtruderManager:
|
||||
if self._extruder_manager is None:
|
||||
self._extruder_manager = ExtruderManager()
|
||||
return self._extruder_manager
|
||||
|
||||
def getVariantManager(self, *args):
|
||||
def getVariantManager(self, *args) -> VariantManager:
|
||||
return self._variant_manager
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
def getMaterialManager(self, *args):
|
||||
def getMaterialManager(self, *args) -> "MaterialManager":
|
||||
return self._material_manager
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
def getQualityManager(self, *args):
|
||||
def getQualityManager(self, *args) -> "QualityManager":
|
||||
return self._quality_manager
|
||||
|
||||
def getObjectsModel(self, *args):
|
||||
@ -838,23 +834,23 @@ class CuraApplication(QtApplication):
|
||||
return self._object_manager
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
def getMultiBuildPlateModel(self, *args):
|
||||
def getMultiBuildPlateModel(self, *args) -> MultiBuildPlateModel:
|
||||
if self._multi_build_plate_model is None:
|
||||
self._multi_build_plate_model = MultiBuildPlateModel(self)
|
||||
return self._multi_build_plate_model
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
def getBuildPlateModel(self, *args):
|
||||
def getBuildPlateModel(self, *args) -> BuildPlateModel:
|
||||
if self._build_plate_model is None:
|
||||
self._build_plate_model = BuildPlateModel(self)
|
||||
return self._build_plate_model
|
||||
|
||||
def getCuraSceneController(self, *args):
|
||||
def getCuraSceneController(self, *args) -> CuraSceneController:
|
||||
if self._cura_scene_controller is None:
|
||||
self._cura_scene_controller = CuraSceneController.createCuraSceneController()
|
||||
return self._cura_scene_controller
|
||||
|
||||
def getSettingInheritanceManager(self, *args):
|
||||
def getSettingInheritanceManager(self, *args) -> SettingInheritanceManager:
|
||||
if self._setting_inheritance_manager is None:
|
||||
self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager()
|
||||
return self._setting_inheritance_manager
|
||||
@ -908,6 +904,7 @@ class CuraApplication(QtApplication):
|
||||
engine.rootContext().setContextProperty("CuraApplication", self)
|
||||
engine.rootContext().setContextProperty("PrintInformation", self._print_information)
|
||||
engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
|
||||
engine.rootContext().setContextProperty("CuraSDKVersion", CuraSDKVersion)
|
||||
|
||||
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
|
||||
|
||||
@ -924,9 +921,9 @@ class CuraApplication(QtApplication):
|
||||
qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer")
|
||||
qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
|
||||
|
||||
qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel")
|
||||
qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel")
|
||||
qmlRegisterType(BrandMaterialsModel, "Cura", 1, 0, "BrandMaterialsModel")
|
||||
qmlRegisterType(MaterialManagementModel, "Cura", 1, 0, "MaterialManagementModel")
|
||||
qmlRegisterType(MaterialBrandsModel, "Cura", 1, 0, "MaterialBrandsModel")
|
||||
qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel")
|
||||
qmlRegisterType(MachineManagementModel, "Cura", 1, 0, "MachineManagementModel")
|
||||
|
||||
@ -942,6 +939,7 @@ class CuraApplication(QtApplication):
|
||||
qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
|
||||
qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel")
|
||||
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.
|
||||
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.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()
|
||||
activityChanged = pyqtSignal()
|
||||
sceneBoundingBoxChanged = pyqtSignal()
|
||||
preferredOutputMimetypeChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(bool, notify = activityChanged)
|
||||
def platformActivity(self):
|
||||
return self._platform_activity
|
||||
|
||||
@pyqtProperty(str, notify=preferredOutputMimetypeChanged)
|
||||
def preferredOutputMimetype(self):
|
||||
return self._preferred_mimetype
|
||||
|
||||
@pyqtProperty(str, notify = sceneBoundingBoxChanged)
|
||||
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()}
|
||||
@ -1728,4 +1710,15 @@ class CuraApplication(QtApplication):
|
||||
|
||||
@pyqtSlot()
|
||||
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
|
||||
|
||||
|
@ -21,7 +21,7 @@ class LayerPolygon:
|
||||
__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)
|
||||
|
||||
|
||||
## LayerPolygon, used in ProcessSlicedLayersJob
|
||||
# \param extruder
|
||||
# \param line_types array with line_types
|
||||
|
@ -9,9 +9,6 @@ from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
||||
from UM.Logger import Logger
|
||||
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.
|
||||
@ -24,29 +21,34 @@ if TYPE_CHECKING:
|
||||
# This is used in Variant, Material, and Quality Managers.
|
||||
#
|
||||
class ContainerNode:
|
||||
__slots__ = ("metadata", "container", "children_map")
|
||||
__slots__ = ("_metadata", "_container", "children_map")
|
||||
|
||||
def __init__(self, metadata: Optional[Dict[str, Any]] = None) -> None:
|
||||
self.metadata = metadata
|
||||
self.container = None
|
||||
self.children_map = OrderedDict() #type: OrderedDict[str, Union[QualityGroup, ContainerNode]]
|
||||
self._metadata = metadata
|
||||
self._container = None # type: Optional[InstanceContainer]
|
||||
self.children_map = OrderedDict() # type: ignore # This is because it's children are supposed to override it.
|
||||
|
||||
## Get an entry value from the metadata
|
||||
def getMetaDataEntry(self, entry: str, default: Any = None) -> Any:
|
||||
if self.metadata is None:
|
||||
if self._metadata is None:
|
||||
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"]:
|
||||
return self.children_map.get(child_key)
|
||||
|
||||
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.")
|
||||
return None
|
||||
|
||||
if self.container is None:
|
||||
container_id = self.metadata["id"]
|
||||
if self._container is None:
|
||||
container_id = self._metadata["id"]
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
container_list = ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
|
||||
if not container_list:
|
||||
@ -54,9 +56,9 @@ class ContainerNode:
|
||||
error_message = ConfigurationErrorMessage.getInstance()
|
||||
error_message.addFaultyContainers(container_id)
|
||||
return None
|
||||
self.container = container_list[0]
|
||||
self._container = container_list[0]
|
||||
|
||||
return self.container
|
||||
return self._container
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "%s[%s]" % (self.__class__.__name__, self.getMetaDataEntry("id"))
|
||||
|
@ -50,7 +50,7 @@ class MachineErrorChecker(QObject):
|
||||
self._error_check_timer.setInterval(100)
|
||||
self._error_check_timer.setSingleShot(True)
|
||||
|
||||
def initialize(self):
|
||||
def initialize(self) -> None:
|
||||
self._error_check_timer.timeout.connect(self._rescheduleCheck)
|
||||
|
||||
# Reconnect all signals when the active machine gets changed.
|
||||
@ -62,7 +62,7 @@ class MachineErrorChecker(QObject):
|
||||
|
||||
self._onMachineChanged()
|
||||
|
||||
def _onMachineChanged(self):
|
||||
def _onMachineChanged(self) -> None:
|
||||
if self._global_stack:
|
||||
self._global_stack.propertyChanged.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
|
||||
|
||||
# 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:
|
||||
self._need_to_check = True
|
||||
self.needToWaitForResultChanged.emit()
|
||||
@ -103,7 +103,7 @@ class MachineErrorChecker(QObject):
|
||||
# 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
|
||||
# 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:
|
||||
self._need_to_check = True
|
||||
self.needToWaitForResultChanged.emit()
|
||||
@ -128,7 +128,7 @@ class MachineErrorChecker(QObject):
|
||||
self._start_time = time.time()
|
||||
Logger.log("d", "New error check scheduled.")
|
||||
|
||||
def _checkStack(self):
|
||||
def _checkStack(self) -> None:
|
||||
if self._need_to_check:
|
||||
Logger.log("d", "Need to check for errors again. Discard the current progress and reschedule a check.")
|
||||
self._check_in_progress = False
|
||||
@ -169,7 +169,7 @@ class MachineErrorChecker(QObject):
|
||||
# Schedule the check for the next key
|
||||
self._application.callLater(self._checkStack)
|
||||
|
||||
def _setResult(self, result: bool):
|
||||
def _setResult(self, result: bool) -> None:
|
||||
if result != self._has_errors:
|
||||
self._has_errors = result
|
||||
self.hasErrorUpdated.emit()
|
||||
|
@ -24,8 +24,8 @@ class MaterialGroup:
|
||||
def __init__(self, name: str, root_material_node: "MaterialNode") -> None:
|
||||
self.name = name
|
||||
self.is_read_only = False
|
||||
self.root_material_node = root_material_node # type: MaterialNode
|
||||
self.derived_material_node_list = [] # type: List[MaterialNode]
|
||||
self.root_material_node = root_material_node # type: MaterialNode
|
||||
self.derived_material_node_list = [] # type: List[MaterialNode]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "%s[%s]" % (self.__class__.__name__, self.name)
|
||||
|
@ -4,8 +4,7 @@
|
||||
from collections import defaultdict, OrderedDict
|
||||
import copy
|
||||
import uuid
|
||||
from typing import Dict, cast
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from typing import Dict, Optional, TYPE_CHECKING, Any, Set, List, cast, Tuple
|
||||
|
||||
from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot
|
||||
|
||||
@ -18,10 +17,10 @@ from UM.Util import parseBool
|
||||
|
||||
from .MaterialNode import MaterialNode
|
||||
from .MaterialGroup import MaterialGroup
|
||||
from .VariantType import VariantType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from cura.Settings.ExtruderStack import ExtruderStack
|
||||
|
||||
@ -39,24 +38,34 @@ if TYPE_CHECKING:
|
||||
class MaterialManager(QObject):
|
||||
|
||||
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):
|
||||
super().__init__(parent)
|
||||
self._application = Application.getInstance()
|
||||
self._container_registry = container_registry # type: ContainerRegistry
|
||||
|
||||
self._fallback_materials_map = dict() # material_type -> generic material metadata
|
||||
self._material_group_map = dict() # root_material_id -> MaterialGroup
|
||||
self._diameter_machine_variant_material_map = dict() # approximate diameter str -> dict(machine_definition_id -> MaterialNode)
|
||||
# Material_type -> generic material metadata
|
||||
self._fallback_materials_map = dict() # type: Dict[str, Dict[str, Any]]
|
||||
|
||||
# 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
|
||||
# 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
|
||||
self._material_diameter_map = defaultdict(dict) # 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)
|
||||
# root_material_id -> approximate diameter str -> root_material_id for that diameter
|
||||
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.
|
||||
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.
|
||||
# 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.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.
|
||||
material_metadatas = {metadata["id"]: metadata for metadata in
|
||||
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
|
||||
# root_material_id -> MaterialGroup
|
||||
@ -90,7 +101,7 @@ class MaterialManager(QObject):
|
||||
if material_id == "empty_material":
|
||||
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:
|
||||
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)
|
||||
@ -106,26 +117,26 @@ class MaterialManager(QObject):
|
||||
|
||||
# Map #1.5
|
||||
# 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():
|
||||
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)
|
||||
|
||||
# Map #2
|
||||
# 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()
|
||||
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:
|
||||
grouped_by_type_dict[material_type] = {"generic": None,
|
||||
"others": []}
|
||||
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":
|
||||
to_add = True
|
||||
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:
|
||||
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 is not, then it is a custom material with a fallback material (parent)
|
||||
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)
|
||||
|
||||
# Remove the materials that have no fallback materials
|
||||
@ -151,15 +162,15 @@ class MaterialManager(QObject):
|
||||
self._diameter_material_map = dict()
|
||||
|
||||
# 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")
|
||||
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:
|
||||
key_data.append(root_material_metadata.get(key))
|
||||
key_data = tuple(key_data)
|
||||
key_data_list.append(machine_node.root_material_node.getMetaDataEntry(key))
|
||||
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 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
|
||||
if not machine_node.is_read_only:
|
||||
continue
|
||||
approximate_diameter = root_material_metadata.get("approximate_diameter")
|
||||
material_group_dict[key_data][approximate_diameter] = root_material_metadata["id"]
|
||||
approximate_diameter = machine_node.root_material_node.getMetaDataEntry("approximate_diameter", "")
|
||||
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
|
||||
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
|
||||
|
||||
# Map #4
|
||||
# "machine" -> "variant_name" -> "root material ID" -> specific material InstanceContainer
|
||||
# Construct the "machine" -> "variant" -> "root material ID" -> specific material InstanceContainer
|
||||
self._diameter_machine_variant_material_map = dict()
|
||||
# "machine" -> "nozzle name" -> "buildplate name" -> "root material ID" -> specific material InstanceContainer
|
||||
self._diameter_machine_nozzle_buildplate_material_map = dict() # type: Dict[str, Dict[str, MaterialNode]]
|
||||
for material_metadata in material_metadatas.values():
|
||||
# We don't store empty material in the lookup tables
|
||||
if material_metadata["id"] == "empty_material":
|
||||
continue
|
||||
self.__addMaterialMetadataIntoLookupTree(material_metadata)
|
||||
|
||||
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_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"])
|
||||
favorites = self._application.getPreferences().getValue("cura/favorite_materials")
|
||||
for item in favorites.split(";"):
|
||||
self._favorites.add(item)
|
||||
|
||||
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):
|
||||
Logger.log("i", "Updating material lookup data ...")
|
||||
self.initialize()
|
||||
@ -255,7 +296,7 @@ class MaterialManager(QObject):
|
||||
return self._material_diameter_map.get(root_material_id, {}).get(approximate_diameter, root_material_id)
|
||||
|
||||
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]:
|
||||
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.
|
||||
#
|
||||
def getAvailableMaterials(self, machine_definition: "DefinitionContainer", extruder_variant_name: Optional[str],
|
||||
diameter: float) -> Dict[str, MaterialNode]:
|
||||
def getAvailableMaterials(self, machine_definition: "DefinitionContainer", nozzle_name: Optional[str],
|
||||
buildplate_name: Optional[str], diameter: float) -> Dict[str, MaterialNode]:
|
||||
# round the diameter to get the approximate 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)
|
||||
return dict()
|
||||
|
||||
machine_definition_id = machine_definition.getId()
|
||||
|
||||
# If there are variant materials, get the variant material
|
||||
machine_variant_material_map = self._diameter_machine_variant_material_map[rounded_diameter]
|
||||
machine_node = machine_variant_material_map.get(machine_definition_id)
|
||||
default_machine_node = machine_variant_material_map.get(self._default_machine_definition_id)
|
||||
variant_node = None
|
||||
if extruder_variant_name is not None and machine_node is not None:
|
||||
variant_node = machine_node.getChildNode(extruder_variant_name)
|
||||
# If there are nozzle-and-or-buildplate materials, get the nozzle-and-or-buildplate material
|
||||
machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter]
|
||||
machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id)
|
||||
default_machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)
|
||||
nozzle_node = None
|
||||
buildplate_node = None
|
||||
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:
|
||||
# 1. variant-specific material
|
||||
# 2. machine-specific material
|
||||
# 3. generic material (for fdmprinter)
|
||||
# 1. buildplate-specific material
|
||||
# 2. nozzle-specific material
|
||||
# 3. machine-specific material
|
||||
# 4. generic material (for fdmprinter)
|
||||
machine_exclude_materials = machine_definition.getMetaDataEntry("exclude_materials", [])
|
||||
|
||||
material_id_metadata_dict = dict() # type: Dict[str, MaterialNode]
|
||||
for node in nodes_to_check:
|
||||
if node is not None:
|
||||
# Only exclude the materials that are explicitly specified in the "exclude_materials" field.
|
||||
# Do not exclude other materials that are of the same type.
|
||||
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
|
||||
material_id_metadata_dict = dict() # type: Dict[str, MaterialNode]
|
||||
excluded_materials = set()
|
||||
for current_node in nodes_to_check:
|
||||
if current_node is None:
|
||||
continue
|
||||
|
||||
if material_id not in material_id_metadata_dict:
|
||||
material_id_metadata_dict[material_id] = node
|
||||
# Only exclude the materials that are explicitly specified in the "exclude_materials" field.
|
||||
# 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
|
||||
|
||||
@ -309,14 +360,15 @@ class MaterialManager(QObject):
|
||||
# A convenience function to get available materials for the given machine with the extruder position.
|
||||
#
|
||||
def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack",
|
||||
extruder_stack: "ExtruderStack") -> Optional[dict]:
|
||||
variant_name = None
|
||||
extruder_stack: "ExtruderStack") -> Optional[Dict[str, MaterialNode]]:
|
||||
buildplate_name = machine.getBuildplateName()
|
||||
nozzle_name = None
|
||||
if extruder_stack.variant.getId() != "empty_variant":
|
||||
variant_name = extruder_stack.variant.getName()
|
||||
nozzle_name = extruder_stack.variant.getName()
|
||||
diameter = extruder_stack.approximateMaterialDiameter
|
||||
|
||||
# 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.
|
||||
@ -324,32 +376,36 @@ class MaterialManager(QObject):
|
||||
# 1. the given machine doesn't have materials;
|
||||
# 2. cannot find any material InstanceContainers with the given settings.
|
||||
#
|
||||
def getMaterialNode(self, machine_definition_id: str, extruder_variant_name: Optional[str],
|
||||
diameter: float, root_material_id: str) -> Optional["InstanceContainer"]:
|
||||
def getMaterialNode(self, machine_definition_id: str, nozzle_name: Optional[str],
|
||||
buildplate_name: Optional[str], diameter: float, root_material_id: str) -> Optional["MaterialNode"]:
|
||||
# round the diameter to get the approximate 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]",
|
||||
diameter, rounded_diameter, root_material_id)
|
||||
return None
|
||||
|
||||
# If there are variant materials, get the variant material
|
||||
machine_variant_material_map = self._diameter_machine_variant_material_map[rounded_diameter]
|
||||
machine_node = machine_variant_material_map.get(machine_definition_id)
|
||||
variant_node = None
|
||||
# If there are nozzle materials, get the nozzle-specific material
|
||||
machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter] # type: Dict[str, MaterialNode]
|
||||
machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id)
|
||||
nozzle_node = None
|
||||
buildplate_node = None
|
||||
|
||||
# Fallback for "fdmprinter" if the machine-specific materials cannot be found
|
||||
if machine_node is None:
|
||||
machine_node = machine_variant_material_map.get(self._default_machine_definition_id)
|
||||
if machine_node is not None and extruder_variant_name is not None:
|
||||
variant_node = machine_node.getChildNode(extruder_variant_name)
|
||||
machine_node = machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)
|
||||
if machine_node is not None and nozzle_name is not None:
|
||||
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:
|
||||
# 1. variant-specific material
|
||||
# 2. machine-specific material
|
||||
# 3. generic material (for fdmprinter)
|
||||
nodes_to_check = [variant_node, machine_node,
|
||||
machine_variant_material_map.get(self._default_machine_definition_id)]
|
||||
# 1. buildplate-specific material
|
||||
# 2. nozzle-specific material
|
||||
# 3. machine-specific material
|
||||
# 4. generic material (for fdmprinter)
|
||||
nodes_to_check = [buildplate_node, nozzle_node, machine_node,
|
||||
machine_nozzle_buildplate_material_map.get(self._default_machine_definition_id)]
|
||||
|
||||
material_node = None
|
||||
for node in nodes_to_check:
|
||||
@ -366,7 +422,8 @@ class MaterialManager(QObject):
|
||||
# 1. the given machine doesn't have materials;
|
||||
# 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
|
||||
machine_definition = global_stack.definition
|
||||
extruder_definition = global_stack.extruders[position].definition
|
||||
@ -378,14 +435,14 @@ class MaterialManager(QObject):
|
||||
# Look at the guid to material dictionary
|
||||
root_material_id = None
|
||||
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
|
||||
|
||||
if not root_material_id:
|
||||
Logger.log("i", "Cannot find materials with guid [%s] ", material_guid)
|
||||
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)
|
||||
return node
|
||||
|
||||
@ -413,13 +470,17 @@ class MaterialManager(QObject):
|
||||
else:
|
||||
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)
|
||||
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
|
||||
|
||||
buildplate_name = global_stack.getBuildplateName()
|
||||
machine_definition = global_stack.definition
|
||||
if extruder_definition is None:
|
||||
extruder_definition = global_stack.extruders[position].definition
|
||||
|
||||
if extruder_definition and parseBool(global_stack.getMetaDataEntry("has_materials", False)):
|
||||
# At this point the extruder_definition is not None
|
||||
material_diameter = extruder_definition.getProperty("material_diameter", "value")
|
||||
@ -428,7 +489,7 @@ class MaterialManager(QObject):
|
||||
approximate_material_diameter = str(round(material_diameter))
|
||||
root_material_id = machine_definition.getMetaDataEntry("preferred_material")
|
||||
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)
|
||||
return node
|
||||
|
||||
@ -450,7 +511,7 @@ class MaterialManager(QObject):
|
||||
# Sets the new name for the given material.
|
||||
#
|
||||
@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")
|
||||
if root_material_id is None:
|
||||
return
|
||||
@ -468,7 +529,7 @@ class MaterialManager(QObject):
|
||||
# Removes the given material.
|
||||
#
|
||||
@pyqtSlot("QVariant")
|
||||
def removeMaterial(self, material_node: "MaterialNode"):
|
||||
def removeMaterial(self, material_node: "MaterialNode") -> None:
|
||||
root_material_id = material_node.getMetaDataEntry("base_file")
|
||||
if root_material_id is not None:
|
||||
self.removeMaterialByRootId(root_material_id)
|
||||
@ -478,8 +539,8 @@ class MaterialManager(QObject):
|
||||
# Returns the root material ID of the duplicated material if successful.
|
||||
#
|
||||
@pyqtSlot("QVariant", result = str)
|
||||
def duplicateMaterial(self, material_node, new_base_id = None, new_metadata = None) -> Optional[str]:
|
||||
root_material_id = material_node.metadata["base_file"]
|
||||
def duplicateMaterial(self, material_node: MaterialNode, new_base_id: Optional[str] = None, new_metadata: Dict[str, Any] = None) -> Optional[str]:
|
||||
root_material_id = cast(str, material_node.getMetaDataEntry("base_file", ""))
|
||||
|
||||
material_group = self.getMaterialGroup(root_material_id)
|
||||
if not material_group:
|
||||
@ -515,8 +576,8 @@ class MaterialManager(QObject):
|
||||
if container_to_copy.getMetaDataEntry("definition") != "fdmprinter":
|
||||
new_id += "_" + container_to_copy.getMetaDataEntry("definition")
|
||||
if container_to_copy.getMetaDataEntry("variant_name"):
|
||||
variant_name = container_to_copy.getMetaDataEntry("variant_name")
|
||||
new_id += "_" + variant_name.replace(" ", "_")
|
||||
nozzle_name = container_to_copy.getMetaDataEntry("variant_name")
|
||||
new_id += "_" + nozzle_name.replace(" ", "_")
|
||||
|
||||
new_container = copy.deepcopy(container_to_copy)
|
||||
new_container.getMetaData()["id"] = new_id
|
||||
@ -530,11 +591,17 @@ class MaterialManager(QObject):
|
||||
for container_to_add in new_containers:
|
||||
container_to_add.setDirty(True)
|
||||
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
|
||||
|
||||
#
|
||||
# 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)
|
||||
def createMaterial(self) -> str:
|
||||
from UM.i18n import i18nCatalog
|
||||
@ -565,3 +632,25 @@ class MaterialManager(QObject):
|
||||
new_base_id = new_id,
|
||||
new_metadata = new_metadata)
|
||||
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
|
@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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
|
||||
|
||||
|
||||
@ -14,6 +14,12 @@ from .ContainerNode import ContainerNode
|
||||
class MaterialNode(ContainerNode):
|
||||
__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)
|
||||
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)
|
@ -2,45 +2,60 @@
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Qt.ListModel import ListModel
|
||||
|
||||
|
||||
#
|
||||
# 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.
|
||||
# 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
|
||||
#
|
||||
## This is the base model class for GenericMaterialsModel and MaterialBrandsModel.
|
||||
# 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
|
||||
# bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu
|
||||
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()
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
self._application = Application.getInstance()
|
||||
self._machine_manager = self._application.getMachineManager()
|
||||
|
||||
self.addRoleName(self.RootMaterialIdRole, "root_material_id")
|
||||
self.addRoleName(self.IdRole, "id")
|
||||
self.addRoleName(self.NameRole, "name")
|
||||
self.addRoleName(self.BrandRole, "brand")
|
||||
self.addRoleName(self.MaterialRole, "material")
|
||||
self.addRoleName(self.ColorRole, "color_name")
|
||||
self.addRoleName(self.ContainerNodeRole, "container_node")
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
self._application = CuraApplication.getInstance()
|
||||
|
||||
# Make these managers available to all material models
|
||||
self._container_registry = self._application.getInstance().getContainerRegistry()
|
||||
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_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):
|
||||
global_stack = self._machine_manager.activeMachine
|
||||
@ -65,8 +80,55 @@ class BaseMaterialsModel(ListModel):
|
||||
def extruderPosition(self) -> int:
|
||||
return self._extruder_position
|
||||
|
||||
#
|
||||
# This is an abstract method that needs to be implemented by
|
||||
#
|
||||
## This is an abstract method that needs to be implemented by the specific
|
||||
# models themselves.
|
||||
def _update(self):
|
||||
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
|
||||
|
||||
|
@ -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)
|
@ -8,7 +8,7 @@ from UM.Logger import Logger
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Util import parseBool
|
||||
|
||||
from cura.Machines.VariantManager import VariantType
|
||||
from cura.Machines.VariantType import VariantType
|
||||
|
||||
|
||||
class BuildPlateModel(ListModel):
|
||||
|
42
cura/Machines/Models/FavoriteMaterialsModel.py
Normal file
42
cura/Machines/Models/FavoriteMaterialsModel.py
Normal 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)
|
@ -4,63 +4,39 @@
|
||||
from UM.Logger import Logger
|
||||
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
|
||||
|
||||
|
||||
class GenericMaterialsModel(BaseMaterialsModel):
|
||||
|
||||
def __init__(self, parent = None):
|
||||
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()
|
||||
|
||||
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:
|
||||
# Perform standard check and reset if the check fails
|
||||
if not self._canUpdate():
|
||||
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[extruder_position]
|
||||
|
||||
available_material_dict = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack,
|
||||
extruder_stack)
|
||||
if available_material_dict is None:
|
||||
self.setItems([])
|
||||
return
|
||||
# Get updated list of favorites
|
||||
self._favorite_ids = self._material_manager.getFavorites()
|
||||
|
||||
item_list = []
|
||||
for root_material_id, container_node in available_material_dict.items():
|
||||
metadata = container_node.metadata
|
||||
|
||||
# Only add results for generic materials
|
||||
if metadata["brand"].lower() != "generic":
|
||||
continue
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
# Only add results for generic materials
|
||||
if metadata["brand"].lower() != "generic":
|
||||
continue
|
||||
|
||||
item = self._createMaterialItem(root_material_id, container_node)
|
||||
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())
|
||||
|
||||
self.setItems(item_list)
|
||||
|
107
cura/Machines/Models/MaterialBrandsModel.py
Normal file
107
cura/Machines/Models/MaterialBrandsModel.py
Normal 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)
|
@ -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)
|
@ -8,6 +8,8 @@ from UM.Logger import Logger
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Util import parseBool
|
||||
|
||||
from cura.Machines.VariantType import VariantType
|
||||
|
||||
|
||||
class NozzleModel(ListModel):
|
||||
IdRole = Qt.UserRole + 1
|
||||
@ -43,7 +45,6 @@ class NozzleModel(ListModel):
|
||||
self.setItems([])
|
||||
return
|
||||
|
||||
from cura.Machines.VariantManager import VariantType
|
||||
variant_node_dict = self._variant_manager.getVariantNodes(global_stack, VariantType.NOZZLE)
|
||||
if not variant_node_dict:
|
||||
self.setItems([])
|
||||
|
@ -6,10 +6,10 @@ from PyQt5.QtCore import Qt
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
|
||||
from cura.Machines.QualityManager import QualityGroup
|
||||
|
||||
|
||||
#
|
||||
# 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
|
||||
if container and container.hasProperty("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)
|
||||
|
@ -58,7 +58,7 @@ class SettingVisibilityPresetsModel(ListModel):
|
||||
break
|
||||
return result
|
||||
|
||||
def _populate(self):
|
||||
def _populate(self) -> None:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
items = []
|
||||
for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
|
||||
@ -147,7 +147,7 @@ class SettingVisibilityPresetsModel(ListModel):
|
||||
def activePreset(self) -> str:
|
||||
return self._active_preset_item["id"]
|
||||
|
||||
def _onPreferencesChanged(self, name: str):
|
||||
def _onPreferencesChanged(self, name: str) -> None:
|
||||
if name != "general/visible_settings":
|
||||
return
|
||||
|
||||
|
@ -17,16 +17,16 @@ class QualityChangesGroup(QualityGroup):
|
||||
super().__init__(name, quality_type, parent)
|
||||
self._container_registry = Application.getInstance().getContainerRegistry()
|
||||
|
||||
def addNode(self, node: "QualityNode"):
|
||||
def addNode(self, node: "QualityNode") -> None:
|
||||
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.
|
||||
ConfigurationErrorMessage.getInstance().addFaultyContainers(node.getMetaDataEntry("id"))
|
||||
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
|
||||
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
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
@ -6,6 +6,7 @@ from typing import Dict, Optional, List, Set
|
||||
from PyQt5.QtCore import QObject, pyqtSlot
|
||||
from cura.Machines.ContainerNode import ContainerNode
|
||||
|
||||
|
||||
#
|
||||
# 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
|
||||
@ -34,7 +35,7 @@ class QualityGroup(QObject):
|
||||
return self.name
|
||||
|
||||
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()):
|
||||
if node is None:
|
||||
continue
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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
|
||||
|
||||
@ -20,6 +20,8 @@ if TYPE_CHECKING:
|
||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from .QualityChangesGroup import QualityChangesGroup
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
|
||||
|
||||
#
|
||||
@ -36,17 +38,20 @@ class QualityManager(QObject):
|
||||
|
||||
qualitiesUpdated = pyqtSignal()
|
||||
|
||||
def __init__(self, container_registry, parent = None):
|
||||
def __init__(self, container_registry: "ContainerRegistry", parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._application = Application.getInstance()
|
||||
self._application = Application.getInstance() # type: CuraApplication
|
||||
self._material_manager = self._application.getMaterialManager()
|
||||
self._container_registry = container_registry
|
||||
|
||||
self._empty_quality_container = self._application.empty_quality_container
|
||||
self._empty_quality_changes_container = self._application.empty_quality_changes_container
|
||||
|
||||
self._machine_variant_material_quality_type_to_quality_dict = {} # for quality lookup
|
||||
self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup
|
||||
# For quality 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"
|
||||
|
||||
@ -62,12 +67,12 @@ class QualityManager(QObject):
|
||||
self._update_timer.setSingleShot(True)
|
||||
self._update_timer.timeout.connect(self._updateMaps)
|
||||
|
||||
def initialize(self):
|
||||
def initialize(self) -> None:
|
||||
# Initialize the lookup tree for quality profiles with following structure:
|
||||
# <machine> -> <variant> -> <material>
|
||||
# -> <material>
|
||||
# <machine> -> <nozzle> -> <buildplate> -> <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
|
||||
|
||||
quality_metadata_list = self._container_registry.findContainersMetadata(type = "quality")
|
||||
@ -79,53 +84,41 @@ class QualityManager(QObject):
|
||||
quality_type = metadata["quality_type"]
|
||||
|
||||
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 = 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
|
||||
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"])
|
||||
continue
|
||||
|
||||
if definition_id not in self._machine_variant_material_quality_type_to_quality_dict:
|
||||
self._machine_variant_material_quality_type_to_quality_dict[definition_id] = QualityNode()
|
||||
machine_node = cast(QualityNode, self._machine_variant_material_quality_type_to_quality_dict[definition_id])
|
||||
if definition_id not in self._machine_nozzle_buildplate_material_quality_type_to_quality_dict:
|
||||
self._machine_nozzle_buildplate_material_quality_type_to_quality_dict[definition_id] = QualityNode()
|
||||
machine_node = cast(QualityNode, self._machine_nozzle_buildplate_material_quality_type_to_quality_dict[definition_id])
|
||||
|
||||
if is_global_quality:
|
||||
# For global qualities, save data in the machine node
|
||||
machine_node.addQualityMetadata(quality_type, metadata)
|
||||
continue
|
||||
|
||||
if variant_name is not None:
|
||||
# If variant_name is specified in the quality/quality_changes profile, check if material is specified,
|
||||
# too.
|
||||
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])
|
||||
current_node = machine_node
|
||||
intermediate_node_info_list = [nozzle_name, buildplate_name, root_material_id]
|
||||
current_intermediate_node_info_idx = 0
|
||||
|
||||
if root_material_id is None:
|
||||
# If only variant_name is specified but material is not, add the quality/quality_changes metadata
|
||||
# into the current variant node.
|
||||
variant_node.addQualityMetadata(quality_type, metadata)
|
||||
else:
|
||||
# If only variant_name and material are both specified, go one level deeper: create a material node
|
||||
# under the current variant node, and then add the quality/quality_changes metadata into the
|
||||
# material node.
|
||||
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])
|
||||
while current_intermediate_node_info_idx < len(intermediate_node_info_list):
|
||||
node_name = intermediate_node_info_list[current_intermediate_node_info_idx]
|
||||
if node_name is not None:
|
||||
# There is specific information, update the current node to go deeper so we can add this quality
|
||||
# at the most specific branch in the lookup tree.
|
||||
if node_name not in current_node.children_map:
|
||||
current_node.children_map[node_name] = QualityNode()
|
||||
current_node = cast(QualityNode, current_node.children_map[node_name])
|
||||
|
||||
material_node.addQualityMetadata(quality_type, metadata)
|
||||
current_intermediate_node_info_idx += 1
|
||||
|
||||
else:
|
||||
# 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)
|
||||
current_node.addQualityMetadata(quality_type, metadata)
|
||||
|
||||
# Initialize the lookup tree for quality_changes profiles with following structure:
|
||||
# <machine> -> <quality_type> -> <name>
|
||||
@ -145,13 +138,13 @@ class QualityManager(QObject):
|
||||
Logger.log("d", "Lookup tables updated.")
|
||||
self.qualitiesUpdated.emit()
|
||||
|
||||
def _updateMaps(self):
|
||||
def _updateMaps(self) -> None:
|
||||
self.initialize()
|
||||
|
||||
def _onContainerMetadataChanged(self, container):
|
||||
def _onContainerMetadataChanged(self, container: InstanceContainer) -> None:
|
||||
self._onContainerChanged(container)
|
||||
|
||||
def _onContainerChanged(self, container):
|
||||
def _onContainerChanged(self, container: InstanceContainer) -> None:
|
||||
container_type = container.getMetaDataEntry("type")
|
||||
if container_type not in ("quality", "quality_changes"):
|
||||
return
|
||||
@ -160,7 +153,7 @@ class QualityManager(QObject):
|
||||
self._update_timer.start()
|
||||
|
||||
# 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()
|
||||
for i in range(machine.getProperty("machine_extruder_count", "value")):
|
||||
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.
|
||||
# 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)
|
||||
|
||||
# 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:
|
||||
# (1) the machine-specific node
|
||||
# (2) the generic node
|
||||
machine_node = self._machine_variant_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)
|
||||
nodes_to_check = [machine_node, default_machine_node]
|
||||
machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id)
|
||||
# Check if this machine has specific quality profiles for its extruders, if so, when looking up extruder
|
||||
# qualities, we should not fall back to use the global qualities.
|
||||
has_extruder_specific_qualities = False
|
||||
if machine_node:
|
||||
if machine_node.children_map:
|
||||
has_extruder_specific_qualities = True
|
||||
|
||||
default_machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(self._default_machine_definition_id)
|
||||
|
||||
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
|
||||
quality_group_dict = {}
|
||||
for node in nodes_to_check:
|
||||
if node and node.quality_type_map:
|
||||
# Only include global qualities
|
||||
if has_variant_materials:
|
||||
quality_node = list(node.quality_type_map.values())[0]
|
||||
is_global_quality = parseBool(quality_node.metadata.get("global_quality", False))
|
||||
if not is_global_quality:
|
||||
continue
|
||||
quality_node = list(node.quality_type_map.values())[0]
|
||||
is_global_quality = parseBool(quality_node.getMetaDataEntry("global_quality", False))
|
||||
if not is_global_quality:
|
||||
continue
|
||||
|
||||
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_dict[quality_type] = quality_group
|
||||
break
|
||||
|
||||
buildplate_name = machine.getBuildplateName()
|
||||
|
||||
# Iterate over all extruders to find quality containers for each extruder
|
||||
for position, extruder in machine.extruders.items():
|
||||
variant_name = None
|
||||
nozzle_name = None
|
||||
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.
|
||||
# The root material IDs in this list are in prioritized order.
|
||||
root_material_id_list = []
|
||||
has_material = False # flag indicating whether this extruder has a material assigned
|
||||
root_material_id = None
|
||||
if extruder.material.getId() != "empty_material":
|
||||
has_material = True
|
||||
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.
|
||||
# The use case is that, when we look for qualities for a machine, we first want to search in the following
|
||||
# order:
|
||||
# 1. machine-variant-and-material-specific qualities if exist
|
||||
# 2. machine-variant-specific qualities if exist
|
||||
# 3. machine-material-specific qualities if exist
|
||||
# 4. machine-specific qualities if exist
|
||||
# 5. generic qualities if exist
|
||||
# 1. machine-nozzle-buildplate-and-material-specific qualities if exist
|
||||
# 2. machine-nozzle-and-material-specific qualities if exist
|
||||
# 3. machine-nozzle-specific qualities if exist
|
||||
# 4. machine-material-specific 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
|
||||
# the list with priorities as the order. Later, we just need to loop over each node in this list and fetch
|
||||
# qualities from there.
|
||||
node_info_list_0 = [nozzle_name, buildplate_name, root_material_id] # type: List[Optional[str]]
|
||||
nodes_to_check = []
|
||||
|
||||
if variant_name:
|
||||
# In this case, we have both a specific variant and a specific material
|
||||
variant_node = machine_node.getChildNode(variant_name)
|
||||
if variant_node and has_material:
|
||||
for root_material_id in root_material_id_list:
|
||||
material_node = variant_node.getChildNode(root_material_id)
|
||||
# This function tries to recursively find the deepest (the most specific) branch and add those nodes to
|
||||
# the search list in the order described above. So, by iterating over that search node list, we first look
|
||||
# in the more specific branches and then the less specific (generic) ones.
|
||||
def addNodesToCheck(node: Optional[QualityNode], nodes_to_check_list: List[QualityNode], node_info_list, node_info_idx: int) -> None:
|
||||
if node is None:
|
||||
return
|
||||
|
||||
if node_info_idx < len(node_info_list):
|
||||
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:
|
||||
nodes_to_check.append(material_node)
|
||||
nodes_to_check_list.append(material_node)
|
||||
break
|
||||
nodes_to_check.append(variant_node)
|
||||
|
||||
# In this case, we only have a specific material but NOT a variant
|
||||
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_list.append(node)
|
||||
|
||||
nodes_to_check += [machine_node, default_machine_node]
|
||||
for node in nodes_to_check:
|
||||
addNodesToCheck(machine_node, nodes_to_check, node_info_list_0, 0)
|
||||
|
||||
# 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 has_variant_materials:
|
||||
if has_extruder_specific_qualities:
|
||||
# Only include variant qualities; skip non global qualities
|
||||
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:
|
||||
continue
|
||||
|
||||
for quality_type, quality_node in node.quality_type_map.items():
|
||||
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 = quality_group_dict[quality_type]
|
||||
quality_group.nodes_for_extruders[position] = quality_node
|
||||
break
|
||||
if position not in quality_group.nodes_for_extruders:
|
||||
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
|
||||
self._updateQualityGroupsAvailability(machine, quality_group_dict.values())
|
||||
|
||||
return quality_group_dict
|
||||
|
||||
def getQualityGroupsForMachineDefinition(self, machine: "GlobalStack") -> dict:
|
||||
def getQualityGroupsForMachineDefinition(self, machine: "GlobalStack") -> Dict[str, QualityGroup]:
|
||||
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
|
||||
|
||||
# To find the quality container for the GlobalStack, check in the following fall-back manner:
|
||||
# (1) the machine-specific node
|
||||
# (2) the generic node
|
||||
machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(machine_definition_id)
|
||||
default_machine_node = self._machine_variant_material_quality_type_to_quality_dict.get(
|
||||
machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id)
|
||||
default_machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(
|
||||
self._default_machine_definition_id)
|
||||
nodes_to_check = [machine_node, default_machine_node]
|
||||
|
||||
@ -333,7 +364,7 @@ class QualityManager(QObject):
|
||||
for node in nodes_to_check:
|
||||
if node and node.quality_type_map:
|
||||
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_dict[quality_type] = quality_group
|
||||
break
|
||||
@ -355,10 +386,21 @@ class QualityManager(QObject):
|
||||
# Remove the given quality changes group.
|
||||
#
|
||||
@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)
|
||||
removed_quality_changes_ids = set()
|
||||
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.
|
||||
@ -387,7 +429,7 @@ class QualityManager(QObject):
|
||||
# Duplicates the given quality.
|
||||
#
|
||||
@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()
|
||||
if not global_stack:
|
||||
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
|
||||
# stack and clear the user settings.
|
||||
@pyqtSlot(str)
|
||||
def createQualityChanges(self, base_name):
|
||||
def createQualityChanges(self, base_name: str) -> None:
|
||||
machine_manager = Application.getInstance().getMachineManager()
|
||||
|
||||
global_stack = machine_manager.activeMachine
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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 .QualityChangesGroup import QualityChangesGroup
|
||||
@ -12,18 +12,21 @@ from .QualityChangesGroup import QualityChangesGroup
|
||||
#
|
||||
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)
|
||||
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:
|
||||
self.quality_type_map[quality_type] = QualityNode(metadata)
|
||||
|
||||
def getQualityNode(self, quality_type: str) -> Optional["QualityNode"]:
|
||||
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:
|
||||
self.quality_type_map[quality_type] = QualityNode()
|
||||
quality_type_node = self.quality_type_map[quality_type]
|
||||
|
@ -1,9 +1,8 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from enum import Enum
|
||||
from collections import OrderedDict
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from typing import Optional, TYPE_CHECKING, Dict
|
||||
|
||||
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
||||
from UM.Logger import Logger
|
||||
@ -11,20 +10,13 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Util import parseBool
|
||||
|
||||
from cura.Machines.ContainerNode import ContainerNode
|
||||
from cura.Machines.VariantType import VariantType, ALL_VARIANT_TYPES
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
||||
if TYPE_CHECKING:
|
||||
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
|
||||
# structure:
|
||||
@ -44,11 +36,11 @@ ALL_VARIANT_TYPES = (VariantType.BUILD_PLATE, VariantType.NOZZLE)
|
||||
#
|
||||
class VariantManager:
|
||||
|
||||
def __init__(self, container_registry):
|
||||
self._container_registry = container_registry # type: ContainerRegistry
|
||||
def __init__(self, container_registry: ContainerRegistry) -> None:
|
||||
self._container_registry = container_registry
|
||||
|
||||
self._machine_to_variant_dict_map = dict() # <machine_type> -> <variant_dict>
|
||||
self._machine_to_buildplate_dict_map = dict()
|
||||
self._machine_to_variant_dict_map = dict() # type: Dict[str, Dict["VariantType", Dict[str, ContainerNode]]]
|
||||
self._machine_to_buildplate_dict_map = dict() # type: Dict[str, Dict[str, ContainerNode]]
|
||||
|
||||
self._exclude_variant_id_list = ["empty_variant"]
|
||||
|
||||
@ -56,7 +48,7 @@ class VariantManager:
|
||||
# Initializes the VariantManager including:
|
||||
# - 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_buildplate_dict_map = OrderedDict()
|
||||
|
||||
@ -114,10 +106,10 @@ class VariantManager:
|
||||
variant_node = variant_dict[variant_name]
|
||||
break
|
||||
return variant_node
|
||||
|
||||
return self._machine_to_variant_dict_map[machine_definition_id].get(variant_type, {}).get(variant_name)
|
||||
|
||||
def getVariantNodes(self, machine: "GlobalStack",
|
||||
variant_type: Optional["VariantType"] = None) -> dict:
|
||||
def getVariantNodes(self, machine: "GlobalStack", variant_type: "VariantType") -> Dict[str, ContainerNode]:
|
||||
machine_definition_id = machine.definition.getId()
|
||||
return self._machine_to_variant_dict_map.get(machine_definition_id, {}).get(variant_type, {})
|
||||
|
||||
|
15
cura/Machines/VariantType.py
Normal file
15
cura/Machines/VariantType.py
Normal 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"]
|
@ -9,6 +9,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.i18n import i18nCatalog
|
||||
from collections import defaultdict
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
@ -40,6 +41,8 @@ class ObjectsModel(ListModel):
|
||||
filter_current_build_plate = Application.getInstance().getPreferences().getValue("view/filter_current_build_plate")
|
||||
active_build_plate_number = self._build_plate_number
|
||||
group_nr = 1
|
||||
name_count_dict = defaultdict(int)
|
||||
|
||||
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
|
||||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
@ -55,6 +58,7 @@ class ObjectsModel(ListModel):
|
||||
|
||||
if not node.callDecoration("isGroup"):
|
||||
name = node.getName()
|
||||
|
||||
else:
|
||||
name = catalog.i18nc("@label", "Group #{group_nr}").format(group_nr = str(group_nr))
|
||||
group_nr += 1
|
||||
@ -63,6 +67,14 @@ class ObjectsModel(ListModel):
|
||||
is_outside_build_area = node.isOutsideBuildArea()
|
||||
else:
|
||||
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({
|
||||
"name": name,
|
||||
@ -71,6 +83,7 @@ class ObjectsModel(ListModel):
|
||||
"buildPlateNumber": node_build_plate_number,
|
||||
"node": node
|
||||
})
|
||||
|
||||
nodes = sorted(nodes, key=lambda n: n["name"])
|
||||
self.setItems(nodes)
|
||||
|
||||
|
@ -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.
|
||||
|
||||
from UM.Scene.Iterator import Iterator
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from functools import cmp_to_key
|
||||
from UM.Application import Application
|
||||
import sys
|
||||
|
||||
from shapely import affinity
|
||||
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):
|
||||
super().__init__(scene_node) # Call super to make multiple inheritence work.
|
||||
self._hit_map = [[]]
|
||||
from cura.CuraApplication import CuraApplication
|
||||
self._global_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
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):
|
||||
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 = []
|
||||
for node in self._scene_node.getChildren():
|
||||
if not issubclass(type(node), SceneNode):
|
||||
continue
|
||||
|
||||
if node.callDecoration("getConvexHull"):
|
||||
node_list.append(node)
|
||||
convex_hull = node.callDecoration("getConvexHull")
|
||||
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:
|
||||
self._node_stack = node_list[:]
|
||||
return
|
||||
node_list.append({"node": node,
|
||||
"min_coord": [convex_hull_polygon.bounds[0], convex_hull_polygon.bounds[1]],
|
||||
})
|
||||
|
||||
# Copy the list
|
||||
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
|
||||
node_list = sorted(node_list, key = lambda d: d["min_coord"])
|
||||
|
||||
self._node_stack = [d["node"] for d in node_list]
|
||||
|
@ -10,7 +10,6 @@ from typing import Dict
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Qt.Duration import Duration
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
@ -52,6 +51,8 @@ class PrintInformation(QObject):
|
||||
super().__init__(parent)
|
||||
self._application = application
|
||||
|
||||
self.UNTITLED_JOB_NAME = "Untitled"
|
||||
|
||||
self.initializeCuraMessagePrintTimeProperties()
|
||||
|
||||
self._material_lengths = {} # indexed by build plate number
|
||||
@ -70,12 +71,13 @@ class PrintInformation(QObject):
|
||||
self._base_name = ""
|
||||
self._abbr_machine = ""
|
||||
self._job_name = ""
|
||||
self._project_name = ""
|
||||
self._active_build_plate = 0
|
||||
self._initVariablesWithBuildPlate(self._active_build_plate)
|
||||
|
||||
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.setToZeroPrintInformation)
|
||||
self._application.fileLoaded.connect(self.setBaseName)
|
||||
@ -300,13 +302,13 @@ class PrintInformation(QObject):
|
||||
|
||||
def _updateJobName(self):
|
||||
if self._base_name == "":
|
||||
self._job_name = "unnamed"
|
||||
self._job_name = self.UNTITLED_JOB_NAME
|
||||
self._is_user_specified_job_name = False
|
||||
self.jobNameChanged.emit()
|
||||
return
|
||||
|
||||
base_name = self._stripAccents(self._base_name)
|
||||
self._setAbbreviatedMachineName()
|
||||
self._defineAbbreviatedMachineName()
|
||||
|
||||
# Only update the job name when it's not user-specified.
|
||||
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
|
||||
# active machine name.
|
||||
# Called each time the global stack is switched.
|
||||
def _setAbbreviatedMachineName(self):
|
||||
def _defineAbbreviatedMachineName(self):
|
||||
global_container_stack = self._application.getGlobalContainerStack()
|
||||
if not global_container_stack:
|
||||
self._abbr_machine = ""
|
||||
|
27
cura/PrintJobPreviewImageProvider.py
Normal file
27
cura/PrintJobPreviewImageProvider.py
Normal 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)
|
@ -13,36 +13,42 @@ class ConfigurationModel(QObject):
|
||||
|
||||
configurationChanged = pyqtSignal()
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._printer_type = None
|
||||
self._printer_type = ""
|
||||
self._extruder_configurations = [] # type: List[ExtruderConfigurationModel]
|
||||
self._buildplate_configuration = None
|
||||
self._buildplate_configuration = ""
|
||||
|
||||
def setPrinterType(self, printer_type):
|
||||
self._printer_type = printer_type
|
||||
|
||||
@pyqtProperty(str, fset = setPrinterType, notify = configurationChanged)
|
||||
def printerType(self):
|
||||
def printerType(self) -> str:
|
||||
return self._printer_type
|
||||
|
||||
def setExtruderConfigurations(self, extruder_configurations):
|
||||
self._extruder_configurations = extruder_configurations
|
||||
def setExtruderConfigurations(self, extruder_configurations: List["ExtruderConfigurationModel"]):
|
||||
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)
|
||||
def extruderConfigurations(self):
|
||||
return self._extruder_configurations
|
||||
|
||||
def setBuildplateConfiguration(self, buildplate_configuration):
|
||||
def setBuildplateConfiguration(self, buildplate_configuration: str) -> None:
|
||||
self._buildplate_configuration = buildplate_configuration
|
||||
|
||||
@pyqtProperty(str, fset = setBuildplateConfiguration, notify = configurationChanged)
|
||||
def buildplateConfiguration(self):
|
||||
def buildplateConfiguration(self) -> str:
|
||||
return self._buildplate_configuration
|
||||
|
||||
## This method is intended to indicate whether the configuration is valid or not.
|
||||
# The method checks if the mandatory fields are or not set
|
||||
def isValid(self):
|
||||
def isValid(self) -> bool:
|
||||
if not self._extruder_configurations:
|
||||
return False
|
||||
for configuration in self._extruder_configurations:
|
||||
|
@ -1,56 +1,67 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal
|
||||
|
||||
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
||||
|
||||
|
||||
class ExtruderConfigurationModel(QObject):
|
||||
|
||||
extruderConfigurationChanged = pyqtSignal()
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, position: int = -1) -> None:
|
||||
super().__init__()
|
||||
self._position = -1
|
||||
self._material = None
|
||||
self._hotend_id = None
|
||||
self._position = position # type: int
|
||||
self._material = None # type: Optional[MaterialOutputModel]
|
||||
self._hotend_id = None # type: Optional[str]
|
||||
|
||||
def setPosition(self, position):
|
||||
def setPosition(self, position: int) -> None:
|
||||
self._position = position
|
||||
|
||||
@pyqtProperty(int, fset = setPosition, notify = extruderConfigurationChanged)
|
||||
def position(self):
|
||||
def position(self) -> int:
|
||||
return self._position
|
||||
|
||||
def setMaterial(self, material):
|
||||
self._material = material
|
||||
def setMaterial(self, material: Optional[MaterialOutputModel]) -> None:
|
||||
if self._hotend_id != material:
|
||||
self._material = material
|
||||
self.extruderConfigurationChanged.emit()
|
||||
|
||||
@pyqtProperty(QObject, fset = setMaterial, notify = extruderConfigurationChanged)
|
||||
def material(self):
|
||||
def activeMaterial(self) -> Optional[MaterialOutputModel]:
|
||||
return self._material
|
||||
|
||||
def setHotendID(self, hotend_id):
|
||||
self._hotend_id = hotend_id
|
||||
@pyqtProperty(QObject, fset=setMaterial, notify=extruderConfigurationChanged)
|
||||
def material(self) -> Optional[MaterialOutputModel]:
|
||||
return self._material
|
||||
|
||||
def setHotendID(self, hotend_id: Optional[str]) -> None:
|
||||
if self._hotend_id != hotend_id:
|
||||
self._hotend_id = hotend_id
|
||||
self.extruderConfigurationChanged.emit()
|
||||
|
||||
@pyqtProperty(str, fset = setHotendID, notify = extruderConfigurationChanged)
|
||||
def hotendID(self):
|
||||
def hotendID(self) -> Optional[str]:
|
||||
return self._hotend_id
|
||||
|
||||
## This method is intended to indicate whether the configuration is valid or not.
|
||||
# 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.
|
||||
def isValid(self):
|
||||
def isValid(self) -> bool:
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
def __str__(self) -> str:
|
||||
message_chunks = []
|
||||
message_chunks.append("Position: " + str(self._position))
|
||||
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("HotendID: " + self.hotendID if self.hotendID else "empty")
|
||||
return " ".join(message_chunks)
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other) -> bool:
|
||||
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
|
||||
|
@ -12,64 +12,61 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class ExtruderOutputModel(QObject):
|
||||
hotendIDChanged = pyqtSignal()
|
||||
targetHotendTemperatureChanged = pyqtSignal()
|
||||
hotendTemperatureChanged = pyqtSignal()
|
||||
activeMaterialChanged = pyqtSignal()
|
||||
|
||||
extruderConfigurationChanged = 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)
|
||||
self._printer = printer
|
||||
self._printer = printer # type: PrinterOutputModel
|
||||
self._position = position
|
||||
self._target_hotend_temperature = 0 # type: float
|
||||
self._hotend_temperature = 0 # type: float
|
||||
self._hotend_id = ""
|
||||
self._active_material = None # type: Optional[MaterialOutputModel]
|
||||
self._extruder_configuration = ExtruderConfigurationModel()
|
||||
self._extruder_configuration.position = self._position
|
||||
self._target_hotend_temperature = 0.0 # type: float
|
||||
self._hotend_temperature = 0.0 # type: float
|
||||
|
||||
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
|
||||
|
||||
def getPosition(self):
|
||||
def getPosition(self) -> int:
|
||||
return self._position
|
||||
|
||||
# Does the printer support pre-heating the bed at all
|
||||
@pyqtProperty(bool, constant=True)
|
||||
def canPreHeatHotends(self):
|
||||
def canPreHeatHotends(self) -> bool:
|
||||
if self._printer:
|
||||
return self._printer.canPreHeatHotends
|
||||
return False
|
||||
|
||||
@pyqtProperty(QObject, notify = activeMaterialChanged)
|
||||
@pyqtProperty(QObject, notify = extruderConfigurationChanged)
|
||||
def activeMaterial(self) -> Optional["MaterialOutputModel"]:
|
||||
return self._active_material
|
||||
return self._extruder_configuration.activeMaterial
|
||||
|
||||
def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]):
|
||||
if self._active_material != material:
|
||||
self._active_material = material
|
||||
self._extruder_configuration.material = self._active_material
|
||||
self.activeMaterialChanged.emit()
|
||||
self.extruderConfigurationChanged.emit()
|
||||
def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]) -> None:
|
||||
self._extruder_configuration.setMaterial(material)
|
||||
|
||||
## 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:
|
||||
self._hotend_temperature = temperature
|
||||
self.hotendTemperatureChanged.emit()
|
||||
|
||||
def updateTargetHotendTemperature(self, temperature: float):
|
||||
def updateTargetHotendTemperature(self, temperature: float) -> None:
|
||||
if self._target_hotend_temperature != temperature:
|
||||
self._target_hotend_temperature = temperature
|
||||
self.targetHotendTemperatureChanged.emit()
|
||||
|
||||
## Set the target hotend temperature. This ensures that it's actually sent to the remote.
|
||||
@pyqtSlot(float)
|
||||
def setTargetHotendTemperature(self, temperature: float):
|
||||
def setTargetHotendTemperature(self, temperature: float) -> None:
|
||||
self._printer.getController().setTargetHotendTemperature(self._printer, self, temperature)
|
||||
self.updateTargetHotendTemperature(temperature)
|
||||
|
||||
@ -81,30 +78,26 @@ class ExtruderOutputModel(QObject):
|
||||
def hotendTemperature(self) -> float:
|
||||
return self._hotend_temperature
|
||||
|
||||
@pyqtProperty(str, notify = hotendIDChanged)
|
||||
@pyqtProperty(str, notify = extruderConfigurationChanged)
|
||||
def hotendID(self) -> str:
|
||||
return self._hotend_id
|
||||
return self._extruder_configuration.hotendID
|
||||
|
||||
def updateHotendID(self, id: str):
|
||||
if self._hotend_id != id:
|
||||
self._hotend_id = id
|
||||
self._extruder_configuration.hotendID = self._hotend_id
|
||||
self.hotendIDChanged.emit()
|
||||
self.extruderConfigurationChanged.emit()
|
||||
def updateHotendID(self, hotend_id: str) -> None:
|
||||
self._extruder_configuration.setHotendID(hotend_id)
|
||||
|
||||
@pyqtProperty(QObject, notify = extruderConfigurationChanged)
|
||||
def extruderConfiguration(self):
|
||||
def extruderConfiguration(self) -> Optional[ExtruderConfigurationModel]:
|
||||
if self._extruder_configuration.isValid():
|
||||
return self._extruder_configuration
|
||||
return None
|
||||
|
||||
def updateIsPreheating(self, pre_heating):
|
||||
def updateIsPreheating(self, pre_heating: bool) -> None:
|
||||
if self._is_preheating != pre_heating:
|
||||
self._is_preheating = pre_heating
|
||||
self.isPreheatingChanged.emit()
|
||||
|
||||
@pyqtProperty(bool, notify=isPreheatingChanged)
|
||||
def isPreheating(self):
|
||||
def isPreheating(self) -> bool:
|
||||
return self._is_preheating
|
||||
|
||||
## Pre-heats the extruder before printer.
|
||||
@ -113,9 +106,9 @@ class ExtruderOutputModel(QObject):
|
||||
# Celsius.
|
||||
# \param duration How long the bed should stay warm, in seconds.
|
||||
@pyqtSlot(float, float)
|
||||
def preheatHotend(self, temperature, duration):
|
||||
def preheatHotend(self, temperature: float, duration: float) -> None:
|
||||
self._printer._controller.preheatHotend(self, temperature, duration)
|
||||
|
||||
@pyqtSlot()
|
||||
def cancelPreheatHotend(self):
|
||||
self._printer._controller.cancelPreheatHotend(self)
|
||||
def cancelPreheatHotend(self) -> None:
|
||||
self._printer._controller.cancelPreheatHotend(self)
|
||||
|
@ -66,7 +66,7 @@ class GenericOutputController(PrinterOutputController):
|
||||
self._output_device.sendCommand("G28 Z")
|
||||
|
||||
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):
|
||||
if state == "pause":
|
||||
|
@ -53,21 +53,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
self._sending_gcode = False
|
||||
self._compressing_gcode = False
|
||||
self._gcode = [] # type: List[str]
|
||||
|
||||
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:
|
||||
raise NotImplementedError("requestWrite needs to be implemented")
|
||||
|
||||
@ -188,40 +175,55 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
if reply in self._kept_alive_multiparts:
|
||||
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:
|
||||
self._createNetworkManager()
|
||||
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)
|
||||
self._last_request_time = time()
|
||||
reply = self._manager.put(request, data.encode())
|
||||
self._registerOnFinishedCallback(reply, on_finished)
|
||||
if self._manager is not None:
|
||||
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:
|
||||
if self._manager is None:
|
||||
self._createNetworkManager()
|
||||
assert (self._manager is not None)
|
||||
self._validateManager()
|
||||
request = self._createEmptyRequest(target)
|
||||
self._last_request_time = time()
|
||||
reply = self._manager.get(request)
|
||||
self._registerOnFinishedCallback(reply, on_finished)
|
||||
if self._manager is not None:
|
||||
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:
|
||||
if self._manager is None:
|
||||
self._createNetworkManager()
|
||||
assert (self._manager is not None)
|
||||
self._validateManager()
|
||||
request = self._createEmptyRequest(target)
|
||||
self._last_request_time = time()
|
||||
reply = self._manager.post(request, data)
|
||||
if on_progress is not None:
|
||||
reply.uploadProgress.connect(on_progress)
|
||||
self._registerOnFinishedCallback(reply, on_finished)
|
||||
if self._manager is not None:
|
||||
reply = self._manager.post(request, data)
|
||||
if on_progress is not None:
|
||||
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:
|
||||
|
||||
if self._manager is None:
|
||||
self._createNetworkManager()
|
||||
assert (self._manager is not None)
|
||||
self._validateManager()
|
||||
request = self._createEmptyRequest(target, content_type=None)
|
||||
multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType)
|
||||
for part in parts:
|
||||
@ -229,15 +231,18 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
|
||||
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:
|
||||
reply.uploadProgress.connect(on_progress)
|
||||
self._registerOnFinishedCallback(reply, on_finished)
|
||||
if on_progress is not None:
|
||||
reply.uploadProgress.connect(on_progress)
|
||||
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:
|
||||
post_part = QHttpPart()
|
||||
@ -323,7 +328,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
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
|
||||
@pyqtProperty(str, constant = True)
|
||||
|
@ -2,11 +2,15 @@
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
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:
|
||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
||||
|
||||
|
||||
class PrintJobOutputModel(QObject):
|
||||
@ -17,6 +21,9 @@ class PrintJobOutputModel(QObject):
|
||||
keyChanged = pyqtSignal()
|
||||
assignedPrinterChanged = pyqtSignal()
|
||||
ownerChanged = pyqtSignal()
|
||||
configurationChanged = pyqtSignal()
|
||||
previewImageChanged = pyqtSignal()
|
||||
compatibleMachineFamiliesChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
@ -29,6 +36,48 @@ class PrintJobOutputModel(QObject):
|
||||
self._assigned_printer = None # type: Optional[PrinterOutputModel]
|
||||
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)
|
||||
def owner(self):
|
||||
return self._owner
|
||||
|
@ -35,7 +35,7 @@ class PrinterOutputModel(QObject):
|
||||
self._key = "" # Unique identifier
|
||||
self._controller = output_controller
|
||||
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._active_print_job = None # type: Optional[PrintJobOutputModel]
|
||||
self._firmware_version = firmware_version
|
||||
@ -43,9 +43,9 @@ class PrinterOutputModel(QObject):
|
||||
self._is_preheating = False
|
||||
self._printer_type = ""
|
||||
self._buildplate_name = None
|
||||
# Update the printer configuration every time any of the extruders changes its configuration
|
||||
for extruder in self._extruders:
|
||||
extruder.extruderConfigurationChanged.connect(self._updateExtruderConfiguration)
|
||||
|
||||
self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in
|
||||
self._extruders]
|
||||
|
||||
self._camera = None
|
||||
|
||||
@ -120,7 +120,7 @@ class PrinterOutputModel(QObject):
|
||||
|
||||
@pyqtProperty(QVariant, notify = headPositionChanged)
|
||||
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):
|
||||
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):
|
||||
if self._printer_configuration.isValid():
|
||||
return self._printer_configuration
|
||||
return None
|
||||
|
||||
def _updateExtruderConfiguration(self):
|
||||
self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in self._extruders]
|
||||
self.configurationChanged.emit()
|
||||
return None
|
@ -13,23 +13,31 @@ from cura.Scene import ConvexHullNode
|
||||
|
||||
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.
|
||||
# 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):
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self._convex_hull_node = None
|
||||
self._convex_hull_node = None # type: Optional["SceneNode"]
|
||||
self._init2DConvexHullCache()
|
||||
|
||||
self._global_stack = None
|
||||
self._global_stack = None # type: Optional[GlobalStack]
|
||||
|
||||
# Make sure the timer is created on the main thread
|
||||
self._recompute_convex_hull_timer = None
|
||||
Application.getInstance().callLater(self.createRecomputeConvexHullTimer)
|
||||
self._recompute_convex_hull_timer = None # type: Optional[QTimer]
|
||||
|
||||
if Application.getInstance() is not None:
|
||||
Application.getInstance().callLater(self.createRecomputeConvexHullTimer)
|
||||
|
||||
self._raft_thickness = 0.0
|
||||
# For raft thickness, DRY
|
||||
self._build_volume = Application.getInstance().getBuildVolume()
|
||||
self._build_volume.raftThicknessChanged.connect(self._onChanged)
|
||||
|
||||
@ -39,13 +47,13 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
|
||||
self._onGlobalStackChanged()
|
||||
|
||||
def createRecomputeConvexHullTimer(self):
|
||||
def createRecomputeConvexHullTimer(self) -> None:
|
||||
self._recompute_convex_hull_timer = QTimer()
|
||||
self._recompute_convex_hull_timer.setInterval(200)
|
||||
self._recompute_convex_hull_timer.setSingleShot(True)
|
||||
self._recompute_convex_hull_timer.timeout.connect(self.recomputeConvexHull)
|
||||
|
||||
def setNode(self, node):
|
||||
def setNode(self, node: "SceneNode") -> None:
|
||||
previous_node = self._node
|
||||
# Disconnect from previous node signals
|
||||
if previous_node is not None and node is not previous_node:
|
||||
@ -63,14 +71,14 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
def __deepcopy__(self, memo):
|
||||
return ConvexHullDecorator()
|
||||
|
||||
## Get the unmodified 2D projected convex hull of the node
|
||||
def getConvexHull(self):
|
||||
## Get the unmodified 2D projected convex hull of the node (if any)
|
||||
def getConvexHull(self) -> Optional[Polygon]:
|
||||
if self._node is None:
|
||||
return None
|
||||
|
||||
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.
|
||||
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)))
|
||||
@ -78,7 +86,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
return hull
|
||||
|
||||
## 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:
|
||||
return None
|
||||
|
||||
@ -87,7 +95,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
## Get convex hull of the object + head size
|
||||
# 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
|
||||
def getConvexHullHead(self):
|
||||
def getConvexHullHead(self) -> Optional[Polygon]:
|
||||
if self._node is None:
|
||||
return None
|
||||
|
||||
@ -101,7 +109,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
## Get convex hull of the node
|
||||
# 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.
|
||||
def getConvexHullBoundary(self):
|
||||
def getConvexHullBoundary(self) -> Optional[Polygon]:
|
||||
if self._node is None:
|
||||
return None
|
||||
|
||||
@ -111,13 +119,14 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
return self._compute2DConvexHull()
|
||||
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:
|
||||
self._recompute_convex_hull_timer.start()
|
||||
else:
|
||||
self.recomputeConvexHull()
|
||||
|
||||
def recomputeConvexHull(self):
|
||||
def recomputeConvexHull(self) -> None:
|
||||
controller = Application.getInstance().getController()
|
||||
root = controller.getScene().getRoot()
|
||||
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)
|
||||
self._convex_hull_node = hull_node
|
||||
|
||||
def _onSettingValueChanged(self, key, property_name):
|
||||
if property_name != "value": #Not the value that was changed.
|
||||
def _onSettingValueChanged(self, key: str, property_name: str) -> None:
|
||||
if property_name != "value": # Not the value that was changed.
|
||||
return
|
||||
|
||||
if key in self._affected_settings:
|
||||
self._onChanged()
|
||||
if key in self._influencing_settings:
|
||||
self._init2DConvexHullCache() #Invalidate the cache.
|
||||
self._init2DConvexHullCache() # Invalidate the cache.
|
||||
self._onChanged()
|
||||
|
||||
def _init2DConvexHullCache(self):
|
||||
def _init2DConvexHullCache(self) -> None:
|
||||
# Cache for the group code path in _compute2DConvexHull()
|
||||
self._2d_convex_hull_group_child_polygon = 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_result = None
|
||||
|
||||
def _compute2DConvexHull(self):
|
||||
def _compute2DConvexHull(self) -> Optional[Polygon]:
|
||||
if self._node.callDecoration("isGroup"):
|
||||
points = numpy.zeros((0, 2), dtype=numpy.int32)
|
||||
for child in self._node.getChildren():
|
||||
@ -179,8 +188,6 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
|
||||
else:
|
||||
offset_hull = None
|
||||
mesh = None
|
||||
world_transform = None
|
||||
if self._node.getMeshData():
|
||||
mesh = self._node.getMeshData()
|
||||
world_transform = self._node.getWorldTransformation()
|
||||
@ -228,24 +235,33 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
|
||||
return offset_hull
|
||||
|
||||
def _getHeadAndFans(self):
|
||||
return Polygon(numpy.array(self._global_stack.getProperty("machine_head_with_fans_polygon", "value"), numpy.float32))
|
||||
def _getHeadAndFans(self) -> Polygon:
|
||||
if self._global_stack:
|
||||
return Polygon(numpy.array(self._global_stack.getHeadAndFansCoordinates(), numpy.float32))
|
||||
return Polygon()
|
||||
|
||||
def _compute2DConvexHeadFull(self):
|
||||
return self._compute2DConvexHull().getMinkowskiHull(self._getHeadAndFans())
|
||||
def _compute2DConvexHeadFull(self) -> Optional[Polygon]:
|
||||
convex_hull = self._compute2DConvexHull()
|
||||
if convex_hull:
|
||||
return convex_hull.getMinkowskiHull(self._getHeadAndFans())
|
||||
return None
|
||||
|
||||
def _compute2DConvexHeadMin(self):
|
||||
headAndFans = self._getHeadAndFans()
|
||||
mirrored = headAndFans.mirror([0, 0], [0, 1]).mirror([0, 0], [1, 0]) # Mirror horizontally & vertically.
|
||||
def _compute2DConvexHeadMin(self) -> Optional[Polygon]:
|
||||
head_and_fans = self._getHeadAndFans()
|
||||
mirrored = head_and_fans.mirror([0, 0], [0, 1]).mirror([0, 0], [1, 0]) # Mirror horizontally & vertically.
|
||||
head_and_fans = self._getHeadAndFans().intersectionConvexHulls(mirrored)
|
||||
|
||||
# Min head hull is used for the push free
|
||||
min_head_hull = self._compute2DConvexHull().getMinkowskiHull(head_and_fans)
|
||||
return min_head_hull
|
||||
convex_hull = self._compute2DConvexHeadFull()
|
||||
if convex_hull:
|
||||
return convex_hull.getMinkowskiHull(head_and_fans)
|
||||
return None
|
||||
|
||||
## Compensate given 2D polygon with adhesion 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
|
||||
# Add extra margin depending on adhesion type
|
||||
adhesion_type = self._global_stack.getProperty("adhesion_type", "value")
|
||||
@ -263,7 +279,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
else:
|
||||
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:
|
||||
extra_margin_polygon = Polygon.approximatedCircle(extra_margin)
|
||||
poly = poly.getMinkowskiHull(extra_margin_polygon)
|
||||
@ -274,7 +290,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
# \param convex_hull Polygon of the original convex hull.
|
||||
# \return New Polygon instance that is offset with everything that
|
||||
# influences the collision area.
|
||||
def _offsetHull(self, convex_hull):
|
||||
def _offsetHull(self, convex_hull: Polygon) -> Polygon:
|
||||
horizontal_expansion = max(
|
||||
self._getSettingProperty("xy_offset", "value"),
|
||||
self._getSettingProperty("xy_offset_layer_0", "value")
|
||||
@ -295,16 +311,16 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
else:
|
||||
return convex_hull
|
||||
|
||||
def _onChanged(self, *args):
|
||||
def _onChanged(self, *args) -> None:
|
||||
self._raft_thickness = self._build_volume.getRaftThickness()
|
||||
if not args or args[0] == self._node:
|
||||
self.recomputeConvexHullDelayed()
|
||||
|
||||
def _onGlobalStackChanged(self):
|
||||
def _onGlobalStackChanged(self) -> None:
|
||||
if self._global_stack:
|
||||
self._global_stack.propertyChanged.disconnect(self._onSettingValueChanged)
|
||||
self._global_stack.containersChanged.disconnect(self._onChanged)
|
||||
extruders = ExtruderManager.getInstance().getMachineExtruders(self._global_stack.getId())
|
||||
extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||
for extruder in extruders:
|
||||
extruder.propertyChanged.disconnect(self._onSettingValueChanged)
|
||||
|
||||
@ -314,14 +330,16 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
self._global_stack.propertyChanged.connect(self._onSettingValueChanged)
|
||||
self._global_stack.containersChanged.connect(self._onChanged)
|
||||
|
||||
extruders = ExtruderManager.getInstance().getMachineExtruders(self._global_stack.getId())
|
||||
extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||
for extruder in extruders:
|
||||
extruder.propertyChanged.connect(self._onSettingValueChanged)
|
||||
|
||||
self._onChanged()
|
||||
|
||||
## 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")
|
||||
if per_mesh_stack:
|
||||
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
|
||||
return self._global_stack.getProperty(setting_key, prop)
|
||||
|
||||
## Returns true if node is a descendant or the same as the root node.
|
||||
def __isDescendant(self, root, node):
|
||||
## Returns True if node is a descendant or the same as the root node.
|
||||
def __isDescendant(self, root: "SceneNode", node: "SceneNode") -> bool:
|
||||
if node is None:
|
||||
return False
|
||||
if root is node:
|
||||
|
@ -1,13 +1,19 @@
|
||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
from typing import List
|
||||
|
||||
|
||||
class GCodeListDecorator(SceneNodeDecorator):
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._gcode_list = []
|
||||
self._gcode_list = [] # type: List[str]
|
||||
|
||||
def getGCodeList(self):
|
||||
def getGCodeList(self) -> List[str]:
|
||||
return self._gcode_list
|
||||
|
||||
def setGCodeList(self, list):
|
||||
def setGCodeList(self, list: List[str]):
|
||||
self._gcode_list = list
|
||||
|
||||
def __deepcopy__(self, memo) -> "GCodeListDecorator":
|
||||
copied_decorator = GCodeListDecorator()
|
||||
copied_decorator.setGCodeList(self.getGCodeList())
|
||||
return copied_decorator
|
@ -2,11 +2,11 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
|
||||
|
||||
class SliceableObjectDecorator(SceneNodeDecorator):
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
def isSliceable(self):
|
||||
def isSliceable(self) -> bool:
|
||||
return True
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
def __deepcopy__(self, memo) -> "SliceableObjectDecorator":
|
||||
return type(self)()
|
||||
|
@ -1,18 +1,19 @@
|
||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
|
||||
|
||||
## A decorator that stores the amount an object has been moved below the platform.
|
||||
class ZOffsetDecorator(SceneNodeDecorator):
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._z_offset = 0
|
||||
self._z_offset = 0.
|
||||
|
||||
def setZOffset(self, offset):
|
||||
def setZOffset(self, offset: float) -> None:
|
||||
self._z_offset = offset
|
||||
|
||||
def getZOffset(self):
|
||||
def getZOffset(self) -> float:
|
||||
return self._z_offset
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
def __deepcopy__(self, memo) -> "ZOffsetDecorator":
|
||||
copied_decorator = ZOffsetDecorator()
|
||||
copied_decorator.setZOffset(self.getZOffset())
|
||||
return copied_decorator
|
||||
|
@ -4,12 +4,12 @@
|
||||
import os
|
||||
import urllib.parse
|
||||
import uuid
|
||||
from typing import Any
|
||||
from typing import Dict, Union, Optional
|
||||
from typing import Dict, Union, Any, TYPE_CHECKING, List
|
||||
|
||||
from PyQt5.QtCore import QObject, QUrl, QVariant
|
||||
from PyQt5.QtCore import QObject, QUrl
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from UM.Logger import Logger
|
||||
@ -21,6 +21,18 @@ from UM.Settings.ContainerStack import ContainerStack
|
||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||
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")
|
||||
|
||||
|
||||
@ -31,20 +43,20 @@ catalog = i18nCatalog("cura")
|
||||
# when a certain action happens. This can be done through this class.
|
||||
class ContainerManager(QObject):
|
||||
|
||||
def __init__(self, application):
|
||||
def __init__(self, application: "CuraApplication") -> None:
|
||||
if ContainerManager.__instance is not None:
|
||||
raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__)
|
||||
ContainerManager.__instance = self
|
||||
|
||||
super().__init__(parent = application)
|
||||
|
||||
self._application = application
|
||||
self._plugin_registry = self._application.getPluginRegistry()
|
||||
self._container_registry = self._application.getContainerRegistry()
|
||||
self._machine_manager = self._application.getMachineManager()
|
||||
self._material_manager = self._application.getMaterialManager()
|
||||
self._quality_manager = self._application.getQualityManager()
|
||||
self._container_name_filters = {} # type: Dict[str, Dict[str, Any]]
|
||||
self._application = application # type: CuraApplication
|
||||
self._plugin_registry = self._application.getPluginRegistry() # type: PluginRegistry
|
||||
self._container_registry = self._application.getContainerRegistry() # type: ContainerRegistry
|
||||
self._machine_manager = self._application.getMachineManager() # type: MachineManager
|
||||
self._material_manager = self._application.getMaterialManager() # type: MaterialManager
|
||||
self._quality_manager = self._application.getQualityManager() # type: QualityManager
|
||||
self._container_name_filters = {} # type: Dict[str, Dict[str, Any]]
|
||||
|
||||
@pyqtSlot(str, str, result=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
|
||||
# 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_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.
|
||||
# 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)
|
||||
def setContainerMetaDataEntry(self, container_node, entry_name, entry_value):
|
||||
root_material_id = container_node.metadata["base_file"]
|
||||
def setContainerMetaDataEntry(self, container_node: "ContainerNode", entry_name: str, entry_value: str) -> bool:
|
||||
root_material_id = container_node.getMetaDataEntry("base_file", "")
|
||||
if self._container_registry.isReadOnly(root_material_id):
|
||||
Logger.log("w", "Cannot set metadata of read-only container %s.", root_material_id)
|
||||
return False
|
||||
|
||||
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("/")
|
||||
entry_name = entries.pop()
|
||||
@ -91,11 +105,11 @@ class ContainerManager(QObject):
|
||||
sub_item_changed = False
|
||||
if entries:
|
||||
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
|
||||
for _ in range(len(entries)):
|
||||
item = item.get(entries.pop(0), { })
|
||||
item = item.get(entries.pop(0), {})
|
||||
|
||||
if item[entry_name] != entry_value:
|
||||
sub_item_changed = True
|
||||
@ -109,9 +123,10 @@ class ContainerManager(QObject):
|
||||
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.
|
||||
container.metaDataChanged.emit(container)
|
||||
return True
|
||||
|
||||
@pyqtSlot(str, result = str)
|
||||
def makeUniqueName(self, original_name):
|
||||
def makeUniqueName(self, original_name: str) -> str:
|
||||
return self._container_registry.uniqueName(original_name)
|
||||
|
||||
## 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.
|
||||
@pyqtSlot(str, result = "QStringList")
|
||||
def getContainerNameFilters(self, type_name):
|
||||
def getContainerNameFilters(self, type_name: str) -> List[str]:
|
||||
if not self._container_name_filters:
|
||||
self._updateContainerNameFilters()
|
||||
|
||||
@ -257,7 +272,7 @@ class ContainerManager(QObject):
|
||||
#
|
||||
# \return \type{bool} True if successful, False if not.
|
||||
@pyqtSlot(result = bool)
|
||||
def updateQualityChanges(self):
|
||||
def updateQualityChanges(self) -> bool:
|
||||
global_stack = self._machine_manager.activeMachine
|
||||
if not global_stack:
|
||||
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.
|
||||
# \return \type{list} a list of names of materials with the same GUID
|
||||
@pyqtSlot("QVariant", bool, result = "QStringList")
|
||||
def getLinkedMaterials(self, material_node, exclude_self = False):
|
||||
guid = material_node.metadata["GUID"]
|
||||
def getLinkedMaterials(self, material_node: "MaterialNode", exclude_self: bool = False):
|
||||
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)
|
||||
|
||||
linked_material_names = []
|
||||
@ -324,15 +339,19 @@ class ContainerManager(QObject):
|
||||
for material_group in material_group_list:
|
||||
if exclude_self and material_group.name == self_root_material_id:
|
||||
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
|
||||
|
||||
## 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.
|
||||
@pyqtSlot("QVariant")
|
||||
def unlinkMaterial(self, material_node):
|
||||
def unlinkMaterial(self, material_node: "MaterialNode") -> None:
|
||||
# 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
|
||||
new_guid = str(uuid.uuid4())
|
||||
@ -344,7 +363,7 @@ class ContainerManager(QObject):
|
||||
if container is not None:
|
||||
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:
|
||||
return
|
||||
|
||||
@ -400,7 +419,7 @@ class ContainerManager(QObject):
|
||||
|
||||
## Import single profile, file_url does not have to end with curaprofile
|
||||
@pyqtSlot(QUrl, result="QVariantMap")
|
||||
def importProfile(self, file_url):
|
||||
def importProfile(self, file_url: QUrl):
|
||||
if not file_url.isValid():
|
||||
return
|
||||
path = file_url.toLocalFile()
|
||||
@ -409,7 +428,7 @@ class ContainerManager(QObject):
|
||||
return self._container_registry.importProfile(path)
|
||||
|
||||
@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():
|
||||
return
|
||||
path = file_url.toLocalFile()
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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 UM.Application import Application
|
||||
@ -13,6 +13,7 @@ from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.Interfaces import ContainerInterface, DefinitionContainerInterface
|
||||
from cura.Settings import cura_empty_instance_containers
|
||||
|
||||
from . import Exceptions
|
||||
|
||||
@ -39,14 +40,12 @@ class CuraContainerStack(ContainerStack):
|
||||
def __init__(self, container_id: str) -> None:
|
||||
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 = self._container_registry.findInstanceContainers(id = "empty_quality_changes")[0] #type: InstanceContainer
|
||||
self._empty_quality = self._container_registry.findInstanceContainers(id = "empty_quality")[0] #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._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_material = cura_empty_instance_containers.empty_material_container #type: InstanceContainer
|
||||
self._empty_variant = cura_empty_instance_containers.empty_variant_container #type: InstanceContainer
|
||||
|
||||
self._containers = [self._empty_instance_container for i in range(len(_ContainerIndexes.IndexTypeMap))] #type: List[ContainerInterface]
|
||||
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.
|
||||
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
|
||||
# and its properties rather than, for example, the extruder. Defaults to simply returning the definition property.
|
||||
|
@ -8,13 +8,14 @@ from UM.Logger import Logger
|
||||
from UM.Settings.Interfaces import DefinitionContainerInterface
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
|
||||
from cura.Machines.VariantManager import VariantType
|
||||
from cura.Machines.VariantType import VariantType
|
||||
from .GlobalStack import GlobalStack
|
||||
from .ExtruderStack import ExtruderStack
|
||||
|
||||
|
||||
## Contains helper functions to create new machines.
|
||||
class CuraStackBuilder:
|
||||
|
||||
## Create a new instance of a machine.
|
||||
#
|
||||
# \param name The name of the new machine.
|
||||
@ -26,7 +27,6 @@ class CuraStackBuilder:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
application = CuraApplication.getInstance()
|
||||
variant_manager = application.getVariantManager()
|
||||
material_manager = application.getMaterialManager()
|
||||
quality_manager = application.getQualityManager()
|
||||
registry = application.getContainerRegistry()
|
||||
|
||||
@ -46,16 +46,6 @@ class CuraStackBuilder:
|
||||
if not global_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())
|
||||
# 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
|
||||
@ -74,50 +64,35 @@ class CuraStackBuilder:
|
||||
|
||||
# Create ExtruderStacks
|
||||
extruder_dict = machine_definition.getMetaDataEntry("machine_extruder_trains")
|
||||
|
||||
for position, extruder_definition_id in extruder_dict.items():
|
||||
# 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 position in extruder_dict:
|
||||
cls.createExtruderStackWithDefaultSetup(new_global_stack, position)
|
||||
|
||||
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)
|
||||
|
||||
preferred_quality_type = machine_definition.getMetaDataEntry("preferred_quality_type")
|
||||
quality_group_dict = quality_manager.getQualityGroups(new_global_stack)
|
||||
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:
|
||||
if not quality_group_dict:
|
||||
# There is no available quality group, set all quality containers to empty.
|
||||
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:
|
||||
for extruder_stack in new_global_stack.extruders.values():
|
||||
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
|
||||
# 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
|
||||
|
||||
## 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
|
||||
#
|
||||
# \param new_stack_id The ID of the new stack.
|
||||
# \param definition The definition to base the new stack on.
|
||||
# \param machine_definition_id The ID of the machine definition to use for
|
||||
# the user container.
|
||||
# \param kwargs You can add keyword arguments to specify IDs of containers to use for a specific type, for example "variant": "0.4mm"
|
||||
# \param extruder_definition The definition to base the new stack on.
|
||||
# \param machine_definition_id The ID of the machine definition to use for the user container.
|
||||
# \param position The position the extruder occupies in the machine.
|
||||
# \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
|
||||
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,
|
||||
variant_container, material_container, quality_container) -> ExtruderStack:
|
||||
variant_container: "InstanceContainer",
|
||||
material_container: "InstanceContainer",
|
||||
quality_container: "InstanceContainer") -> ExtruderStack:
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
application = CuraApplication.getInstance()
|
||||
registry = application.getContainerRegistry()
|
||||
@ -146,7 +175,7 @@ class CuraStackBuilder:
|
||||
stack.setName(extruder_definition.getName())
|
||||
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,
|
||||
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"
|
||||
#
|
||||
# \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
|
||||
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
|
||||
application = CuraApplication.getInstance()
|
||||
registry = application.getContainerRegistry()
|
||||
|
@ -4,7 +4,8 @@
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt.
|
||||
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.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
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.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID.
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
from UM.Settings.SettingInstance import SettingInstance
|
||||
from UM.Settings.ContainerStack import ContainerStack
|
||||
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:
|
||||
from cura.Settings.ExtruderStack import ExtruderStack
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
||||
|
||||
## Manages all existing extruder stacks.
|
||||
@ -38,9 +37,13 @@ class ExtruderManager(QObject):
|
||||
|
||||
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._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()
|
||||
|
||||
Selection.selectionChanged.connect(self.resetSelectedObjectExtruders)
|
||||
@ -68,7 +71,7 @@ class ExtruderManager(QObject):
|
||||
|
||||
## Return extruder count according to extruder trains.
|
||||
@pyqtProperty(int, notify = extrudersChanged)
|
||||
def extruderCount(self):
|
||||
def extruderCount(self) -> int:
|
||||
if not self._application.getGlobalContainerStack():
|
||||
return 0 # No active machine, so no extruders.
|
||||
try:
|
||||
@ -79,28 +82,14 @@ class ExtruderManager(QObject):
|
||||
## Gets a dict with the extruder stack ids with the extruder number as the key.
|
||||
@pyqtProperty("QVariantMap", notify = extrudersChanged)
|
||||
def extruderIds(self) -> Dict[str, str]:
|
||||
extruder_stack_ids = {}
|
||||
extruder_stack_ids = {} # type: Dict[str, str]
|
||||
|
||||
global_container_stack = self._application.getGlobalContainerStack()
|
||||
if global_container_stack:
|
||||
global_stack_id = global_container_stack.getId()
|
||||
|
||||
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()
|
||||
extruder_stack_ids = {position: extruder.id for position, extruder in global_container_stack.extruders.items()}
|
||||
|
||||
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.
|
||||
#
|
||||
# \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.
|
||||
@pyqtSlot(int, result = str)
|
||||
def getExtruderName(self, index):
|
||||
def getExtruderName(self, index: int) -> str:
|
||||
try:
|
||||
return list(self.getActiveExtruderStacks())[index].getName()
|
||||
return self.getActiveExtruderStacks()[index].getName()
|
||||
except IndexError:
|
||||
return ""
|
||||
|
||||
@ -128,12 +117,12 @@ class ExtruderManager(QObject):
|
||||
|
||||
## Provides a list of extruder IDs used by the current selected objects.
|
||||
@pyqtProperty("QVariantList", notify = selectedObjectExtrudersChanged)
|
||||
def selectedObjectExtruders(self) -> List[str]:
|
||||
def selectedObjectExtruders(self) -> List[Union[str, "ExtruderStack"]]:
|
||||
if not self._selected_object_extruders:
|
||||
object_extruders = set()
|
||||
|
||||
# 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():
|
||||
if node.callDecoration("isGroup"):
|
||||
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)
|
||||
|
||||
# Then, figure out which nodes are used by those selected nodes.
|
||||
global_stack = self._application.getGlobalContainerStack()
|
||||
current_extruder_trains = self._extruder_trains.get(global_stack.getId())
|
||||
current_extruder_trains = self.getActiveExtruderStacks()
|
||||
for node in selected_nodes:
|
||||
extruder = node.callDecoration("getActiveExtruder")
|
||||
if extruder:
|
||||
object_extruders.add(extruder)
|
||||
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
|
||||
|
||||
@ -163,19 +151,12 @@ class ExtruderManager(QObject):
|
||||
# This will trigger a recalculation of the extruders used for the
|
||||
# selection.
|
||||
def resetSelectedObjectExtruders(self) -> None:
|
||||
self._selected_object_extruders = []
|
||||
self._selected_object_extruders = [] # type: List[Union[str, "ExtruderStack"]]
|
||||
self.selectedObjectExtrudersChanged.emit()
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
def getActiveExtruderStack(self) -> Optional["ExtruderStack"]:
|
||||
global_container_stack = self._application.getGlobalContainerStack()
|
||||
|
||||
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
|
||||
return self.getExtruderStack(self._active_extruder_index)
|
||||
|
||||
## Get an extruder stack by index
|
||||
def getExtruderStack(self, index) -> Optional["ExtruderStack"]:
|
||||
@ -186,16 +167,7 @@ class ExtruderManager(QObject):
|
||||
return self._extruder_trains[global_container_stack.getId()][str(index)]
|
||||
return None
|
||||
|
||||
## Get all extruder stacks
|
||||
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):
|
||||
def registerExtruder(self, extruder_train: "ExtruderStack", machine_id: str) -> None:
|
||||
changed = False
|
||||
|
||||
if machine_id not in self._extruder_trains:
|
||||
@ -214,23 +186,20 @@ class ExtruderManager(QObject):
|
||||
if changed:
|
||||
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.
|
||||
#
|
||||
# \param setting_key \type{str} The setting to get the property of.
|
||||
# \param property \type{str} The property to get.
|
||||
# \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 = []
|
||||
for index in self.extruderIds:
|
||||
extruder_stack_id = self.extruderIds[str(index)]
|
||||
extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
|
||||
|
||||
for extruder_stack in self.getActiveExtruderStacks():
|
||||
result.append(extruder_stack.getProperty(setting_key, prop))
|
||||
|
||||
return result
|
||||
|
||||
def extruderValueWithDefault(self, value):
|
||||
def extruderValueWithDefault(self, value: str) -> str:
|
||||
machine_manager = self._application.getMachineManager()
|
||||
if value == "-1":
|
||||
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.
|
||||
#
|
||||
# \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):
|
||||
ContainerRegistry.getInstance().removeContainer(extruder.userChanges.getId())
|
||||
ContainerRegistry.getInstance().removeContainer(extruder.getId())
|
||||
@ -331,24 +300,11 @@ class ExtruderManager(QObject):
|
||||
## Returns extruders for a specific machine.
|
||||
#
|
||||
# \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:
|
||||
return []
|
||||
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.
|
||||
#
|
||||
# \return \type{List[ContainerStack]} a list of
|
||||
@ -357,14 +313,11 @@ class ExtruderManager(QObject):
|
||||
if not global_stack:
|
||||
return []
|
||||
|
||||
result = []
|
||||
if global_stack.getId() in self._extruder_trains:
|
||||
for extruder in sorted(self._extruder_trains[global_stack.getId()]):
|
||||
result.append(self._extruder_trains[global_stack.getId()][extruder])
|
||||
result_tuple_list = sorted(list(global_stack.extruders.items()), key = lambda x: int(x[0]))
|
||||
result_list = [item[1] for item in result_tuple_list]
|
||||
|
||||
machine_extruder_count = global_stack.getProperty("machine_extruder_count", "value")
|
||||
|
||||
return result[:machine_extruder_count]
|
||||
return result_list[:machine_extruder_count]
|
||||
|
||||
def _globalContainerStackChanged(self) -> None:
|
||||
# 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
|
||||
# "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"]
|
||||
extruder_stack_0 = global_stack.extruders["0"]
|
||||
if extruder_stack_0.definition.getId() != expected_extruder_definition_0_id:
|
||||
extruder_stack_0 = global_stack.extruders.get("0")
|
||||
|
||||
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(
|
||||
printer = global_stack.getId(), expected = expected_extruder_definition_0_id, got = extruder_stack_0.definition.getId()))
|
||||
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.
|
||||
# If no extruder has the value, the list will contain the global value.
|
||||
@staticmethod
|
||||
def getExtruderValues(key):
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
def getExtruderValues(key: str) -> List[Any]:
|
||||
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 = []
|
||||
for extruder in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()):
|
||||
for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
|
||||
if not extruder.isEnabled:
|
||||
continue
|
||||
# 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.
|
||||
# If no extruder has the value, the list will contain the global value.
|
||||
@staticmethod
|
||||
def getDefaultExtruderValues(key):
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
def getDefaultExtruderValues(key: str) -> List[Any]:
|
||||
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.context["evaluate_from_container_index"] = 1 # skip the user settings container
|
||||
context.context["override_operators"] = {
|
||||
@ -471,7 +431,7 @@ class ExtruderManager(QObject):
|
||||
}
|
||||
|
||||
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
|
||||
if int(extruder.getMetaDataEntry("position")) >= global_stack.getProperty("machine_extruder_count", "value", context = context):
|
||||
continue
|
||||
@ -504,7 +464,7 @@ class ExtruderManager(QObject):
|
||||
#
|
||||
# \return String representing the extruder values
|
||||
@pyqtSlot(str, result="QVariant")
|
||||
def getInstanceExtruderValues(self, key):
|
||||
def getInstanceExtruderValues(self, key) -> List:
|
||||
return ExtruderManager.getExtruderValues(key)
|
||||
|
||||
## 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
|
||||
# global stack if not found.
|
||||
@staticmethod
|
||||
def getExtruderValue(extruder_index, key):
|
||||
def getExtruderValue(extruder_index: int, key: str) -> Any:
|
||||
if extruder_index == -1:
|
||||
extruder_index = int(cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition)
|
||||
extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index)
|
||||
@ -528,7 +488,7 @@ class ExtruderManager(QObject):
|
||||
value = value(extruder)
|
||||
else:
|
||||
# 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
|
||||
|
||||
@ -542,7 +502,7 @@ class ExtruderManager(QObject):
|
||||
# \return The value of the setting for the specified extruder or for the
|
||||
# global stack if not found.
|
||||
@staticmethod
|
||||
def getDefaultExtruderValue(extruder_index, key):
|
||||
def getDefaultExtruderValue(extruder_index: int, key: str) -> Any:
|
||||
extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index)
|
||||
context = PropertyEvaluationContext(extruder)
|
||||
context.context["evaluate_from_container_index"] = 1 # skip the user settings container
|
||||
@ -557,7 +517,7 @@ class ExtruderManager(QObject):
|
||||
if isinstance(value, SettingFunction):
|
||||
value = value(extruder, context = context)
|
||||
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
|
||||
|
||||
@ -569,8 +529,8 @@ class ExtruderManager(QObject):
|
||||
#
|
||||
# \return The effective value
|
||||
@staticmethod
|
||||
def getResolveOrValue(key):
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
def getResolveOrValue(key: str) -> Any:
|
||||
global_stack = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack())
|
||||
resolved_value = global_stack.getProperty(key, "value")
|
||||
|
||||
return resolved_value
|
||||
@ -583,8 +543,8 @@ class ExtruderManager(QObject):
|
||||
#
|
||||
# \return The effective value
|
||||
@staticmethod
|
||||
def getDefaultResolveOrValue(key):
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
def getDefaultResolveOrValue(key: str) -> Any:
|
||||
global_stack = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack())
|
||||
context = PropertyEvaluationContext(global_stack)
|
||||
context.context["evaluate_from_container_index"] = 1 # skip the user settings container
|
||||
context.context["override_operators"] = {
|
||||
|
@ -139,9 +139,6 @@ class ExtruderStack(CuraContainerStack):
|
||||
super().deserialize(contents, file_name)
|
||||
if "enabled" not in self.getMetaData():
|
||||
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:
|
||||
# When there is a setting that is not settable per extruder that depends on a value from a setting that is,
|
||||
|
@ -134,7 +134,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
# Link to new extruders
|
||||
self._active_machine_extruders = []
|
||||
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.
|
||||
continue
|
||||
extruder.containersChanged.connect(self._onExtruderStackContainersChanged)
|
||||
@ -171,7 +171,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
# get machine extruder count for verification
|
||||
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
|
||||
try:
|
||||
position = int(position)
|
||||
|
@ -13,6 +13,8 @@ from UM.Settings.SettingInstance import InstanceState
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.Interfaces import PropertyEvaluationContext
|
||||
from UM.Logger import Logger
|
||||
from UM.Util import parseBool
|
||||
|
||||
import cura.CuraApplication
|
||||
|
||||
from . import Exceptions
|
||||
@ -21,6 +23,7 @@ from .CuraContainerStack import CuraContainerStack
|
||||
if TYPE_CHECKING:
|
||||
from cura.Settings.ExtruderStack import ExtruderStack
|
||||
|
||||
|
||||
## Represents the Global or Machine stack and its related containers.
|
||||
#
|
||||
class GlobalStack(CuraContainerStack):
|
||||
@ -55,6 +58,16 @@ class GlobalStack(CuraContainerStack):
|
||||
return "machine_stack"
|
||||
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.
|
||||
#
|
||||
# \param extruder The extruder to add.
|
||||
@ -96,6 +109,9 @@ class GlobalStack(CuraContainerStack):
|
||||
|
||||
# Handle the "resolve" property.
|
||||
#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):
|
||||
current_thread = threading.current_thread()
|
||||
self._resolving_settings[current_thread.name].add(key)
|
||||
@ -172,6 +188,18 @@ class GlobalStack(CuraContainerStack):
|
||||
return False
|
||||
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:
|
||||
global_stack_mime = MimeType(
|
||||
|
@ -21,9 +21,6 @@ from UM.Settings.SettingFunction import SettingFunction
|
||||
from UM.Signal import postponeSignals, CompressTechnique
|
||||
|
||||
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.PrinterOutputDevice import PrinterOutputDevice
|
||||
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
||||
@ -44,12 +41,16 @@ if TYPE_CHECKING:
|
||||
from cura.Machines.MaterialManager import MaterialManager
|
||||
from cura.Machines.QualityManager import QualityManager
|
||||
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):
|
||||
def __init__(self, parent: QObject = None) -> None:
|
||||
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._current_root_material_id = {} # type: Dict[str, str]
|
||||
@ -366,6 +367,7 @@ class MachineManager(QObject):
|
||||
return
|
||||
|
||||
global_stack = containers[0]
|
||||
ExtruderManager.getInstance()._fixSingleExtrusionMachineExtruderDefinition(global_stack)
|
||||
if not global_stack.isValid():
|
||||
# Mark global stack as invalid
|
||||
ConfigurationErrorMessage.getInstance().addFaultyContainers(global_stack.getId())
|
||||
@ -374,7 +376,7 @@ class MachineManager(QObject):
|
||||
self._global_container_stack = global_stack
|
||||
self._application.setGlobalContainerStack(global_stack)
|
||||
ExtruderManager.getInstance()._globalContainerStackChanged()
|
||||
self._initMachineState(containers[0])
|
||||
self._initMachineState(global_stack)
|
||||
self._onGlobalContainerChanged()
|
||||
|
||||
self.__emitChangedSignals()
|
||||
@ -384,7 +386,9 @@ class MachineManager(QObject):
|
||||
# \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
|
||||
@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)
|
||||
for machine in machines:
|
||||
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
|
||||
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
|
||||
for stack in extruder_stacks:
|
||||
md = stack.getMetaData()
|
||||
@ -434,7 +438,7 @@ class MachineManager(QObject):
|
||||
if self._global_container_stack.getTop().findInstances():
|
||||
return True
|
||||
|
||||
stacks = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
|
||||
stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||
for stack in stacks:
|
||||
if stack.getTop().findInstances():
|
||||
return True
|
||||
@ -447,7 +451,7 @@ class MachineManager(QObject):
|
||||
return 0
|
||||
num_user_settings = 0
|
||||
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:
|
||||
num_user_settings += len(stack.getTop().findInstances())
|
||||
return num_user_settings
|
||||
@ -472,7 +476,7 @@ class MachineManager(QObject):
|
||||
stack = ExtruderManager.getInstance().getActiveExtruderStack()
|
||||
stacks = [stack]
|
||||
else:
|
||||
stacks = ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())
|
||||
stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||
|
||||
for stack in stacks:
|
||||
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:
|
||||
return
|
||||
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
|
||||
for extruder_stack in extruder_stacks:
|
||||
@ -889,7 +893,11 @@ class MachineManager(QObject):
|
||||
extruder_nr = node.callDecoration("getActiveExtruderPosition")
|
||||
|
||||
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
|
||||
extruder_manager.setActiveExtruderIndex(0)
|
||||
@ -1087,7 +1095,7 @@ class MachineManager(QObject):
|
||||
self.activeQualityGroupChanged.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:
|
||||
return
|
||||
if quality_group is None:
|
||||
@ -1118,7 +1126,7 @@ class MachineManager(QObject):
|
||||
self.activeQualityGroupChanged.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())
|
||||
containers = [n.getContainer() for n in nodes if n is not None]
|
||||
for container in containers:
|
||||
@ -1126,7 +1134,7 @@ class MachineManager(QObject):
|
||||
container.setMetaDataEntry("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:
|
||||
return #Can't change that.
|
||||
quality_type = quality_changes_group.quality_type
|
||||
@ -1170,20 +1178,20 @@ class MachineManager(QObject):
|
||||
self.activeQualityGroupChanged.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:
|
||||
return
|
||||
self._global_container_stack.extruders[position].variant = container_node.getContainer()
|
||||
self.activeVariantChanged.emit()
|
||||
|
||||
def _setGlobalVariant(self, container_node: ContainerNode) -> None:
|
||||
def _setGlobalVariant(self, container_node: "ContainerNode") -> None:
|
||||
if self._global_container_stack is None:
|
||||
return
|
||||
self._global_container_stack.variant = container_node.getContainer()
|
||||
if not self._global_container_stack.variant:
|
||||
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:
|
||||
return
|
||||
if container_node and container_node.getContainer():
|
||||
@ -1256,13 +1264,17 @@ class MachineManager(QObject):
|
||||
else:
|
||||
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:
|
||||
extruder = self._global_container_stack.extruders[position_item]
|
||||
|
||||
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():
|
||||
current_variant_name = extruder.variant.getMetaDataEntry("name")
|
||||
current_nozzle_name = extruder.variant.getMetaDataEntry("name")
|
||||
|
||||
from UM.Settings.Interfaces import PropertyEvaluationContext
|
||||
from cura.Settings.CuraContainerStack import _ContainerIndexes
|
||||
@ -1271,7 +1283,8 @@ class MachineManager(QObject):
|
||||
material_diameter = extruder.getProperty("material_diameter", "value", context)
|
||||
candidate_materials = self._material_manager.getAvailableMaterials(
|
||||
self._global_container_stack.definition,
|
||||
current_variant_name,
|
||||
current_nozzle_name,
|
||||
buildplate_name,
|
||||
material_diameter)
|
||||
|
||||
if not candidate_materials:
|
||||
@ -1284,7 +1297,7 @@ class MachineManager(QObject):
|
||||
continue
|
||||
|
||||
# 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:
|
||||
self._setMaterial(position_item, material_node)
|
||||
|
||||
@ -1326,7 +1339,12 @@ class MachineManager(QObject):
|
||||
for extruder_configuration in configuration.extruderConfigurations:
|
||||
position = str(extruder_configuration.position)
|
||||
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:
|
||||
self._setVariantNode(position, variant_container_node)
|
||||
else:
|
||||
@ -1378,7 +1396,7 @@ class MachineManager(QObject):
|
||||
return bool(containers)
|
||||
|
||||
@pyqtSlot("QVariant")
|
||||
def setGlobalVariant(self, container_node: ContainerNode) -> None:
|
||||
def setGlobalVariant(self, container_node: "ContainerNode") -> None:
|
||||
self.blurSettings.emit()
|
||||
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
||||
self._setGlobalVariant(container_node)
|
||||
@ -1389,12 +1407,17 @@ class MachineManager(QObject):
|
||||
def setMaterialById(self, position: str, root_material_id: str) -> None:
|
||||
if self._global_container_stack is None:
|
||||
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
|
||||
position = str(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_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)
|
||||
|
||||
## 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)
|
||||
|
||||
@pyqtSlot(str, "QVariant")
|
||||
def setVariant(self, position: str, container_node: ContainerNode) -> None:
|
||||
def setVariant(self, position: str, container_node: "ContainerNode") -> None:
|
||||
position = str(position)
|
||||
self.blurSettings.emit()
|
||||
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
|
||||
# The active global_stack is treated differently.
|
||||
@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 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:
|
||||
Logger.log("e", "Could not set quality group [%s] because it has no node_for_global", str(quality_group))
|
||||
return
|
||||
# This is not changing the quality for the active machine !!!!!!!!
|
||||
global_stack.quality = quality_group.node_for_global.getContainer()
|
||||
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
|
||||
|
||||
self.blurSettings.emit()
|
||||
@ -1469,11 +1497,11 @@ class MachineManager(QObject):
|
||||
self._application.discardOrKeepProfileChanges()
|
||||
|
||||
@pyqtProperty(QObject, fset = setQualityGroup, notify = activeQualityGroupChanged)
|
||||
def activeQualityGroup(self) -> Optional[QualityGroup]:
|
||||
def activeQualityGroup(self) -> Optional["QualityGroup"]:
|
||||
return self._current_quality_group
|
||||
|
||||
@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()
|
||||
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
||||
self._setQualityChangesGroup(quality_changes_group)
|
||||
@ -1492,7 +1520,7 @@ class MachineManager(QObject):
|
||||
stack.userChanges.clear()
|
||||
|
||||
@pyqtProperty(QObject, fset = setQualityChangesGroup, notify = activeQualityChangesGroupChanged)
|
||||
def activeQualityChangesGroup(self) -> Optional[QualityChangesGroup]:
|
||||
def activeQualityChangesGroup(self) -> Optional["QualityChangesGroup"]:
|
||||
return self._current_quality_changes_group
|
||||
|
||||
@pyqtProperty(str, notify = activeQualityGroupChanged)
|
||||
|
@ -35,10 +35,9 @@ class MachineNameValidator(QObject):
|
||||
## Check if a specified machine name is allowed.
|
||||
#
|
||||
# \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
|
||||
# ``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).
|
||||
try:
|
||||
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.
|
||||
@pyqtSlot(str)
|
||||
def updateValidation(self, new_name):
|
||||
is_valid = self.validate(new_name, 0)
|
||||
is_valid = self.validate(new_name)
|
||||
if is_valid == QValidator.Acceptable:
|
||||
self.validation_regex = "^.*$" #Matches anything.
|
||||
else:
|
||||
|
41
cura/Settings/SidebarCustomMenuItemsModel.py
Normal file
41
cura/Settings/SidebarCustomMenuItemsModel.py
Normal 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
|
@ -43,7 +43,9 @@ class UserChangesModel(ListModel):
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
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.
|
||||
definition = global_stack.getBottom()
|
||||
|
56
cura/Settings/cura_empty_instance_containers.py
Normal file
56
cura/Settings/cura_empty_instance_containers.py
Normal 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"
|
||||
]
|
@ -225,7 +225,7 @@ class ThreeMFReader(MeshReader):
|
||||
|
||||
except Exception:
|
||||
Logger.logException("e", "An exception occurred in 3mf reader.")
|
||||
return []
|
||||
return None
|
||||
|
||||
return result
|
||||
|
||||
|
@ -24,6 +24,7 @@ from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
|
||||
from UM.Job import Job
|
||||
from UM.Preferences import Preferences
|
||||
|
||||
from cura.Machines.VariantType import VariantType
|
||||
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
||||
from cura.Settings.ExtruderStack import ExtruderStack
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
@ -84,14 +85,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
MimeTypeDatabase.addMimeType(
|
||||
MimeType(
|
||||
name="application/x-curaproject+xml",
|
||||
comment="Cura Project File",
|
||||
suffixes=["curaproject.3mf"]
|
||||
)
|
||||
)
|
||||
|
||||
self._supported_extensions = [".3mf"]
|
||||
self._dialog = WorkspaceDialog()
|
||||
self._3mf_mesh_reader = None
|
||||
@ -629,6 +622,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
type = "extruder_train")
|
||||
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...")
|
||||
# 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)]
|
||||
@ -720,8 +718,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
nodes = []
|
||||
|
||||
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)
|
||||
return nodes
|
||||
|
||||
@ -889,7 +885,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
parser = self._machine_info.variant_info.parser
|
||||
variant_name = parser["general"]["name"]
|
||||
|
||||
from cura.Machines.VariantManager import VariantType
|
||||
variant_type = VariantType.BUILD_PLATE
|
||||
|
||||
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
|
||||
|
||||
variant_name = parser["general"]["name"]
|
||||
from cura.Machines.VariantManager import VariantType
|
||||
variant_type = VariantType.NOZZLE
|
||||
|
||||
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 = self._old_new_materials.get(root_material_id, root_material_id)
|
||||
|
||||
build_plate_id = global_stack.variant.getId()
|
||||
|
||||
# get material diameter of this extruder
|
||||
machine_material_diameter = extruder_stack.materialDiameter
|
||||
material_node = material_manager.getMaterialNode(global_stack.definition.getId(),
|
||||
extruder_stack.variant.getName(),
|
||||
build_plate_id,
|
||||
machine_material_diameter,
|
||||
root_material_id)
|
||||
|
||||
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):
|
||||
# Clear all first
|
||||
|
@ -18,11 +18,7 @@ catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
def getMetaData() -> Dict:
|
||||
# Workaround for osx not supporting double file extensions correctly.
|
||||
if Platform.isOSX():
|
||||
workspace_extension = "3mf"
|
||||
else:
|
||||
workspace_extension = "curaproject.3mf"
|
||||
workspace_extension = "3mf"
|
||||
|
||||
metaData = {}
|
||||
if "3MFReader.ThreeMFReader" in sys.modules:
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides support for reading 3MF files.",
|
||||
"api": 4,
|
||||
"api": 5,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -15,11 +15,7 @@ from UM.Platform import Platform
|
||||
i18n_catalog = i18nCatalog("uranium")
|
||||
|
||||
def getMetaData():
|
||||
# Workarround for osx not supporting double file extensions correctly.
|
||||
if Platform.isOSX():
|
||||
workspace_extension = "3mf"
|
||||
else:
|
||||
workspace_extension = "curaproject.3mf"
|
||||
workspace_extension = "3mf"
|
||||
|
||||
metaData = {}
|
||||
|
||||
@ -36,7 +32,7 @@ def getMetaData():
|
||||
"output": [{
|
||||
"extension": workspace_extension,
|
||||
"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
|
||||
}]
|
||||
}
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides support for writing 3MF files.",
|
||||
"api": 4,
|
||||
"api": 5,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -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]
|
||||
*Bug fixes
|
||||
- Fixed an issue that would occasionally cause an unnecessary extra skin wall to be printed, which increased print time.
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Shows changes since latest checked version.",
|
||||
"api": 4,
|
||||
"api": 5,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ message Object
|
||||
bytes normals = 3; //An array of 3 floats.
|
||||
bytes indices = 4; //An array of ints.
|
||||
repeated Setting settings = 5; // Setting override per object, overruling the global settings.
|
||||
string name = 6;
|
||||
}
|
||||
|
||||
message Progress
|
||||
|
@ -1,6 +1,7 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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
|
||||
import os
|
||||
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.
|
||||
# \return list of commands and args / parameters.
|
||||
def getEngineCommand(self) -> List[str]:
|
||||
json_path = Resources.getPath(Resources.DefinitionContainers, "fdmprinter.def.json")
|
||||
return [self._application.getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", json_path, ""]
|
||||
command = [self._application.getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), ""]
|
||||
|
||||
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.
|
||||
# This also implies the slicing has finished.
|
||||
@ -334,7 +342,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||
if not self._global_container_stack:
|
||||
Logger.log("w", "Global container stack not assigned to CuraEngineBackend!")
|
||||
return
|
||||
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
|
||||
extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||
error_keys = [] #type: List[str]
|
||||
for extruder in extruders:
|
||||
error_keys.extend(extruder.getErrorKeys())
|
||||
|
@ -178,7 +178,7 @@ class ProcessSlicedLayersJob(Job):
|
||||
# Find out colors per extruder
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
manager = ExtruderManager.getInstance()
|
||||
extruders = list(manager.getMachineExtruders(global_container_stack.getId()))
|
||||
extruders = manager.getActiveExtruderStacks()
|
||||
if extruders:
|
||||
material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32)
|
||||
for extruder in extruders:
|
||||
|
@ -41,7 +41,7 @@ class StartJobResult(IntEnum):
|
||||
|
||||
## Formatter class that handles token expansion in start/end gcode
|
||||
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),
|
||||
# and a default_extruder_nr to use when no extruder_nr is specified
|
||||
|
||||
@ -220,8 +220,10 @@ class StartSliceJob(Job):
|
||||
stack = global_stack
|
||||
skip_group = False
|
||||
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")
|
||||
if not extruders_enabled[extruder_position]:
|
||||
if not is_non_printing_mesh and not extruders_enabled[extruder_position]:
|
||||
skip_group = True
|
||||
has_model_with_disabled_extruders = True
|
||||
associated_disabled_extruders.add(extruder_position)
|
||||
@ -268,7 +270,7 @@ class StartSliceJob(Job):
|
||||
|
||||
obj = group_message.addRepeatedMessage("objects")
|
||||
obj.id = id(object)
|
||||
|
||||
obj.name = object.getName()
|
||||
indices = mesh_data.getIndices()
|
||||
if indices is not None:
|
||||
flat_verts = numpy.take(verts, indices.flatten(), axis=0)
|
||||
@ -331,7 +333,7 @@ class StartSliceJob(Job):
|
||||
"-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")
|
||||
self._all_extruders_settings[str(extruder_nr)] = self._buildReplacementTokens(extruder_stack)
|
||||
|
||||
@ -438,8 +440,7 @@ class StartSliceJob(Job):
|
||||
Job.yieldThread()
|
||||
|
||||
# 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
|
||||
for key in changed_setting_keys:
|
||||
|
@ -2,7 +2,7 @@
|
||||
"name": "CuraEngine Backend",
|
||||
"author": "Ultimaker B.V.",
|
||||
"description": "Provides the link to the CuraEngine slicing backend.",
|
||||
"api": 4,
|
||||
"api": 5,
|
||||
"version": "1.0.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides support for importing Cura profiles.",
|
||||
"api": 4,
|
||||
"api": 5,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides support for exporting Cura profiles.",
|
||||
"api": 4,
|
||||
"api": 5,
|
||||
"i18n-catalog":"cura"
|
||||
}
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Checks for firmware updates.",
|
||||
"api": 4,
|
||||
"api": 5,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -4,15 +4,22 @@
|
||||
import gzip
|
||||
|
||||
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
|
||||
|
||||
|
||||
## A file reader that reads gzipped g-code.
|
||||
#
|
||||
# If you're zipping g-code, you might as well use gzip!
|
||||
class GCodeGzReader(MeshReader):
|
||||
def __init__(self) -> None:
|
||||
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"]
|
||||
|
||||
def _read(self, file_name):
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Reads g-code from a compressed archive.",
|
||||
"api": 4,
|
||||
"api": 5,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -17,6 +17,10 @@ catalog = i18nCatalog("cura")
|
||||
#
|
||||
# If you're zipping g-code, you might as well use gzip!
|
||||
class GCodeGzWriter(MeshWriter):
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(add_to_recent_files = False)
|
||||
|
||||
## Writes the gzipped g-code to a stream.
|
||||
#
|
||||
# Note that even though the function accepts a collection of nodes, the
|
||||
|
@ -16,7 +16,8 @@ def getMetaData():
|
||||
"extension": file_extension,
|
||||
"description": catalog.i18nc("@item:inlistbox", "Compressed G-code File"),
|
||||
"mime_type": "application/gzip",
|
||||
"mode": GCodeGzWriter.GCodeGzWriter.OutputMode.BinaryMode
|
||||
"mode": GCodeGzWriter.GCodeGzWriter.OutputMode.BinaryMode,
|
||||
"hide_in_file_dialog": True,
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Writes g-code to a compressed archive.",
|
||||
"api": 4,
|
||||
"api": 5,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides support for importing profiles from g-code files.",
|
||||
"api": 4,
|
||||
"api": 5,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -275,7 +275,7 @@ class FlavorParser:
|
||||
## For showing correct x, y offsets for each extruder
|
||||
def _extruderOffsets(self) -> Dict[int, List[float]]:
|
||||
result = {}
|
||||
for extruder in ExtruderManager.getInstance().getExtruderStacks():
|
||||
for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
|
||||
result[int(extruder.getMetaData().get("position", "0"))] = [
|
||||
extruder.getProperty("machine_nozzle_offset_x", "value"),
|
||||
extruder.getProperty("machine_nozzle_offset_y", "value")]
|
||||
|
@ -1,4 +1,5 @@
|
||||
# Copyright (c) 2017 Aleph Objects, Inc.
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.FileHandler.FileReader import FileReader
|
||||
@ -11,13 +12,7 @@ catalog = i18nCatalog("cura")
|
||||
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
|
||||
@ -29,7 +24,15 @@ class GCodeReader(MeshReader):
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
MimeTypeDatabase.addMimeType(
|
||||
MimeType(
|
||||
name = "application/x-cura-gcode-file",
|
||||
comment = "Cura GCode File",
|
||||
suffixes = ["gcode"]
|
||||
)
|
||||
)
|
||||
self._supported_extensions = [".gcode", ".g"]
|
||||
|
||||
self._flavor_reader = None
|
||||
|
||||
Application.getInstance().getPreferences().addPreference("gcodereader/show_caution", True)
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Victor Larchenko",
|
||||
"version": "1.0.0",
|
||||
"description": "Allows loading and displaying G-code files.",
|
||||
"api": 4,
|
||||
"api": 5,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ class GCodeWriter(MeshWriter):
|
||||
_setting_keyword = ";SETTING_"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
super().__init__(add_to_recent_files = False)
|
||||
|
||||
self._application = Application.getInstance()
|
||||
|
||||
@ -70,7 +70,7 @@ class GCodeWriter(MeshWriter):
|
||||
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
||||
scene = Application.getInstance().getController().getScene()
|
||||
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
|
||||
gcode_dict = getattr(scene, "gcode_dict")
|
||||
gcode_list = gcode_dict.get(active_build_plate, None)
|
||||
@ -86,7 +86,7 @@ class GCodeWriter(MeshWriter):
|
||||
stream.write(settings)
|
||||
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
|
||||
|
||||
## Create a new container with container 2 as base and container 1 written over it.
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Writes g-code to a file.",
|
||||
"api": 4,
|
||||
"api": 5,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Enables ability to generate printable geometry from 2D image files.",
|
||||
"api": 4,
|
||||
"api": 5,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides support for importing profiles from legacy Cura versions.",
|
||||
"api": 4,
|
||||
"api": 5,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -16,6 +16,8 @@ Cura.MachineAction
|
||||
property var extrudersModel: Cura.ExtrudersModel{}
|
||||
property int extruderTabsCount: 0
|
||||
|
||||
property var activeMachineId: Cura.MachineManager.activeMachine != null ? Cura.MachineManager.activeMachine.id : ""
|
||||
|
||||
Connections
|
||||
{
|
||||
target: base.extrudersModel
|
||||
@ -511,7 +513,7 @@ Cura.MachineAction
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return Cura.MachineManager.activeMachineId;
|
||||
return base.activeMachineId
|
||||
}
|
||||
key: settingKey
|
||||
watchedProperties: [ "value", "description" ]
|
||||
@ -564,7 +566,7 @@ Cura.MachineAction
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return Cura.MachineManager.activeMachineId;
|
||||
return base.activeMachineId
|
||||
}
|
||||
key: settingKey
|
||||
watchedProperties: [ "value", "description" ]
|
||||
@ -655,7 +657,7 @@ Cura.MachineAction
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return Cura.MachineManager.activeMachineId;
|
||||
return base.activeMachineId
|
||||
}
|
||||
key: settingKey
|
||||
watchedProperties: [ "value", "options", "description" ]
|
||||
@ -754,7 +756,7 @@ Cura.MachineAction
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return Cura.MachineManager.activeMachineId;
|
||||
return base.activeMachineId
|
||||
}
|
||||
key: settingKey
|
||||
watchedProperties: [ "value", "description" ]
|
||||
@ -879,7 +881,7 @@ Cura.MachineAction
|
||||
{
|
||||
id: machineExtruderCountProvider
|
||||
|
||||
containerStackId: Cura.MachineManager.activeMachineId
|
||||
containerStackId: base.activeMachineId
|
||||
key: "machine_extruder_count"
|
||||
watchedProperties: [ "value", "description" ]
|
||||
storeIndex: manager.containerIndex
|
||||
@ -889,7 +891,7 @@ Cura.MachineAction
|
||||
{
|
||||
id: machineHeadPolygonProvider
|
||||
|
||||
containerStackId: Cura.MachineManager.activeMachineId
|
||||
containerStackId: base.activeMachineId
|
||||
key: "machine_head_with_fans_polygon"
|
||||
watchedProperties: [ "value" ]
|
||||
storeIndex: manager.containerIndex
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "fieldOfView",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).",
|
||||
"api": 4,
|
||||
"api": 5,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
"name": "Model Checker",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "0.1",
|
||||
"api": 4,
|
||||
"api": 5,
|
||||
"description": "Checks models and print configuration for possible printing issues and give suggestions.",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides a monitor stage in Cura.",
|
||||
"api": 4,
|
||||
"api": 5,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
@ -17,7 +17,6 @@ Item {
|
||||
|
||||
width: childrenRect.width;
|
||||
height: childrenRect.height;
|
||||
|
||||
property var all_categories_except_support: [ "machine_settings", "resolution", "shell", "infill", "material", "speed",
|
||||
"travel", "cooling", "platform_adhesion", "dual", "meshfix", "blackmagic", "experimental"]
|
||||
|
||||
@ -45,7 +44,7 @@ Item {
|
||||
UM.SettingPropertyProvider
|
||||
{
|
||||
id: meshTypePropertyProvider
|
||||
containerStackId: Cura.MachineManager.activeMachineId
|
||||
containerStack: Cura.MachineManager.activeMachine
|
||||
watchedProperties: [ "enabled" ]
|
||||
}
|
||||
|
||||
@ -518,7 +517,7 @@ Item {
|
||||
{
|
||||
id: machineExtruderCount
|
||||
|
||||
containerStackId: Cura.MachineManager.activeMachineId
|
||||
containerStack: Cura.MachineManager.activeMachine
|
||||
key: "machine_extruder_count"
|
||||
watchedProperties: [ "value" ]
|
||||
storeIndex: 0
|
||||
@ -528,7 +527,7 @@ Item {
|
||||
{
|
||||
id: printSequencePropertyProvider
|
||||
|
||||
containerStackId: Cura.MachineManager.activeMachineId
|
||||
containerStack: Cura.MachineManager.activeMachine
|
||||
key: "print_sequence"
|
||||
watchedProperties: [ "value" ]
|
||||
storeIndex: 0
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides the Per Model Settings.",
|
||||
"api": 4,
|
||||
"api": 5,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -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.
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
|
||||
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
@ -260,6 +261,9 @@ class PostProcessingPlugin(QObject, Extension):
|
||||
# Create the plugin dialog component
|
||||
path = os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml")
|
||||
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.")
|
||||
|
||||
# Create the save button component
|
||||
@ -269,6 +273,9 @@ class PostProcessingPlugin(QObject, Extension):
|
||||
def showPopup(self):
|
||||
if self._view is None:
|
||||
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()
|
||||
|
||||
## Property changed: trigger re-slice
|
||||
|
@ -384,7 +384,7 @@ UM.Dialog
|
||||
UM.SettingPropertyProvider
|
||||
{
|
||||
id: inheritStackProvider
|
||||
containerStackId: Cura.MachineManager.activeMachineId
|
||||
containerStack: Cura.MachineManager.activeMachine
|
||||
key: model.key ? model.key : "None"
|
||||
watchedProperties: [ "limit_to_extruder" ]
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
"name": "Post Processing",
|
||||
"author": "Ultimaker",
|
||||
"version": "2.2",
|
||||
"api": 4,
|
||||
"api": 5,
|
||||
"description": "Extension that allows for user created scripts for post processing",
|
||||
"catalog": "cura"
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user