mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-05-19 09:10:47 +08:00
Merge branch 'master' of https://github.com/Ultimaker/Cura into first_layer_line_width
This commit is contained in:
commit
ea35fdfd30
8
.gitignore
vendored
8
.gitignore
vendored
@ -11,6 +11,10 @@ resources/firmware
|
||||
resources/materials
|
||||
LC_MESSAGES
|
||||
.cache
|
||||
*.qmlc
|
||||
|
||||
#MacOS
|
||||
.DS_Store
|
||||
|
||||
# Editors and IDEs.
|
||||
*kdev*
|
||||
@ -26,9 +30,6 @@ cura.desktop
|
||||
.pydevproject
|
||||
.settings
|
||||
|
||||
# Debian packaging
|
||||
debian*
|
||||
|
||||
#Externally located plug-ins.
|
||||
plugins/Doodle3D-cura-plugin
|
||||
plugins/GodMode
|
||||
@ -37,6 +38,7 @@ plugins/X3GWriter
|
||||
plugins/FlatProfileExporter
|
||||
plugins/ProfileFlattener
|
||||
plugins/cura-god-mode-plugin
|
||||
plugins/cura-big-flame-graph
|
||||
|
||||
#Build stuff
|
||||
CMakeCache.txt
|
||||
|
9
Jenkinsfile
vendored
9
Jenkinsfile
vendored
@ -14,9 +14,14 @@ parallel_nodes(['linux && cura', 'windows && cura']) {
|
||||
dir('build') {
|
||||
// Perform the "build". Since Uranium is Python code, this basically only ensures CMake is setup.
|
||||
stage('Build') {
|
||||
def branch = env.BRANCH_NAME
|
||||
if(!(branch =~ /^2.\d+$/)) {
|
||||
branch = "master"
|
||||
}
|
||||
|
||||
// Ensure CMake is setup. Note that since this is Python code we do not really "build" it.
|
||||
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/master")
|
||||
cmake("..", "-DCMAKE_PREFIX_PATH=${env.CURA_ENVIRONMENT_PATH} -DCMAKE_BUILD_TYPE=Release -DURANIUM_DIR=${uranium_dir}")
|
||||
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
|
||||
cmake("..", "-DCMAKE_PREFIX_PATH=${env.CURA_ENVIRONMENT_PATH}/${branch} -DCMAKE_BUILD_TYPE=Release -DURANIUM_DIR=${uranium_dir}")
|
||||
}
|
||||
|
||||
// Try and run the unit tests. If this stage fails, we consider the build to be "unstable".
|
||||
|
11
README.md
11
README.md
@ -14,10 +14,11 @@ For crashes and similar issues, please attach the following information:
|
||||
|
||||
* (On Windows) The log as produced by dxdiag (start -> run -> dxdiag -> save output)
|
||||
* The Cura GUI log file, located at
|
||||
* $User/AppData/Local/cura/cura.log (Windows)
|
||||
* $User/Library/Application Support/cura (OSX)
|
||||
* $USER/.local/share/cura (Ubuntu/Linux)
|
||||
* The Cura Engine log, using Help -> Show Engine Log
|
||||
* %APPDATA%\cura\\`<Cura version>`\cura.log (Windows), or usually C:\Users\\`<your username>`\AppData\Roaming\cura\\`<Cura version>`\cura.log
|
||||
* $User/Library/Application Support/cura/`<Cura version>`/cura.log (OSX)
|
||||
* $USER/.local/share/cura/`<Cura version>`/cura.log (Ubuntu/Linux)
|
||||
|
||||
If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
@ -44,13 +45,13 @@ Please checkout [cura-build](https://github.com/Ultimaker/cura-build)
|
||||
|
||||
Third party plugins
|
||||
-------------
|
||||
* [Print Cost Calculator](https://github.com/nallath/PrintCostCalculator): Calculates weight and monetary cost of your print.
|
||||
* [Post Processing Plugin](https://github.com/nallath/PostProcessingPlugin): Allows for post-processing scripts to run on g-code.
|
||||
* [Barbarian Plugin](https://github.com/nallath/BarbarianPlugin): Simple scale tool for imperial to metric.
|
||||
* [X3G Writer](https://github.com/Ghostkeeper/X3GWriter): Adds support for exporting X3G files.
|
||||
* [Auto orientation](https://github.com/nallath/CuraOrientationPlugin): Calculate the optimal orientation for a model.
|
||||
* [OctoPrint Plugin](https://github.com/fieldofview/OctoPrintPlugin): Send printjobs directly to OctoPrint and monitor their progress in Cura.
|
||||
* [WirelessPrinting Plugin](https://github.com/probonopd/WirelessPrinting): Print wirelessly from Cura to your 3D printer connected to an ESP8266 module.
|
||||
* [Electric Print Cost Calculator Plugin](https://github.com/zoff99/ElectricPrintCostCalculator): Calculate the electric costs of a print.
|
||||
|
||||
Making profiles for other printers
|
||||
----------------------------------
|
||||
|
@ -104,7 +104,6 @@ class BuildVolume(SceneNode):
|
||||
# but it does not update the disallowed areas after material change
|
||||
Application.getInstance().getMachineManager().activeStackChanged.connect(self._onStackChanged)
|
||||
|
||||
|
||||
def _onSceneChanged(self, source):
|
||||
if self._global_container_stack:
|
||||
self._change_timer.start()
|
||||
@ -433,7 +432,8 @@ class BuildVolume(SceneNode):
|
||||
self._global_container_stack.getProperty("raft_interface_thickness", "value") +
|
||||
self._global_container_stack.getProperty("raft_surface_layers", "value") *
|
||||
self._global_container_stack.getProperty("raft_surface_thickness", "value") +
|
||||
self._global_container_stack.getProperty("raft_airgap", "value"))
|
||||
self._global_container_stack.getProperty("raft_airgap", "value") -
|
||||
self._global_container_stack.getProperty("layer_0_z_overlap", "value"))
|
||||
|
||||
# Rounding errors do not matter, we check if raft_thickness has changed at all
|
||||
if old_raft_thickness != self._raft_thickness:
|
||||
@ -562,7 +562,7 @@ class BuildVolume(SceneNode):
|
||||
used_extruders = [self._global_container_stack]
|
||||
|
||||
result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders) #Normal machine disallowed areas can always be added.
|
||||
prime_areas = self._computeDisallowedAreasPrime(disallowed_border_size, used_extruders)
|
||||
prime_areas = self._computeDisallowedAreasPrimeBlob(disallowed_border_size, used_extruders)
|
||||
prime_disallowed_areas = self._computeDisallowedAreasStatic(0, used_extruders) #Where the priming is not allowed to happen. This is not added to the result, just for collision checking.
|
||||
|
||||
#Check if prime positions intersect with disallowed areas.
|
||||
@ -636,7 +636,7 @@ class BuildVolume(SceneNode):
|
||||
result[extruder.getId()] = []
|
||||
|
||||
#Currently, the only normally printed object is the prime tower.
|
||||
if ExtruderManager.getInstance().getResolveOrValue("prime_tower_enable") == True:
|
||||
if ExtruderManager.getInstance().getResolveOrValue("prime_tower_enable"):
|
||||
prime_tower_size = self._global_container_stack.getProperty("prime_tower_size", "value")
|
||||
machine_width = self._global_container_stack.getProperty("machine_width", "value")
|
||||
machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
|
||||
@ -658,7 +658,7 @@ class BuildVolume(SceneNode):
|
||||
|
||||
return result
|
||||
|
||||
## Computes the disallowed areas for the prime locations.
|
||||
## Computes the disallowed areas for the prime blobs.
|
||||
#
|
||||
# These are special because they are not subject to things like brim or
|
||||
# travel avoidance. They do get a dilute with the border size though
|
||||
@ -669,17 +669,18 @@ class BuildVolume(SceneNode):
|
||||
# \param used_extruders The extruder stacks to generate disallowed areas
|
||||
# for.
|
||||
# \return A dictionary with for each used extruder ID the prime areas.
|
||||
def _computeDisallowedAreasPrime(self, border_size, used_extruders):
|
||||
def _computeDisallowedAreasPrimeBlob(self, border_size, used_extruders):
|
||||
result = {}
|
||||
|
||||
machine_width = self._global_container_stack.getProperty("machine_width", "value")
|
||||
machine_depth = self._global_container_stack.getProperty("machine_depth", "value")
|
||||
for extruder in used_extruders:
|
||||
prime_blob_enabled = extruder.getProperty("prime_blob_enable", "value")
|
||||
prime_x = extruder.getProperty("extruder_prime_pos_x", "value")
|
||||
prime_y = - extruder.getProperty("extruder_prime_pos_y", "value")
|
||||
prime_y = -extruder.getProperty("extruder_prime_pos_y", "value")
|
||||
|
||||
#Ignore extruder prime position if it is not set
|
||||
if prime_x == 0 and prime_y == 0:
|
||||
#Ignore extruder prime position if it is not set or if blob is disabled
|
||||
if (prime_x == 0 and prime_y == 0) or not prime_blob_enabled:
|
||||
result[extruder.getId()] = []
|
||||
continue
|
||||
|
||||
@ -715,6 +716,11 @@ class BuildVolume(SceneNode):
|
||||
polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(border_size))
|
||||
machine_disallowed_polygons.append(polygon)
|
||||
|
||||
# For certain machines we don't need to compute disallowed areas for each nozzle.
|
||||
# So we check here and only do the nozzle offsetting if needed.
|
||||
nozzle_offsetting_for_disallowed_areas = self._global_container_stack.getMetaDataEntry(
|
||||
"nozzle_offsetting_for_disallowed_areas", True)
|
||||
|
||||
result = {}
|
||||
for extruder in used_extruders:
|
||||
extruder_id = extruder.getId()
|
||||
@ -724,6 +730,8 @@ class BuildVolume(SceneNode):
|
||||
offset_y = extruder.getProperty("machine_nozzle_offset_y", "value")
|
||||
if offset_y is None:
|
||||
offset_y = 0
|
||||
else:
|
||||
offset_y = -offset_y
|
||||
result[extruder_id] = []
|
||||
|
||||
for polygon in machine_disallowed_polygons:
|
||||
@ -734,14 +742,17 @@ class BuildVolume(SceneNode):
|
||||
right_unreachable_border = 0
|
||||
top_unreachable_border = 0
|
||||
bottom_unreachable_border = 0
|
||||
#The build volume is defined as the union of the area that all extruders can reach, so we need to know the relative offset to all extruders.
|
||||
for other_extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
|
||||
other_offset_x = other_extruder.getProperty("machine_nozzle_offset_x", "value")
|
||||
other_offset_y = other_extruder.getProperty("machine_nozzle_offset_y", "value")
|
||||
left_unreachable_border = min(left_unreachable_border, other_offset_x - offset_x)
|
||||
right_unreachable_border = max(right_unreachable_border, other_offset_x - offset_x)
|
||||
top_unreachable_border = min(top_unreachable_border, other_offset_y - offset_y)
|
||||
bottom_unreachable_border = max(bottom_unreachable_border, other_offset_y - offset_y)
|
||||
|
||||
# Only do nozzle offsetting if needed
|
||||
if nozzle_offsetting_for_disallowed_areas:
|
||||
#The build volume is defined as the union of the area that all extruders can reach, so we need to know the relative offset to all extruders.
|
||||
for other_extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
|
||||
other_offset_x = other_extruder.getProperty("machine_nozzle_offset_x", "value")
|
||||
other_offset_y = -other_extruder.getProperty("machine_nozzle_offset_y", "value")
|
||||
left_unreachable_border = min(left_unreachable_border, other_offset_x - offset_x)
|
||||
right_unreachable_border = max(right_unreachable_border, other_offset_x - offset_x)
|
||||
top_unreachable_border = min(top_unreachable_border, other_offset_y - offset_y)
|
||||
bottom_unreachable_border = max(bottom_unreachable_border, other_offset_y - offset_y)
|
||||
half_machine_width = self._global_container_stack.getProperty("machine_width", "value") / 2
|
||||
half_machine_depth = self._global_container_stack.getProperty("machine_depth", "value") / 2
|
||||
|
||||
@ -869,7 +880,7 @@ class BuildVolume(SceneNode):
|
||||
else:
|
||||
extruder_index = self._global_container_stack.getProperty(extruder_setting_key, "value")
|
||||
|
||||
if extruder_index == "-1": # If extruder index is -1 use global instead
|
||||
if str(extruder_index) == "-1": # If extruder index is -1 use global instead
|
||||
stack = self._global_container_stack
|
||||
else:
|
||||
extruder_stack_id = ExtruderManager.getInstance().extruderIds[str(extruder_index)]
|
||||
@ -950,9 +961,9 @@ class BuildVolume(SceneNode):
|
||||
return max(min(value, max_value), min_value)
|
||||
|
||||
_skirt_settings = ["adhesion_type", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "brim_width", "brim_line_count", "raft_margin", "draft_shield_enabled", "draft_shield_dist"]
|
||||
_raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap"]
|
||||
_raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"]
|
||||
_extra_z_settings = ["retraction_hop_enabled", "retraction_hop"]
|
||||
_prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z"]
|
||||
_prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z", "prime_blob_enable"]
|
||||
_tower_settings = ["prime_tower_enable", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y"]
|
||||
_ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
|
||||
_distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts"]
|
||||
|
@ -257,7 +257,11 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
# \return New Polygon instance that is offset with everything that
|
||||
# influences the collision area.
|
||||
def _offsetHull(self, convex_hull):
|
||||
horizontal_expansion = self._getSettingProperty("xy_offset", "value")
|
||||
horizontal_expansion = max(
|
||||
self._getSettingProperty("xy_offset", "value"),
|
||||
self._getSettingProperty("xy_offset_layer_0", "value")
|
||||
)
|
||||
|
||||
mold_width = 0
|
||||
if self._getSettingProperty("mold_enabled", "value"):
|
||||
mold_width = self._getSettingProperty("mold_width", "value")
|
||||
@ -298,7 +302,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
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, property="value"):
|
||||
def _getSettingProperty(self, setting_key, property = "value"):
|
||||
per_mesh_stack = self._node.callDecoration("getStack")
|
||||
if per_mesh_stack:
|
||||
return per_mesh_stack.getProperty(setting_key, property)
|
||||
@ -314,10 +318,8 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
extruder_stack_id = ExtruderManager.getInstance().extruderIds["0"]
|
||||
extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
|
||||
return extruder_stack.getProperty(setting_key, property)
|
||||
else: #Limit_to_extruder is set. Use that one.
|
||||
extruder_stack_id = ExtruderManager.getInstance().extruderIds[str(extruder_index)]
|
||||
stack = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
|
||||
return stack.getProperty(setting_key, property)
|
||||
else: #Limit_to_extruder is set. The global stack handles this then.
|
||||
return self._global_stack.getProperty(setting_key, property)
|
||||
|
||||
## Returns true if node is a descendant or the same as the root node.
|
||||
def __isDescendant(self, root, node):
|
||||
@ -328,11 +330,10 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
return self.__isDescendant(root, node.getParent())
|
||||
|
||||
_affected_settings = [
|
||||
"adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers",
|
||||
"raft_surface_thickness", "raft_airgap", "raft_margin", "print_sequence",
|
||||
"adhesion_type", "raft_margin", "print_sequence",
|
||||
"skirt_gap", "skirt_line_count", "skirt_brim_line_width", "skirt_distance", "brim_line_count"]
|
||||
|
||||
## Settings that change the convex hull.
|
||||
#
|
||||
# If these settings change, the convex hull should be recalculated.
|
||||
_influencing_settings = {"xy_offset", "mold_enabled", "mold_width"}
|
||||
_influencing_settings = {"xy_offset", "xy_offset_layer_0", "mold_enabled", "mold_width"}
|
||||
|
@ -2,6 +2,9 @@ import sys
|
||||
import platform
|
||||
import traceback
|
||||
import webbrowser
|
||||
import faulthandler
|
||||
import tempfile
|
||||
import os
|
||||
import urllib
|
||||
|
||||
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, Qt, QCoreApplication
|
||||
@ -91,6 +94,17 @@ def show(exception_type, value, tb):
|
||||
crash_info = "Version: {0}\nPlatform: {1}\nQt: {2}\nPyQt: {3}\n\nException:\n{4}"
|
||||
crash_info = crash_info.format(version, platform.platform(), QT_VERSION_STR, PYQT_VERSION_STR, trace)
|
||||
|
||||
tmp_file_fd, tmp_file_path = tempfile.mkstemp(prefix = "cura-crash", text = True)
|
||||
os.close(tmp_file_fd)
|
||||
with open(tmp_file_path, "w") as f:
|
||||
faulthandler.dump_traceback(f, all_threads=True)
|
||||
with open(tmp_file_path, "r") as f:
|
||||
data = f.read()
|
||||
|
||||
msg = "-------------------------\n"
|
||||
msg += data
|
||||
crash_info += "\n\n" + msg
|
||||
|
||||
textarea.setText(crash_info)
|
||||
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.Close, dialog)
|
||||
|
@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtNetwork import QLocalServer
|
||||
from PyQt5.QtNetwork import QLocalSocket
|
||||
|
||||
@ -25,7 +26,6 @@ from UM.Settings.Validator import Validator
|
||||
from UM.Message import Message
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Workspace.WorkspaceReader import WorkspaceReader
|
||||
from UM.Platform import Platform
|
||||
from UM.Decorators import deprecated
|
||||
|
||||
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
|
||||
@ -47,6 +47,7 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
from cura.Settings.MachineNameValidator import MachineNameValidator
|
||||
from cura.Settings.ProfilesModel import ProfilesModel
|
||||
from cura.Settings.MaterialsModel import MaterialsModel
|
||||
from cura.Settings.QualityAndUserProfilesModel import QualityAndUserProfilesModel
|
||||
from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
|
||||
from cura.Settings.UserProfilesModel import UserProfilesModel
|
||||
@ -62,6 +63,7 @@ from . import CameraImageProvider
|
||||
from . import MachineActionManager
|
||||
|
||||
from cura.Settings.MachineManager import MachineManager
|
||||
from cura.Settings.MaterialManager import MaterialManager
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from cura.Settings.UserChangesModel import UserChangesModel
|
||||
from cura.Settings.ExtrudersModel import ExtrudersModel
|
||||
@ -99,6 +101,11 @@ if not MYPY:
|
||||
|
||||
|
||||
class CuraApplication(QtApplication):
|
||||
# SettingVersion represents the set of settings available in the machine/extruder definitions.
|
||||
# You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
|
||||
# changes of the settings.
|
||||
SettingVersion = 2
|
||||
|
||||
class ResourceTypes:
|
||||
QmlFiles = Resources.UserType + 1
|
||||
Firmware = Resources.UserType + 2
|
||||
@ -133,13 +140,14 @@ class CuraApplication(QtApplication):
|
||||
# From which stack the setting would inherit if not defined per object (handled in the engine)
|
||||
# AND for settings which are not settable_per_mesh:
|
||||
# which extruder is the only extruder this setting is obtained from
|
||||
SettingDefinition.addSupportedProperty("limit_to_extruder", DefinitionPropertyType.Function, default = "-1")
|
||||
SettingDefinition.addSupportedProperty("limit_to_extruder", DefinitionPropertyType.Function, default = "-1", depends_on = "value")
|
||||
|
||||
# For settings which are not settable_per_mesh and not settable_per_extruder:
|
||||
# A function which determines the glabel/meshgroup value by looking at the values of the setting in all (used) extruders
|
||||
SettingDefinition.addSupportedProperty("resolve", DefinitionPropertyType.Function, default = None, depends_on = "value")
|
||||
|
||||
SettingDefinition.addSettingType("extruder", None, str, Validator)
|
||||
SettingDefinition.addSettingType("optional_extruder", None, str, None)
|
||||
|
||||
SettingDefinition.addSettingType("[int]", None, str, None)
|
||||
|
||||
@ -169,11 +177,12 @@ class CuraApplication(QtApplication):
|
||||
|
||||
UM.VersionUpgradeManager.VersionUpgradeManager.getInstance().setCurrentVersions(
|
||||
{
|
||||
("quality", InstanceContainer.Version): (self.ResourceTypes.QualityInstanceContainer, "application/x-uranium-instancecontainer"),
|
||||
("machine_stack", ContainerStack.Version): (self.ResourceTypes.MachineStack, "application/x-uranium-containerstack"),
|
||||
("extruder_train", ContainerStack.Version): (self.ResourceTypes.ExtruderStack, "application/x-uranium-extruderstack"),
|
||||
("preferences", Preferences.Version): (Resources.Preferences, "application/x-uranium-preferences"),
|
||||
("user", InstanceContainer.Version): (self.ResourceTypes.UserInstanceContainer, "application/x-uranium-instancecontainer")
|
||||
("quality_changes", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.QualityInstanceContainer, "application/x-uranium-instancecontainer"),
|
||||
("machine_stack", ContainerStack.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.MachineStack, "application/x-cura-globalstack"),
|
||||
("extruder_train", ContainerStack.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.ExtruderStack, "application/x-cura-extruderstack"),
|
||||
("preferences", Preferences.Version * 1000000 + self.SettingVersion): (Resources.Preferences, "application/x-uranium-preferences"),
|
||||
("user", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.UserInstanceContainer, "application/x-uranium-instancecontainer"),
|
||||
("definition_changes", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.DefinitionChangesContainer, "application/x-uranium-instancecontainer"),
|
||||
}
|
||||
)
|
||||
|
||||
@ -182,6 +191,7 @@ class CuraApplication(QtApplication):
|
||||
|
||||
self._machine_action_manager = MachineActionManager.MachineActionManager()
|
||||
self._machine_manager = None # This is initialized on demand.
|
||||
self._material_manager = None
|
||||
self._setting_inheritance_manager = None
|
||||
|
||||
self._additional_components = {} # Components to add to certain areas in the interface
|
||||
@ -257,11 +267,14 @@ class CuraApplication(QtApplication):
|
||||
with ContainerRegistry.getInstance().lockFile():
|
||||
ContainerRegistry.getInstance().load()
|
||||
|
||||
# set the setting version for Preferences
|
||||
Preferences.getInstance().addPreference("metadata/setting_version", CuraApplication.SettingVersion)
|
||||
|
||||
Preferences.getInstance().addPreference("cura/active_mode", "simple")
|
||||
|
||||
Preferences.getInstance().addPreference("cura/categories_expanded", "")
|
||||
Preferences.getInstance().addPreference("cura/jobname_prefix", True)
|
||||
Preferences.getInstance().addPreference("view/center_on_select", False)
|
||||
Preferences.getInstance().addPreference("view/center_on_select", True)
|
||||
Preferences.getInstance().addPreference("mesh/scale_to_fit", False)
|
||||
Preferences.getInstance().addPreference("mesh/scale_tiny_meshes", True)
|
||||
Preferences.getInstance().addPreference("cura/dialog_on_project_save", True)
|
||||
@ -294,6 +307,7 @@ class CuraApplication(QtApplication):
|
||||
z_seam_y
|
||||
infill
|
||||
infill_sparse_density
|
||||
gradual_infill_steps
|
||||
material
|
||||
material_print_temperature
|
||||
material_bed_temperature
|
||||
@ -314,7 +328,6 @@ class CuraApplication(QtApplication):
|
||||
support_enable
|
||||
support_extruder_nr
|
||||
support_type
|
||||
support_interface_density
|
||||
platform_adhesion
|
||||
adhesion_type
|
||||
adhesion_extruder_nr
|
||||
@ -331,6 +344,7 @@ class CuraApplication(QtApplication):
|
||||
blackmagic
|
||||
print_sequence
|
||||
infill_mesh
|
||||
cutting_mesh
|
||||
experimental
|
||||
""".replace("\n", ";").replace(" ", ""))
|
||||
|
||||
@ -340,6 +354,8 @@ class CuraApplication(QtApplication):
|
||||
self.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||
self._onGlobalContainerChanged()
|
||||
|
||||
self._plugin_registry.addSupportedPluginExtension("curaplugin", "Cura Plugin")
|
||||
|
||||
def _onEngineCreated(self):
|
||||
self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
|
||||
|
||||
@ -481,7 +497,7 @@ class CuraApplication(QtApplication):
|
||||
|
||||
self._plugin_registry.loadPlugins()
|
||||
|
||||
if self.getBackend() == None:
|
||||
if self.getBackend() is None:
|
||||
raise RuntimeError("Could not load the backend plugin!")
|
||||
|
||||
self._plugins_loaded = True
|
||||
@ -618,7 +634,9 @@ class CuraApplication(QtApplication):
|
||||
camera.lookAt(Vector(0, 0, 0))
|
||||
controller.getScene().setActiveCamera("3d")
|
||||
|
||||
self.getController().getTool("CameraTool").setOrigin(Vector(0, 100, 0))
|
||||
camera_tool = self.getController().getTool("CameraTool")
|
||||
camera_tool.setOrigin(Vector(0, 100, 0))
|
||||
camera_tool.setZoomRange(0.1, 200000)
|
||||
|
||||
self._camera_animation = CameraAnimation.CameraAnimation()
|
||||
self._camera_animation.setCameraTool(self.getController().getTool("CameraTool"))
|
||||
@ -628,6 +646,7 @@ class CuraApplication(QtApplication):
|
||||
# Initialise extruder so as to listen to global container stack changes before the first global container stack is set.
|
||||
ExtruderManager.getInstance()
|
||||
qmlRegisterSingletonType(MachineManager, "Cura", 1, 0, "MachineManager", self.getMachineManager)
|
||||
qmlRegisterSingletonType(MaterialManager, "Cura", 1, 0, "MaterialManager", self.getMaterialManager)
|
||||
qmlRegisterSingletonType(SettingInheritanceManager, "Cura", 1, 0, "SettingInheritanceManager",
|
||||
self.getSettingInheritanceManager)
|
||||
|
||||
@ -653,6 +672,11 @@ class CuraApplication(QtApplication):
|
||||
self._machine_manager = MachineManager.createMachineManager()
|
||||
return self._machine_manager
|
||||
|
||||
def getMaterialManager(self, *args):
|
||||
if self._material_manager is None:
|
||||
self._material_manager = MaterialManager.createMaterialManager()
|
||||
return self._material_manager
|
||||
|
||||
def getSettingInheritanceManager(self, *args):
|
||||
if self._setting_inheritance_manager is None:
|
||||
self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager()
|
||||
@ -696,6 +720,7 @@ class CuraApplication(QtApplication):
|
||||
|
||||
qmlRegisterType(ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel")
|
||||
qmlRegisterSingletonType(ProfilesModel, "Cura", 1, 0, "ProfilesModel", ProfilesModel.createProfilesModel)
|
||||
qmlRegisterType(MaterialsModel, "Cura", 1, 0, "MaterialsModel")
|
||||
qmlRegisterType(QualityAndUserProfilesModel, "Cura", 1, 0, "QualityAndUserProfilesModel")
|
||||
qmlRegisterType(UserProfilesModel, "Cura", 1, 0, "UserProfilesModel")
|
||||
qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
|
||||
@ -739,8 +764,7 @@ class CuraApplication(QtApplication):
|
||||
# Default
|
||||
self.getController().setActiveTool("TranslateTool")
|
||||
|
||||
# Hack: QVector bindings are broken on PyQt 5.7.1 on Windows. This disables it being called at all.
|
||||
if Preferences.getInstance().getValue("view/center_on_select") and not Platform.isWindows():
|
||||
if Preferences.getInstance().getValue("view/center_on_select"):
|
||||
self._center_after_select = True
|
||||
else:
|
||||
if self.getController().getActiveTool():
|
||||
|
@ -69,7 +69,7 @@ class LayerDataBuilder(MeshBuilder):
|
||||
|
||||
vertex_offset = 0
|
||||
index_offset = 0
|
||||
for layer, data in self._layers.items():
|
||||
for layer, data in sorted(self._layers.items()):
|
||||
( vertex_offset, index_offset ) = data.build( vertex_offset, index_offset, vertices, colors, line_dimensions, extruders, line_types, indices)
|
||||
self._element_counts[layer] = data.elementCount
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
from UM.Math.Color import Color
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.Application import Application
|
||||
from typing import Any
|
||||
import numpy
|
||||
@ -16,8 +18,9 @@ class LayerPolygon:
|
||||
MoveCombingType = 8
|
||||
MoveRetractionType = 9
|
||||
SupportInterfaceType = 10
|
||||
|
||||
__jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(11) == NoneType, numpy.arange(11) == MoveCombingType), numpy.arange(11) == MoveRetractionType)
|
||||
__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
|
||||
@ -28,6 +31,9 @@ class LayerPolygon:
|
||||
def __init__(self, extruder, line_types, data, line_widths, line_thicknesses):
|
||||
self._extruder = extruder
|
||||
self._types = line_types
|
||||
for i in range(len(self._types)):
|
||||
if self._types[i] >= self.__number_of_types: #Got faulty line data from the engine.
|
||||
self._types[i] = self.NoneType
|
||||
self._data = data
|
||||
self._line_widths = line_widths
|
||||
self._line_thicknesses = line_thicknesses
|
||||
@ -36,11 +42,11 @@ class LayerPolygon:
|
||||
self._vertex_end = 0
|
||||
self._index_begin = 0
|
||||
self._index_end = 0
|
||||
|
||||
|
||||
self._jump_mask = self.__jump_map[self._types]
|
||||
self._jump_count = numpy.sum(self._jump_mask)
|
||||
self._mesh_line_count = len(self._types)-self._jump_count
|
||||
self._vertex_count = self._mesh_line_count + numpy.sum( self._types[1:] == self._types[:-1])
|
||||
self._mesh_line_count = len(self._types) - self._jump_count
|
||||
self._vertex_count = self._mesh_line_count + numpy.sum(self._types[1:] == self._types[:-1])
|
||||
|
||||
# Buffering the colors shouldn't be necessary as it is not
|
||||
# re-used and can save alot of memory usage.
|
||||
|
@ -23,8 +23,8 @@ class PlatformPhysicsOperation(Operation):
|
||||
def mergeWith(self, other):
|
||||
group = GroupedOperation()
|
||||
|
||||
group.addOperation(self)
|
||||
group.addOperation(other)
|
||||
group.addOperation(self)
|
||||
|
||||
return group
|
||||
|
||||
|
@ -31,8 +31,8 @@ catalog = i18nCatalog("cura")
|
||||
# - This triggers a new slice with the current settings - this is the "current settings pass".
|
||||
# - When the slice is done, we update the current print time and material amount.
|
||||
# - If the source of the slice was not a Setting change, we start the second slice pass, the "low quality settings pass". Otherwise we stop here.
|
||||
# - When that is done, we update the minimum print time and start the final slice pass, the "high quality settings pass".
|
||||
# - When the high quality pass is done, we update the maximum print time.
|
||||
# - When that is done, we update the minimum print time and start the final slice pass, the "Extra Fine settings pass".
|
||||
# - When the Extra Fine pass is done, we update the maximum print time.
|
||||
#
|
||||
# This class also mangles the current machine name and the filename of the first loaded mesh into a job name.
|
||||
# This job name is requested by the JobSpecs qml file.
|
||||
@ -52,6 +52,19 @@ class PrintInformation(QObject):
|
||||
super().__init__(parent)
|
||||
|
||||
self._current_print_time = Duration(None, self)
|
||||
self._print_times_per_feature = {
|
||||
"none": Duration(None, self),
|
||||
"inset_0": Duration(None, self),
|
||||
"inset_x": Duration(None, self),
|
||||
"skin": Duration(None, self),
|
||||
"support": Duration(None, self),
|
||||
"skirt": Duration(None, self),
|
||||
"infill": Duration(None, self),
|
||||
"support_infill": Duration(None, self),
|
||||
"travel": Duration(None, self),
|
||||
"retract": Duration(None, self),
|
||||
"support_interface": Duration(None, self)
|
||||
}
|
||||
|
||||
self._material_lengths = []
|
||||
self._material_weights = []
|
||||
@ -93,6 +106,10 @@ class PrintInformation(QObject):
|
||||
def currentPrintTime(self):
|
||||
return self._current_print_time
|
||||
|
||||
@pyqtProperty("QVariantMap", notify = currentPrintTimeChanged)
|
||||
def printTimesPerFeature(self):
|
||||
return self._print_times_per_feature
|
||||
|
||||
materialLengthsChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty("QVariantList", notify = materialLengthsChanged)
|
||||
@ -111,12 +128,16 @@ class PrintInformation(QObject):
|
||||
def materialCosts(self):
|
||||
return self._material_costs
|
||||
|
||||
def _onPrintDurationMessage(self, total_time, material_amounts):
|
||||
if total_time != total_time: # Check for NaN. Engine can sometimes give us weird values.
|
||||
Logger.log("w", "Received NaN for print duration message")
|
||||
self._current_print_time.setDuration(0)
|
||||
else:
|
||||
self._current_print_time.setDuration(total_time)
|
||||
def _onPrintDurationMessage(self, time_per_feature, material_amounts):
|
||||
total_time = 0
|
||||
for feature, time in time_per_feature.items():
|
||||
if time != time: # Check for NaN. Engine can sometimes give us weird values.
|
||||
self._print_times_per_feature[feature].setDuration(0)
|
||||
Logger.log("w", "Received NaN for print duration message")
|
||||
continue
|
||||
total_time += time
|
||||
self._print_times_per_feature[feature].setDuration(time)
|
||||
self._current_print_time.setDuration(total_time)
|
||||
|
||||
self.currentPrintTimeChanged.emit()
|
||||
|
||||
|
@ -3,13 +3,18 @@
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.OutputDevice.OutputDevice import OutputDevice
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, pyqtSignal, QUrl
|
||||
from PyQt5.QtQml import QQmlComponent, QQmlContext
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
from enum import IntEnum # For the connection state tracking.
|
||||
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Logger import Logger
|
||||
from UM.Signal import signalemitter
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.Application import Application
|
||||
|
||||
import os
|
||||
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
@ -57,6 +62,11 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||
|
||||
self._camera_active = False
|
||||
|
||||
self._monitor_view_qml_path = ""
|
||||
self._monitor_component = None
|
||||
self._monitor_item = None
|
||||
self._qml_context = None
|
||||
|
||||
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None):
|
||||
raise NotImplementedError("requestWrite needs to be implemented")
|
||||
|
||||
@ -111,6 +121,32 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||
# Signal to be emitted when some drastic change occurs in the remaining time (not when the time just passes on normally).
|
||||
preheatBedRemainingTimeChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(QObject, constant=True)
|
||||
def monitorItem(self):
|
||||
# Note that we specifically only check if the monitor component is created.
|
||||
# It could be that it failed to actually create the qml item! If we check if the item was created, it will try to
|
||||
# create the item (and fail) every time.
|
||||
if not self._monitor_component:
|
||||
self._createMonitorViewFromQML()
|
||||
|
||||
return self._monitor_item
|
||||
|
||||
def _createMonitorViewFromQML(self):
|
||||
path = QUrl.fromLocalFile(self._monitor_view_qml_path)
|
||||
|
||||
# Because of garbage collection we need to keep this referenced by python.
|
||||
self._monitor_component = QQmlComponent(Application.getInstance()._engine, path)
|
||||
|
||||
# Check if the context was already requested before (Printer output device might have multiple items in the future)
|
||||
if self._qml_context is None:
|
||||
self._qml_context = QQmlContext(Application.getInstance()._engine.rootContext())
|
||||
self._qml_context.setContextProperty("OutputDevice", self)
|
||||
|
||||
self._monitor_item = self._monitor_component.create(self._qml_context)
|
||||
if self._monitor_item is None:
|
||||
Logger.log("e", "QQmlComponent status %s", self._monitor_component.status())
|
||||
Logger.log("e", "QQmlComponent error string %s", self._monitor_component.errorString())
|
||||
|
||||
@pyqtProperty(str, notify=printerTypeChanged)
|
||||
def printerType(self):
|
||||
return self._printer_type
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
# This collects a lot of quality and quality changes related code which was split between ContainerManager
|
||||
# and the MachineManager and really needs to usable from both.
|
||||
from typing import List
|
||||
from typing import List, Optional, Dict, TYPE_CHECKING
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
@ -11,6 +11,10 @@ from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from cura.Settings.ExtruderStack import ExtruderStack
|
||||
from UM.Settings.DefinitionContainer import DefinitionContainerInterface
|
||||
|
||||
class QualityManager:
|
||||
|
||||
@ -27,12 +31,12 @@ class QualityManager:
|
||||
## Find a quality by name for a specific machine definition and materials.
|
||||
#
|
||||
# \param quality_name
|
||||
# \param machine_definition (Optional) \type{ContainerInstance} If nothing is
|
||||
# \param machine_definition (Optional) \type{DefinitionContainerInterface} If nothing is
|
||||
# specified then the currently selected machine definition is used.
|
||||
# \param material_containers (Optional) \type{List[ContainerInstance]} If nothing is specified then
|
||||
# \param material_containers (Optional) \type{List[InstanceContainer]} If nothing is specified then
|
||||
# the current set of selected materials is used.
|
||||
# \return the matching quality container \type{ContainerInstance}
|
||||
def findQualityByName(self, quality_name, machine_definition=None, material_containers=None):
|
||||
# \return the matching quality container \type{InstanceContainer}
|
||||
def findQualityByName(self, quality_name: str, machine_definition: Optional["DefinitionContainerInterface"] = None, material_containers: List[InstanceContainer] = None) -> Optional[InstanceContainer]:
|
||||
criteria = {"type": "quality", "name": quality_name}
|
||||
result = self._getFilteredContainersForStack(machine_definition, material_containers, **criteria)
|
||||
|
||||
@ -46,15 +50,20 @@ class QualityManager:
|
||||
## Find a quality changes container by name.
|
||||
#
|
||||
# \param quality_changes_name \type{str} the name of the quality changes container.
|
||||
# \param machine_definition (Optional) \type{ContainerInstance} If nothing is
|
||||
# specified then the currently selected machine definition is used.
|
||||
# \param material_containers (Optional) \type{List[ContainerInstance]} If nothing is specified then
|
||||
# the current set of selected materials is used.
|
||||
# \return the matching quality changes containers \type{List[ContainerInstance]}
|
||||
def findQualityChangesByName(self, quality_changes_name, machine_definition=None):
|
||||
criteria = {"type": "quality_changes", "name": quality_changes_name}
|
||||
result = self._getFilteredContainersForStack(machine_definition, [], **criteria)
|
||||
# \param machine_definition (Optional) \type{DefinitionContainer} If nothing is
|
||||
# specified then the currently selected machine definition is used..
|
||||
# \return the matching quality changes containers \type{List[InstanceContainer]}
|
||||
def findQualityChangesByName(self, quality_changes_name: str, machine_definition: Optional["DefinitionContainerInterface"] = None):
|
||||
if not machine_definition:
|
||||
global_stack = Application.getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
return [] #No stack, so no current definition could be found, so there are no quality changes either.
|
||||
machine_definition = global_stack.definition
|
||||
|
||||
result = self.findAllQualityChangesForMachine(machine_definition)
|
||||
for extruder in self.findAllExtruderDefinitionsForMachine(machine_definition):
|
||||
result.extend(self.findAllQualityChangesForExtruder(extruder))
|
||||
result = [quality_change for quality_change in result if quality_change.getName() == quality_changes_name]
|
||||
return result
|
||||
|
||||
## Fetch the list of available quality types for this combination of machine definition and materials.
|
||||
@ -62,7 +71,7 @@ class QualityManager:
|
||||
# \param machine_definition \type{DefinitionContainer}
|
||||
# \param material_containers \type{List[InstanceContainer]}
|
||||
# \return \type{List[str]}
|
||||
def findAllQualityTypesForMachineAndMaterials(self, machine_definition, material_containers):
|
||||
def findAllQualityTypesForMachineAndMaterials(self, machine_definition: "DefinitionContainerInterface", material_containers: List[InstanceContainer]) -> List[str]:
|
||||
# Determine the common set of quality types which can be
|
||||
# applied to all of the materials for this machine.
|
||||
quality_type_dict = self.__fetchQualityTypeDictForMaterial(machine_definition, material_containers[0])
|
||||
@ -76,9 +85,9 @@ class QualityManager:
|
||||
## Fetches a dict of quality types names to quality profiles for a combination of machine and material.
|
||||
#
|
||||
# \param machine_definition \type{DefinitionContainer} the machine definition.
|
||||
# \param material \type{ContainerInstance} the material.
|
||||
# \return \type{Dict[str, ContainerInstance]} the dict of suitable quality type names mapping to qualities.
|
||||
def __fetchQualityTypeDictForMaterial(self, machine_definition, material):
|
||||
# \param material \type{InstanceContainer} the material.
|
||||
# \return \type{Dict[str, InstanceContainer]} the dict of suitable quality type names mapping to qualities.
|
||||
def __fetchQualityTypeDictForMaterial(self, machine_definition: "DefinitionContainerInterface", material: InstanceContainer) -> Dict[str, InstanceContainer]:
|
||||
qualities = self.findAllQualitiesForMachineMaterial(machine_definition, material)
|
||||
quality_type_dict = {}
|
||||
for quality in qualities:
|
||||
@ -88,36 +97,36 @@ class QualityManager:
|
||||
## Find a quality container by quality type.
|
||||
#
|
||||
# \param quality_type \type{str} the name of the quality type to search for.
|
||||
# \param machine_definition (Optional) \type{ContainerInstance} If nothing is
|
||||
# \param machine_definition (Optional) \type{InstanceContainer} If nothing is
|
||||
# specified then the currently selected machine definition is used.
|
||||
# \param material_containers (Optional) \type{List[ContainerInstance]} If nothing is specified then
|
||||
# \param material_containers (Optional) \type{List[InstanceContainer]} If nothing is specified then
|
||||
# the current set of selected materials is used.
|
||||
# \return the matching quality container \type{ContainerInstance}
|
||||
def findQualityByQualityType(self, quality_type, machine_definition=None, material_containers=None, **kwargs):
|
||||
# \return the matching quality container \type{InstanceContainer}
|
||||
def findQualityByQualityType(self, quality_type: str, machine_definition: Optional["DefinitionContainerInterface"] = None, material_containers: List[InstanceContainer] = None, **kwargs) -> InstanceContainer:
|
||||
criteria = kwargs
|
||||
criteria["type"] = "quality"
|
||||
if quality_type:
|
||||
criteria["quality_type"] = quality_type
|
||||
result = self._getFilteredContainersForStack(machine_definition, material_containers, **criteria)
|
||||
|
||||
# Fall back to using generic materials and qualities if nothing could be found.
|
||||
if not result and material_containers and len(material_containers) == 1:
|
||||
basic_materials = self._getBasicMaterials(material_containers[0])
|
||||
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
|
||||
|
||||
if basic_materials:
|
||||
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
|
||||
return result[0] if result else None
|
||||
|
||||
## Find all suitable qualities for a combination of machine and material.
|
||||
#
|
||||
# \param machine_definition \type{DefinitionContainer} the machine definition.
|
||||
# \param material_container \type{ContainerInstance} the material.
|
||||
# \return \type{List[ContainerInstance]} the list of suitable qualities.
|
||||
def findAllQualitiesForMachineMaterial(self, machine_definition, material_container):
|
||||
# \param material_container \type{InstanceContainer} the material.
|
||||
# \return \type{List[InstanceContainer]} the list of suitable qualities.
|
||||
def findAllQualitiesForMachineMaterial(self, machine_definition: "DefinitionContainerInterface", material_container: InstanceContainer) -> List[InstanceContainer]:
|
||||
criteria = {"type": "quality" }
|
||||
result = self._getFilteredContainersForStack(machine_definition, [material_container], **criteria)
|
||||
if not result:
|
||||
basic_materials = self._getBasicMaterials(material_container)
|
||||
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
|
||||
if basic_materials:
|
||||
result = self._getFilteredContainersForStack(machine_definition, basic_materials, **criteria)
|
||||
|
||||
return result
|
||||
|
||||
@ -125,7 +134,7 @@ class QualityManager:
|
||||
#
|
||||
# \param machine_definition \type{DefinitionContainer} the machine definition.
|
||||
# \return \type{List[InstanceContainer]} the list of quality changes
|
||||
def findAllQualityChangesForMachine(self, machine_definition: DefinitionContainer) -> List[InstanceContainer]:
|
||||
def findAllQualityChangesForMachine(self, machine_definition: "DefinitionContainerInterface") -> List[InstanceContainer]:
|
||||
if machine_definition.getMetaDataEntry("has_machine_quality"):
|
||||
definition_id = machine_definition.getId()
|
||||
else:
|
||||
@ -135,25 +144,37 @@ class QualityManager:
|
||||
quality_changes_list = ContainerRegistry.getInstance().findInstanceContainers(**filter_dict)
|
||||
return quality_changes_list
|
||||
|
||||
def findAllExtruderDefinitionsForMachine(self, machine_definition: "DefinitionContainerInterface") -> List["DefinitionContainerInterface"]:
|
||||
filter_dict = { "machine": machine_definition.getId() }
|
||||
return ContainerRegistry.getInstance().findDefinitionContainers(**filter_dict)
|
||||
|
||||
## Find all quality changes for a given extruder.
|
||||
#
|
||||
# \param extruder_definition The extruder to find the quality changes for.
|
||||
# \return The list of quality changes for the given extruder.
|
||||
def findAllQualityChangesForExtruder(self, extruder_definition: "DefinitionContainerInterface") -> List[InstanceContainer]:
|
||||
filter_dict = {"type": "quality_changes", "extruder": extruder_definition.getId()}
|
||||
return ContainerRegistry.getInstance().findInstanceContainers(**filter_dict)
|
||||
|
||||
## Find all usable qualities for a machine and extruders.
|
||||
#
|
||||
# Finds all of the qualities for this combination of machine and extruders.
|
||||
# Only one quality per quality type is returned. i.e. if there are 2 qualities with quality_type=normal
|
||||
# then only one of then is returned (at random).
|
||||
#
|
||||
# \param global_container_stack \type{ContainerStack} the global machine definition
|
||||
# \param extruder_stacks \type{List[ContainerStack]} the list of extruder stacks
|
||||
# \param global_container_stack \type{GlobalStack} the global machine definition
|
||||
# \param extruder_stacks \type{List[ExtruderStack]} the list of extruder stacks
|
||||
# \return \type{List[InstanceContainer]} the list of the matching qualities. The quality profiles
|
||||
# return come from the first extruder in the given list of extruders.
|
||||
def findAllUsableQualitiesForMachineAndExtruders(self, global_container_stack, extruder_stacks):
|
||||
def findAllUsableQualitiesForMachineAndExtruders(self, global_container_stack: "GlobalStack", extruder_stacks: List["ExtruderStack"]) -> List[InstanceContainer]:
|
||||
global_machine_definition = global_container_stack.getBottom()
|
||||
|
||||
if extruder_stacks:
|
||||
# Multi-extruder machine detected.
|
||||
materials = [stack.findContainer(type="material") for stack in extruder_stacks]
|
||||
materials = [stack.material for stack in extruder_stacks]
|
||||
else:
|
||||
# Machine with one extruder.
|
||||
materials = [global_container_stack.findContainer(type="material")]
|
||||
materials = [global_container_stack.material]
|
||||
|
||||
quality_types = self.findAllQualityTypesForMachineAndMaterials(global_machine_definition, materials)
|
||||
|
||||
@ -170,14 +191,13 @@ class QualityManager:
|
||||
# This tries to find a generic or basic version of the given material.
|
||||
# \param material_container \type{InstanceContainer} the material
|
||||
# \return \type{List[InstanceContainer]} a list of the basic materials or an empty list if one could not be found.
|
||||
def _getBasicMaterials(self, material_container):
|
||||
def _getBasicMaterials(self, material_container: InstanceContainer):
|
||||
base_material = material_container.getMetaDataEntry("material")
|
||||
material_container_definition = material_container.getDefinition()
|
||||
if material_container_definition and material_container_definition.getMetaDataEntry("has_machine_quality"):
|
||||
definition_id = material_container.getDefinition().getMetaDataEntry("quality_definition", material_container.getDefinition().getId())
|
||||
else:
|
||||
definition_id = "fdmprinter"
|
||||
|
||||
if base_material:
|
||||
# There is a basic material specified
|
||||
criteria = { "type": "material", "name": base_material, "definition": definition_id }
|
||||
@ -192,7 +212,7 @@ class QualityManager:
|
||||
def _getFilteredContainers(self, **kwargs):
|
||||
return self._getFilteredContainersForStack(None, None, **kwargs)
|
||||
|
||||
def _getFilteredContainersForStack(self, machine_definition=None, material_containers=None, **kwargs):
|
||||
def _getFilteredContainersForStack(self, machine_definition: "DefinitionContainerInterface" = None, material_containers: List[InstanceContainer] = None, **kwargs):
|
||||
# Fill in any default values.
|
||||
if machine_definition is None:
|
||||
machine_definition = Application.getInstance().getGlobalContainerStack().getBottom()
|
||||
@ -200,9 +220,14 @@ class QualityManager:
|
||||
if quality_definition_id is not None:
|
||||
machine_definition = ContainerRegistry.getInstance().findDefinitionContainers(id=quality_definition_id)[0]
|
||||
|
||||
# for convenience
|
||||
if material_containers is None:
|
||||
material_containers = []
|
||||
|
||||
if not material_containers:
|
||||
active_stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
|
||||
material_containers = [stack.findContainer(type="material") for stack in active_stacks]
|
||||
if active_stacks:
|
||||
material_containers = [stack.material for stack in active_stacks]
|
||||
|
||||
criteria = kwargs
|
||||
filter_by_material = False
|
||||
@ -218,25 +243,23 @@ class QualityManager:
|
||||
criteria["definition"] = "fdmprinter"
|
||||
|
||||
# Stick the material IDs in a set
|
||||
if material_containers is None or len(material_containers) == 0:
|
||||
filter_by_material = False
|
||||
else:
|
||||
material_ids = set()
|
||||
for material_instance in material_containers:
|
||||
if material_instance is not None:
|
||||
# Add the parent material too.
|
||||
for basic_material in self._getBasicMaterials(material_instance):
|
||||
material_ids.add(basic_material.getId())
|
||||
material_ids.add(material_instance.getId())
|
||||
material_ids = set()
|
||||
|
||||
for material_instance in material_containers:
|
||||
if material_instance is not None:
|
||||
# Add the parent material too.
|
||||
for basic_material in self._getBasicMaterials(material_instance):
|
||||
material_ids.add(basic_material.getId())
|
||||
material_ids.add(material_instance.getId())
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(**criteria)
|
||||
|
||||
result = []
|
||||
for container in containers:
|
||||
# If the machine specifies we should filter by material, exclude containers that do not match any active material.
|
||||
if filter_by_material and container.getMetaDataEntry("material") not in material_ids and not "global_quality" in kwargs:
|
||||
if filter_by_material and container.getMetaDataEntry("material") not in material_ids and "global_quality" not in kwargs:
|
||||
continue
|
||||
result.append(container)
|
||||
|
||||
return result
|
||||
|
||||
## Get the parent machine definition of a machine definition.
|
||||
@ -245,7 +268,7 @@ class QualityManager:
|
||||
# an extruder definition.
|
||||
# \return \type{DefinitionContainer} the parent machine definition. If the given machine
|
||||
# definition doesn't have a parent then it is simply returned.
|
||||
def getParentMachineDefinition(self, machine_definition: DefinitionContainer) -> DefinitionContainer:
|
||||
def getParentMachineDefinition(self, machine_definition: "DefinitionContainerInterface") -> "DefinitionContainerInterface":
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
|
||||
machine_entry = machine_definition.getMetaDataEntry("machine")
|
||||
@ -274,8 +297,8 @@ class QualityManager:
|
||||
#
|
||||
# \param machine_definition \type{DefinitionContainer} This may be a normal machine definition or
|
||||
# an extruder definition.
|
||||
# \return \type{DefinitionContainer}
|
||||
def getWholeMachineDefinition(self, machine_definition):
|
||||
# \return \type{DefinitionContainerInterface}
|
||||
def getWholeMachineDefinition(self, machine_definition: "DefinitionContainerInterface") -> "DefinitionContainerInterface":
|
||||
machine_entry = machine_definition.getMetaDataEntry("machine")
|
||||
if machine_entry is None:
|
||||
# This already is a 'global' machine definition.
|
||||
|
@ -1,13 +1,15 @@
|
||||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import os.path
|
||||
import urllib
|
||||
import uuid
|
||||
from typing import Dict, Union
|
||||
|
||||
from PyQt5.QtCore import QObject, QUrl, QVariant
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
from UM.Util import parseBool
|
||||
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.SaveFile import SaveFile
|
||||
@ -216,23 +218,85 @@ class ContainerManager(QObject):
|
||||
entries = entry_name.split("/")
|
||||
entry_name = entries.pop()
|
||||
|
||||
sub_item_changed = False
|
||||
if entries:
|
||||
root_name = entries.pop(0)
|
||||
root = container.getMetaDataEntry(root_name)
|
||||
|
||||
item = root
|
||||
for entry in entries:
|
||||
for _ in range(len(entries)):
|
||||
item = item.get(entries.pop(0), { })
|
||||
|
||||
if item[entry_name] != entry_value:
|
||||
sub_item_changed = True
|
||||
item[entry_name] = entry_value
|
||||
|
||||
entry_name = root_name
|
||||
entry_value = root
|
||||
|
||||
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
|
||||
|
||||
## Set a setting property of the specified container.
|
||||
#
|
||||
# This will set the specified property of the specified setting of the container
|
||||
# and all containers that share the same base_file (if any). The latter only
|
||||
# happens for material containers.
|
||||
#
|
||||
# \param container_id \type{str} The ID of the container to change.
|
||||
# \param setting_key \type{str} The key of the setting.
|
||||
# \param property_name \type{str} The name of the property, eg "value".
|
||||
# \param property_value \type{str} The new value of the property.
|
||||
#
|
||||
# \return True if successful, False if not.
|
||||
@pyqtSlot(str, str, str, str, result = bool)
|
||||
def setContainerProperty(self, container_id, setting_key, property_name, property_value):
|
||||
containers = self._container_registry.findContainers(None, id = container_id)
|
||||
if not containers:
|
||||
Logger.log("w", "Could not set properties of container %s because it was not found.", container_id)
|
||||
return False
|
||||
|
||||
container = containers[0]
|
||||
|
||||
if container.isReadOnly():
|
||||
Logger.log("w", "Cannot set properties of read-only container %s.", container_id)
|
||||
return False
|
||||
|
||||
container.setProperty(setting_key, property_name, property_value)
|
||||
|
||||
basefile = container.getMetaDataEntry("base_file", container_id)
|
||||
for sibbling_container in ContainerRegistry.getInstance().findInstanceContainers(base_file = basefile):
|
||||
if sibbling_container != container:
|
||||
sibbling_container.setProperty(setting_key, property_name, property_value)
|
||||
|
||||
return True
|
||||
|
||||
## Get a setting property of the specified container.
|
||||
#
|
||||
# This will get the specified property of the specified setting of the
|
||||
# specified container.
|
||||
#
|
||||
# \param container_id The ID of the container to get the setting property
|
||||
# of.
|
||||
# \param setting_key The key of the setting to get the property of.
|
||||
# \param property_name The property to obtain.
|
||||
# \return The value of the specified property. The type of this property
|
||||
# value depends on the type of the property. For instance, the "value"
|
||||
# property of an integer setting will be a Python int, but the "value"
|
||||
# property of an enum setting will be a Python str.
|
||||
@pyqtSlot(str, str, str, result = QVariant)
|
||||
def getContainerProperty(self, container_id: str, setting_key: str, property_name: str):
|
||||
containers = self._container_registry.findContainers(id = container_id)
|
||||
if not containers:
|
||||
Logger.log("w", "Could not get properties of container %s because it was not found.", container_id)
|
||||
return ""
|
||||
container = containers[0]
|
||||
|
||||
return container.getProperty(setting_key, property_name)
|
||||
|
||||
## Set the name of the specified container.
|
||||
@pyqtSlot(str, str, result = bool)
|
||||
def setContainerName(self, container_id, new_name):
|
||||
@ -525,7 +589,7 @@ class ContainerManager(QObject):
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if not global_stack or not quality_name:
|
||||
return ""
|
||||
machine_definition = global_stack.getBottom()
|
||||
machine_definition = QualityManager.getInstance().getParentMachineDefinition(global_stack.getBottom())
|
||||
|
||||
for container in QualityManager.getInstance().findQualityChangesByName(quality_name, machine_definition):
|
||||
containers_found = True
|
||||
@ -671,6 +735,9 @@ class ContainerManager(QObject):
|
||||
|
||||
return new_change_instances
|
||||
|
||||
## Create a duplicate of a material, which has the same GUID and base_file metadata
|
||||
#
|
||||
# \return \type{str} the id of the newly created container.
|
||||
@pyqtSlot(str, result = str)
|
||||
def duplicateMaterial(self, material_id: str) -> str:
|
||||
containers = self._container_registry.findInstanceContainers(id=material_id)
|
||||
@ -693,6 +760,117 @@ class ContainerManager(QObject):
|
||||
duplicated_container.deserialize(f.read())
|
||||
duplicated_container.setDirty(True)
|
||||
self._container_registry.addContainer(duplicated_container)
|
||||
return self._getMaterialContainerIdForActiveMachine(new_id)
|
||||
|
||||
## Create a new material by cloning Generic PLA for the current material diameter and setting the GUID to something unqiue
|
||||
#
|
||||
# \return \type{str} the id of the newly created container.
|
||||
@pyqtSlot(result = str)
|
||||
def createMaterial(self) -> str:
|
||||
# Ensure all settings are saved.
|
||||
Application.getInstance().saveSettings()
|
||||
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
return ""
|
||||
|
||||
approximate_diameter = str(round(global_stack.getProperty("material_diameter", "value")))
|
||||
containers = self._container_registry.findInstanceContainers(id = "generic_pla*", approximate_diameter = approximate_diameter)
|
||||
if not containers:
|
||||
Logger.log("d", "Unable to create a new material by cloning Generic PLA, because it cannot be found for the material diameter for this machine.")
|
||||
return ""
|
||||
|
||||
base_file = containers[0].getMetaDataEntry("base_file")
|
||||
containers = self._container_registry.findInstanceContainers(id = base_file)
|
||||
if not containers:
|
||||
Logger.log("d", "Unable to create a new material by cloning Generic PLA, because the base file for Generic PLA for this machine can not be found.")
|
||||
return ""
|
||||
|
||||
# Create a new ID & container to hold the data.
|
||||
new_id = self._container_registry.uniqueName("custom_material")
|
||||
container_type = type(containers[0]) # Always XMLMaterialProfile, since we specifically clone the base_file
|
||||
duplicated_container = container_type(new_id)
|
||||
|
||||
# Instead of duplicating we load the data from the basefile again.
|
||||
# This ensures that the inheritance goes well and all "cut up" subclasses of the xmlMaterial profile
|
||||
# are also correctly created.
|
||||
with open(containers[0].getPath(), encoding="utf-8") as f:
|
||||
duplicated_container.deserialize(f.read())
|
||||
|
||||
duplicated_container.setMetaDataEntry("GUID", str(uuid.uuid4()))
|
||||
duplicated_container.setMetaDataEntry("brand", catalog.i18nc("@label", "Custom"))
|
||||
# We're defaulting to PLA, as machines with material profiles don't like material types they don't know.
|
||||
# TODO: This is a hack, the only reason this is in now is to bandaid the problem as we're close to a release!
|
||||
duplicated_container.setMetaDataEntry("material", "PLA")
|
||||
duplicated_container.setName(catalog.i18nc("@label", "Custom Material"))
|
||||
|
||||
self._container_registry.addContainer(duplicated_container)
|
||||
return self._getMaterialContainerIdForActiveMachine(new_id)
|
||||
|
||||
## Find the id of a material container based on the new material
|
||||
# Utilty function that is shared between duplicateMaterial and createMaterial
|
||||
#
|
||||
# \param base_file \type{str} the id of the created container.
|
||||
def _getMaterialContainerIdForActiveMachine(self, base_file):
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
return base_file
|
||||
|
||||
has_machine_materials = parseBool(global_stack.getMetaDataEntry("has_machine_materials", default = False))
|
||||
has_variant_materials = parseBool(global_stack.getMetaDataEntry("has_variant_materials", default = False))
|
||||
has_variants = parseBool(global_stack.getMetaDataEntry("has_variants", default = False))
|
||||
if has_machine_materials or has_variant_materials:
|
||||
if has_variants:
|
||||
materials = self._container_registry.findInstanceContainers(type = "material", base_file = base_file, definition = global_stack.getBottom().getId(), variant = self._machine_manager.activeVariantId)
|
||||
else:
|
||||
materials = self._container_registry.findInstanceContainers(type = "material", base_file = base_file, definition = global_stack.getBottom().getId())
|
||||
|
||||
if materials:
|
||||
return materials[0].getId()
|
||||
|
||||
Logger.log("w", "Unable to find a suitable container based on %s for the current machine .", base_file)
|
||||
return "" # do not activate a new material if a container can not be found
|
||||
|
||||
return base_file
|
||||
|
||||
## Get a list of materials that have the same GUID as the reference material
|
||||
#
|
||||
# \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(str, result = "QStringList")
|
||||
def getLinkedMaterials(self, material_id: str):
|
||||
containers = self._container_registry.findInstanceContainers(id=material_id)
|
||||
if not containers:
|
||||
Logger.log("d", "Unable to find materials linked to material with id %s, because it doesn't exist.", material_id)
|
||||
return []
|
||||
|
||||
material_container = containers[0]
|
||||
material_base_file = material_container.getMetaDataEntry("base_file", "")
|
||||
material_guid = material_container.getMetaDataEntry("GUID", "")
|
||||
if not material_guid:
|
||||
Logger.log("d", "Unable to find materials linked to material with id %s, because it doesn't have a GUID.", material_id)
|
||||
return []
|
||||
|
||||
containers = self._container_registry.findInstanceContainers(type = "material", GUID = material_guid)
|
||||
linked_material_names = []
|
||||
for container in containers:
|
||||
if container.getId() in [material_id, material_base_file] or container.getMetaDataEntry("base_file") != container.getId():
|
||||
continue
|
||||
|
||||
linked_material_names.append(container.getName())
|
||||
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(str)
|
||||
def unlinkMaterial(self, material_id: str):
|
||||
containers = self._container_registry.findInstanceContainers(id=material_id)
|
||||
if not containers:
|
||||
Logger.log("d", "Unable to make the material with id %s unique, because it doesn't exist.", material_id)
|
||||
return ""
|
||||
|
||||
containers[0].setMetaDataEntry("GUID", str(uuid.uuid4()))
|
||||
|
||||
|
||||
## Get the singleton instance for this class.
|
||||
@classmethod
|
||||
@ -815,6 +993,9 @@ class ContainerManager(QObject):
|
||||
quality_changes.setDefinition(self._container_registry.findContainers(id = "fdmprinter")[0])
|
||||
else:
|
||||
quality_changes.setDefinition(QualityManager.getInstance().getParentMachineDefinition(machine_definition))
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
quality_changes.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
||||
return quality_changes
|
||||
|
||||
|
||||
|
@ -1,9 +1,12 @@
|
||||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import re
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from UM.Decorators import override
|
||||
@ -22,6 +25,8 @@ from . import GlobalStack
|
||||
from .ContainerManager import ContainerManager
|
||||
from .ExtruderManager import ExtruderManager
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
@ -41,6 +46,14 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
if type(container) == ContainerStack:
|
||||
container = self._convertContainerStack(container)
|
||||
|
||||
if isinstance(container, InstanceContainer) and type(container) != type(self.getEmptyInstanceContainer()):
|
||||
#Check against setting version of the definition.
|
||||
required_setting_version = CuraApplication.SettingVersion
|
||||
actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0))
|
||||
if required_setting_version != actual_setting_version:
|
||||
Logger.log("w", "Instance container {container_id} is outdated. Its setting version is {actual_setting_version} but it should be {required_setting_version}.".format(container_id = container.getId(), actual_setting_version = actual_setting_version, required_setting_version = required_setting_version))
|
||||
return #Don't add.
|
||||
|
||||
super().addContainer(container)
|
||||
|
||||
## Create a name that is not empty and unique
|
||||
@ -189,8 +202,12 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
new_name = self.uniqueName(name_seed)
|
||||
if type(profile_or_list) is not list:
|
||||
profile = profile_or_list
|
||||
self._configureProfile(profile, name_seed, new_name)
|
||||
return { "status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile.getName()) }
|
||||
|
||||
result = self._configureProfile(profile, name_seed, new_name)
|
||||
if result is not None:
|
||||
return {"status": "error", "message": catalog.i18nc("@info:status", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, result)}
|
||||
|
||||
return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile.getName())}
|
||||
else:
|
||||
profile_index = -1
|
||||
global_profile = None
|
||||
@ -220,7 +237,9 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
global_profile = profile
|
||||
profile_id = (global_container_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_")
|
||||
|
||||
self._configureProfile(profile, profile_id, new_name)
|
||||
result = self._configureProfile(profile, profile_id, new_name)
|
||||
if result is not None:
|
||||
return {"status": "error", "message": catalog.i18nc("@info:status", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, result)}
|
||||
|
||||
profile_index += 1
|
||||
|
||||
@ -234,7 +253,14 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
super().load()
|
||||
self._fixupExtruders()
|
||||
|
||||
def _configureProfile(self, profile, id_seed, new_name):
|
||||
## Update an imported profile to match the current machine configuration.
|
||||
#
|
||||
# \param profile The profile to configure.
|
||||
# \param id_seed The base ID for the profile. May be changed so it does not conflict with existing containers.
|
||||
# \param new_name The new name for the profile.
|
||||
#
|
||||
# \return None if configuring was successful or an error message if an error occurred.
|
||||
def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str) -> Optional[str]:
|
||||
profile.setReadOnly(False)
|
||||
profile.setDirty(True) # Ensure the profiles are correctly saved
|
||||
|
||||
@ -247,15 +273,36 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
else:
|
||||
profile.addMetaDataEntry("type", "quality_changes")
|
||||
|
||||
quality_type = profile.getMetaDataEntry("quality_type")
|
||||
if not quality_type:
|
||||
return catalog.i18nc("@info:status", "Profile is missing a quality type.")
|
||||
|
||||
quality_type_criteria = {"quality_type": quality_type}
|
||||
if self._machineHasOwnQualities():
|
||||
profile.setDefinition(self._activeQualityDefinition())
|
||||
if self._machineHasOwnMaterials():
|
||||
profile.addMetaDataEntry("material", self._activeMaterialId())
|
||||
active_material_id = self._activeMaterialId()
|
||||
if active_material_id: # only update if there is an active material
|
||||
profile.addMetaDataEntry("material", active_material_id)
|
||||
quality_type_criteria["material"] = active_material_id
|
||||
|
||||
quality_type_criteria["definition"] = profile.getDefinition().getId()
|
||||
|
||||
else:
|
||||
profile.setDefinition(ContainerRegistry.getInstance().findDefinitionContainers(id="fdmprinter")[0])
|
||||
quality_type_criteria["definition"] = "fdmprinter"
|
||||
|
||||
# Check to make sure the imported profile actually makes sense in context of the current configuration.
|
||||
# This prevents issues where importing a "draft" profile for a machine without "draft" qualities would report as
|
||||
# successfully imported but then fail to show up.
|
||||
qualities = self.findInstanceContainers(**quality_type_criteria)
|
||||
if not qualities:
|
||||
return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type)
|
||||
|
||||
ContainerRegistry.getInstance().addContainer(profile)
|
||||
|
||||
return None
|
||||
|
||||
## Gets a list of profile writer plugins
|
||||
# \return List of tuples of (plugin_id, meta_data).
|
||||
def _getIOPlugins(self, io_type):
|
||||
|
@ -5,7 +5,8 @@ import os.path
|
||||
|
||||
from typing import Any, Optional
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSlot, pyqtSignal
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
|
||||
from UM.Decorators import override
|
||||
from UM.Logger import Logger
|
||||
@ -65,8 +66,8 @@ class CuraContainerStack(ContainerStack):
|
||||
## Set the quality changes container.
|
||||
#
|
||||
# \param new_quality_changes The new quality changes container. It is expected to have a "type" metadata entry with the value "quality_changes".
|
||||
def setQualityChanges(self, new_quality_changes: InstanceContainer) -> None:
|
||||
self.replaceContainer(_ContainerIndexes.QualityChanges, new_quality_changes)
|
||||
def setQualityChanges(self, new_quality_changes: InstanceContainer, postpone_emit = False) -> None:
|
||||
self.replaceContainer(_ContainerIndexes.QualityChanges, new_quality_changes, postpone_emit = postpone_emit)
|
||||
|
||||
## Set the quality changes container by an ID.
|
||||
#
|
||||
@ -92,8 +93,8 @@ class CuraContainerStack(ContainerStack):
|
||||
## Set the quality container.
|
||||
#
|
||||
# \param new_quality The new quality container. It is expected to have a "type" metadata entry with the value "quality".
|
||||
def setQuality(self, new_quality: InstanceContainer) -> None:
|
||||
self.replaceContainer(_ContainerIndexes.Quality, new_quality)
|
||||
def setQuality(self, new_quality: InstanceContainer, postpone_emit = False) -> None:
|
||||
self.replaceContainer(_ContainerIndexes.Quality, new_quality, postpone_emit = postpone_emit)
|
||||
|
||||
## Set the quality container by an ID.
|
||||
#
|
||||
@ -130,8 +131,8 @@ class CuraContainerStack(ContainerStack):
|
||||
## Set the material container.
|
||||
#
|
||||
# \param new_quality_changes The new material container. It is expected to have a "type" metadata entry with the value "quality_changes".
|
||||
def setMaterial(self, new_material: InstanceContainer) -> None:
|
||||
self.replaceContainer(_ContainerIndexes.Material, new_material)
|
||||
def setMaterial(self, new_material: InstanceContainer, postpone_emit = False) -> None:
|
||||
self.replaceContainer(_ContainerIndexes.Material, new_material, postpone_emit = postpone_emit)
|
||||
|
||||
## Set the material container by an ID.
|
||||
#
|
||||
@ -249,10 +250,18 @@ class CuraContainerStack(ContainerStack):
|
||||
## Get the definition container.
|
||||
#
|
||||
# \return The definition container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||
@pyqtProperty(DefinitionContainer, fset = setDefinition, notify = pyqtContainersChanged)
|
||||
@pyqtProperty(QObject, fset = setDefinition, notify = pyqtContainersChanged)
|
||||
def definition(self) -> DefinitionContainer:
|
||||
return self._containers[_ContainerIndexes.Definition]
|
||||
|
||||
@override(ContainerStack)
|
||||
def getBottom(self) -> "DefinitionContainer":
|
||||
return self.definition
|
||||
|
||||
@override(ContainerStack)
|
||||
def getTop(self) -> "InstanceContainer":
|
||||
return self.userChanges
|
||||
|
||||
## Check whether the specified setting has a 'user' value.
|
||||
#
|
||||
# A user value here is defined as the setting having a value in either
|
||||
|
@ -9,7 +9,6 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
|
||||
from .GlobalStack import GlobalStack
|
||||
from .ExtruderStack import ExtruderStack
|
||||
from .CuraContainerStack import CuraContainerStack
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@ -31,6 +30,11 @@ class CuraStackBuilder:
|
||||
|
||||
machine_definition = definitions[0]
|
||||
name = registry.createUniqueName("machine", "", name, machine_definition.name)
|
||||
# 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
|
||||
# Note that we don't go for uniqueName() immediately because that function matches with ignore_case set to true
|
||||
if registry.findContainers(id = name):
|
||||
name = registry.uniqueName(name)
|
||||
|
||||
new_global_stack = cls.createGlobalStack(
|
||||
new_stack_id = name,
|
||||
@ -76,6 +80,8 @@ class CuraStackBuilder:
|
||||
user_container = InstanceContainer(new_stack_id + "_user")
|
||||
user_container.addMetaDataEntry("type", "user")
|
||||
user_container.addMetaDataEntry("extruder", new_stack_id)
|
||||
from cura.CuraApplication import CuraApplication
|
||||
user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
||||
user_container.setDefinition(machine_definition)
|
||||
|
||||
stack.setUserChanges(user_container)
|
||||
@ -124,6 +130,8 @@ class CuraStackBuilder:
|
||||
user_container = InstanceContainer(new_stack_id + "_user")
|
||||
user_container.addMetaDataEntry("type", "user")
|
||||
user_container.addMetaDataEntry("machine", new_stack_id)
|
||||
from cura.CuraApplication import CuraApplication
|
||||
user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
||||
user_container.setDefinition(definition)
|
||||
|
||||
stack.setUserChanges(user_container)
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant #For communicating data and events to Qt.
|
||||
@ -15,8 +15,13 @@ from UM.Settings.ContainerRegistry import ContainerRegistry #Finding containers
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
from UM.Settings.ContainerStack import ContainerStack
|
||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||
from typing import Optional, List
|
||||
from UM.Settings.Interfaces import DefinitionContainerInterface
|
||||
from typing import Optional, List, TYPE_CHECKING, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.Settings.ExtruderStack import ExtruderStack
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
||||
|
||||
## Manages all existing extruder stacks.
|
||||
#
|
||||
@ -35,7 +40,7 @@ class ExtruderManager(QObject):
|
||||
## Registers listeners and such to listen to changes to the extruders.
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
self._extruder_trains = { } #Per machine, a dictionary of extruder container stack IDs.
|
||||
self._extruder_trains = { } #Per machine, a dictionary of extruder container stack IDs. Only for separately defined extruders.
|
||||
self._active_extruder_index = 0
|
||||
self._selected_object_extruders = []
|
||||
Application.getInstance().globalContainerStackChanged.connect(self.__globalContainerStackChanged)
|
||||
@ -69,11 +74,13 @@ class ExtruderManager(QObject):
|
||||
except KeyError:
|
||||
return 0
|
||||
|
||||
@pyqtProperty("QVariantMap", notify=extrudersChanged)
|
||||
@pyqtProperty("QVariantMap", notify = extrudersChanged)
|
||||
def extruderIds(self):
|
||||
map = {}
|
||||
for position in self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()]:
|
||||
map[position] = self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()][position].getId()
|
||||
global_stack_id = Application.getInstance().getGlobalContainerStack().getId()
|
||||
if global_stack_id in self._extruder_trains:
|
||||
for position in self._extruder_trains[global_stack_id]:
|
||||
map[position] = self._extruder_trains[global_stack_id][position].getId()
|
||||
return map
|
||||
|
||||
@pyqtSlot(str, result = str)
|
||||
@ -81,7 +88,7 @@ class ExtruderManager(QObject):
|
||||
for position in self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()]:
|
||||
extruder = self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()][position]
|
||||
if extruder.getId() == id:
|
||||
return extruder.findContainer(type = "quality_changes").getId()
|
||||
return extruder.qualityChanges.getId()
|
||||
|
||||
## The instance of the singleton pattern.
|
||||
#
|
||||
@ -145,13 +152,14 @@ class ExtruderManager(QObject):
|
||||
selected_nodes.append(node)
|
||||
|
||||
# Then, figure out which nodes are used by those selected nodes.
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
current_extruder_trains = self._extruder_trains.get(global_stack.getId())
|
||||
for node in selected_nodes:
|
||||
extruder = node.callDecoration("getActiveExtruder")
|
||||
if extruder:
|
||||
object_extruders.add(extruder)
|
||||
else:
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
object_extruders.add(self._extruder_trains[global_stack.getId()]["0"].getId())
|
||||
elif current_extruder_trains:
|
||||
object_extruders.add(current_extruder_trains["0"].getId())
|
||||
|
||||
self._selected_object_extruders = list(object_extruders)
|
||||
|
||||
@ -165,7 +173,7 @@ class ExtruderManager(QObject):
|
||||
self._selected_object_extruders = []
|
||||
self.selectedObjectExtrudersChanged.emit()
|
||||
|
||||
def getActiveExtruderStack(self) -> ContainerStack:
|
||||
def getActiveExtruderStack(self) -> Optional["ExtruderStack"]:
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
|
||||
if global_container_stack:
|
||||
@ -175,7 +183,7 @@ class ExtruderManager(QObject):
|
||||
return None
|
||||
|
||||
## Get an extruder stack by index
|
||||
def getExtruderStack(self, index):
|
||||
def getExtruderStack(self, index) -> Optional["ExtruderStack"]:
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack:
|
||||
if global_container_stack.getId() in self._extruder_trains:
|
||||
@ -184,7 +192,7 @@ class ExtruderManager(QObject):
|
||||
return None
|
||||
|
||||
## Get all extruder stacks
|
||||
def getExtruderStacks(self):
|
||||
def getExtruderStacks(self) -> List["ExtruderStack"]:
|
||||
result = []
|
||||
for i in range(self.extruderCount):
|
||||
result.append(self.getExtruderStack(i))
|
||||
@ -196,7 +204,7 @@ class ExtruderManager(QObject):
|
||||
# \param machine_definition The machine definition to add the extruders for.
|
||||
# \param machine_id The machine_id to add the extruders for.
|
||||
@deprecated("Use CuraStackBuilder", "2.6")
|
||||
def addMachineExtruders(self, machine_definition: DefinitionContainer, machine_id: str) -> None:
|
||||
def addMachineExtruders(self, machine_definition: DefinitionContainerInterface, machine_id: str) -> None:
|
||||
changed = False
|
||||
machine_definition_id = machine_definition.getId()
|
||||
if machine_id not in self._extruder_trains:
|
||||
@ -230,6 +238,13 @@ class ExtruderManager(QObject):
|
||||
if machine_id not in self._extruder_trains:
|
||||
self._extruder_trains[machine_id] = {}
|
||||
changed = True
|
||||
|
||||
# do not register if an extruder has already been registered at the position on this machine
|
||||
if any(item.getId() == extruder_train.getId() for item in self._extruder_trains[machine_id].values()):
|
||||
Logger.log("w", "Extruder [%s] has already been registered on machine [%s], not doing anything",
|
||||
extruder_train.getId(), machine_id)
|
||||
return
|
||||
|
||||
if extruder_train:
|
||||
self._extruder_trains[machine_id][extruder_train.getMetaDataEntry("position")] = extruder_train
|
||||
changed = True
|
||||
@ -249,7 +264,7 @@ class ExtruderManager(QObject):
|
||||
# \param position The position of this extruder train in the extruder slots of the machine.
|
||||
# \param machine_id The id of the "global" stack this extruder is linked to.
|
||||
@deprecated("Use CuraStackBuilder::createExtruderStack", "2.6")
|
||||
def createExtruderTrain(self, extruder_definition: DefinitionContainer, machine_definition: DefinitionContainer,
|
||||
def createExtruderTrain(self, extruder_definition: DefinitionContainerInterface, machine_definition: DefinitionContainerInterface,
|
||||
position, machine_id: str) -> None:
|
||||
# Cache some things.
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
@ -296,9 +311,9 @@ class ExtruderManager(QObject):
|
||||
if preferred_material_id:
|
||||
global_stack = ContainerRegistry.getInstance().findContainerStacks(id = machine_id)
|
||||
if global_stack:
|
||||
approximate_material_diameter = round(global_stack[0].getProperty("material_diameter", "value"))
|
||||
approximate_material_diameter = str(round(global_stack[0].getProperty("material_diameter", "value")))
|
||||
else:
|
||||
approximate_material_diameter = round(machine_definition.getProperty("material_diameter", "value"))
|
||||
approximate_material_diameter = str(round(machine_definition.getProperty("material_diameter", "value")))
|
||||
|
||||
search_criteria = { "type": "material", "id": preferred_material_id, "approximate_diameter": approximate_material_diameter}
|
||||
if machine_definition.getMetaDataEntry("has_machine_materials"):
|
||||
@ -357,6 +372,8 @@ class ExtruderManager(QObject):
|
||||
user_profile = InstanceContainer(extruder_stack_id + "_current_settings") # Add an empty user profile.
|
||||
user_profile.addMetaDataEntry("type", "user")
|
||||
user_profile.addMetaDataEntry("extruder", extruder_stack_id)
|
||||
from cura.CuraApplication import CuraApplication
|
||||
user_profile.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
||||
user_profile.setDefinition(machine_definition)
|
||||
container_registry.addContainer(user_profile)
|
||||
container_stack.addContainer(user_profile)
|
||||
@ -396,7 +413,7 @@ class ExtruderManager(QObject):
|
||||
# list.
|
||||
#
|
||||
# \return A list of extruder stacks.
|
||||
def getUsedExtruderStacks(self):
|
||||
def getUsedExtruderStacks(self) -> List["ContainerStack"]:
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
|
||||
@ -450,17 +467,17 @@ 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):
|
||||
def removeMachineExtruders(self, machine_id: str):
|
||||
for extruder in self.getMachineExtruders(machine_id):
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(type = "user", extruder = extruder.getId())
|
||||
for container in containers:
|
||||
ContainerRegistry.getInstance().removeContainer(container.getId())
|
||||
ContainerRegistry.getInstance().removeContainer(extruder.userChanges.getId())
|
||||
ContainerRegistry.getInstance().removeContainer(extruder.getId())
|
||||
if machine_id in self._extruder_trains:
|
||||
del self._extruder_trains[machine_id]
|
||||
|
||||
## Returns extruders for a specific machine.
|
||||
#
|
||||
# \param machine_id The machine to get the extruders of.
|
||||
def getMachineExtruders(self, machine_id):
|
||||
def getMachineExtruders(self, machine_id: str):
|
||||
if machine_id not in self._extruder_trains:
|
||||
return []
|
||||
return [self._extruder_trains[machine_id][name] for name in self._extruder_trains[machine_id]]
|
||||
@ -469,7 +486,7 @@ class ExtruderManager(QObject):
|
||||
#
|
||||
# The first element is the global container stack, followed by any extruder stacks.
|
||||
# \return \type{List[ContainerStack]}
|
||||
def getActiveGlobalAndExtruderStacks(self):
|
||||
def getActiveGlobalAndExtruderStacks(self) -> Optional[List[Union["ExtruderStack", "GlobalStack"]]]:
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
return None
|
||||
@ -481,7 +498,7 @@ class ExtruderManager(QObject):
|
||||
## Returns the list of active extruder stacks.
|
||||
#
|
||||
# \return \type{List[ContainerStack]} a list of
|
||||
def getActiveExtruderStacks(self):
|
||||
def getActiveExtruderStacks(self) -> List["ExtruderStack"]:
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
|
||||
result = []
|
||||
@ -509,7 +526,7 @@ class ExtruderManager(QObject):
|
||||
#
|
||||
# This is exposed to SettingFunction so it can be used in value functions.
|
||||
#
|
||||
# \param key The key of the setting to retieve values for.
|
||||
# \param key The key of the setting to retrieve values for.
|
||||
#
|
||||
# \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.
|
||||
@ -519,6 +536,10 @@ class ExtruderManager(QObject):
|
||||
|
||||
result = []
|
||||
for extruder in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()):
|
||||
# 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"):
|
||||
continue
|
||||
|
||||
value = extruder.getRawProperty(key, "value")
|
||||
|
||||
if value is None:
|
||||
|
@ -1,22 +1,21 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from typing import Any
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot
|
||||
from typing import Any, TYPE_CHECKING, Optional
|
||||
|
||||
from UM.Decorators import override
|
||||
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
|
||||
from UM.Settings.ContainerStack import ContainerStack, InvalidContainerStackError
|
||||
from UM.Settings.ContainerStack import ContainerStack
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||
from UM.Settings.Interfaces import ContainerInterface
|
||||
|
||||
from . import Exceptions
|
||||
from .CuraContainerStack import CuraContainerStack
|
||||
from .ExtruderManager import ExtruderManager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
||||
## Represents an Extruder and its related containers.
|
||||
#
|
||||
#
|
||||
@ -38,6 +37,10 @@ class ExtruderStack(CuraContainerStack):
|
||||
# For backward compatibility: Register the extruder with the Extruder Manager
|
||||
ExtruderManager.getInstance().registerExtruder(self, stack.id)
|
||||
|
||||
@override(ContainerStack)
|
||||
def getNextStack(self) -> Optional["GlobalStack"]:
|
||||
return super().getNextStack()
|
||||
|
||||
@classmethod
|
||||
def getLoadingPriority(cls) -> int:
|
||||
return 3
|
||||
@ -59,6 +62,13 @@ class ExtruderStack(CuraContainerStack):
|
||||
if not super().getProperty(key, "settable_per_extruder"):
|
||||
return self.getNextStack().getProperty(key, property_name)
|
||||
|
||||
limit_to_extruder = super().getProperty(key, "limit_to_extruder")
|
||||
if (limit_to_extruder is not None and limit_to_extruder != "-1") and self.getMetaDataEntry("position") != str(limit_to_extruder):
|
||||
if str(limit_to_extruder) in self.getNextStack().extruders:
|
||||
result = self.getNextStack().extruders[str(limit_to_extruder)].getProperty(key, property_name)
|
||||
if result is not None:
|
||||
return result
|
||||
|
||||
return super().getProperty(key, property_name)
|
||||
|
||||
@override(CuraContainerStack)
|
||||
|
@ -1,12 +1,15 @@
|
||||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty, pyqtSlot
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty, QTimer
|
||||
from typing import Iterable
|
||||
|
||||
import UM.Qt.ListModel
|
||||
from UM.Application import Application
|
||||
|
||||
import UM.FlameProfiler
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from cura.Settings.ExtruderStack import ExtruderStack #To listen to changes on the extruders.
|
||||
from cura.Settings.MachineManager import MachineManager #To listen to changes on the extruders of the currently active machine.
|
||||
|
||||
## Model that holds extruders.
|
||||
#
|
||||
@ -58,19 +61,21 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
self.addRoleName(self.MaterialRole, "material")
|
||||
self.addRoleName(self.VariantRole, "variant")
|
||||
|
||||
self._update_extruder_timer = QTimer()
|
||||
self._update_extruder_timer.setInterval(100)
|
||||
self._update_extruder_timer.setSingleShot(True)
|
||||
self._update_extruder_timer.timeout.connect(self.__updateExtruders)
|
||||
|
||||
self._add_global = False
|
||||
self._simple_names = False
|
||||
|
||||
self._active_extruder_stack = None
|
||||
self._active_machine_extruders = [] # type: Iterable[ExtruderStack]
|
||||
self._add_optional_extruder = False
|
||||
|
||||
#Listen to changes.
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._updateExtruders)
|
||||
manager = ExtruderManager.getInstance()
|
||||
|
||||
self._updateExtruders()
|
||||
|
||||
manager.activeExtruderChanged.connect(self._onActiveExtruderChanged)
|
||||
self._onActiveExtruderChanged()
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._extrudersChanged) #When the machine is swapped we must update the active machine extruders.
|
||||
ExtruderManager.getInstance().extrudersChanged.connect(self._extrudersChanged) #When the extruders change we must link to the stack-changed signal of the new extruder.
|
||||
self._extrudersChanged() #Also calls _updateExtruders.
|
||||
|
||||
def setAddGlobal(self, add):
|
||||
if add != self._add_global:
|
||||
@ -84,6 +89,18 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
def addGlobal(self):
|
||||
return self._add_global
|
||||
|
||||
addOptionalExtruderChanged = pyqtSignal()
|
||||
|
||||
def setAddOptionalExtruder(self, add_optional_extruder):
|
||||
if add_optional_extruder != self._add_optional_extruder:
|
||||
self._add_optional_extruder = add_optional_extruder
|
||||
self.addOptionalExtruderChanged.emit()
|
||||
self._updateExtruders()
|
||||
|
||||
@pyqtProperty(bool, fset = setAddOptionalExtruder, notify = addOptionalExtruderChanged)
|
||||
def addOptionalExtruder(self):
|
||||
return self._add_optional_extruder
|
||||
|
||||
## Set the simpleNames property.
|
||||
def setSimpleNames(self, simple_names):
|
||||
if simple_names != self._simple_names:
|
||||
@ -99,40 +116,59 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
def simpleNames(self):
|
||||
return self._simple_names
|
||||
|
||||
def _onActiveExtruderChanged(self):
|
||||
manager = ExtruderManager.getInstance()
|
||||
active_extruder_stack = manager.getActiveExtruderStack()
|
||||
if self._active_extruder_stack != active_extruder_stack:
|
||||
if self._active_extruder_stack:
|
||||
self._active_extruder_stack.containersChanged.disconnect(self._onExtruderStackContainersChanged)
|
||||
## Links to the stack-changed signal of the new extruders when an extruder
|
||||
# is swapped out or added in the current machine.
|
||||
#
|
||||
# \param machine_id The machine for which the extruders changed. This is
|
||||
# filled by the ExtruderManager.extrudersChanged signal when coming from
|
||||
# that signal. Application.globalContainerStackChanged doesn't fill this
|
||||
# signal; it's assumed to be the current printer in that case.
|
||||
def _extrudersChanged(self, machine_id = None):
|
||||
if machine_id is not None:
|
||||
if Application.getInstance().getGlobalContainerStack() is None:
|
||||
return #No machine, don't need to update the current machine's extruders.
|
||||
if machine_id != Application.getInstance().getGlobalContainerStack().getId():
|
||||
return #Not the current machine.
|
||||
#Unlink from old extruders.
|
||||
for extruder in self._active_machine_extruders:
|
||||
extruder.containersChanged.disconnect(self._onExtruderStackContainersChanged)
|
||||
|
||||
if active_extruder_stack:
|
||||
# Update the model when the material container is changed
|
||||
active_extruder_stack.containersChanged.connect(self._onExtruderStackContainersChanged)
|
||||
self._active_extruder_stack = active_extruder_stack
|
||||
#Link to new extruders.
|
||||
self._active_machine_extruders = []
|
||||
extruder_manager = ExtruderManager.getInstance()
|
||||
for extruder in extruder_manager.getExtruderStacks():
|
||||
extruder.containersChanged.connect(self._onExtruderStackContainersChanged)
|
||||
self._active_machine_extruders.append(extruder)
|
||||
|
||||
self._updateExtruders() #Since the new extruders may have different properties, update our own model.
|
||||
|
||||
def _onExtruderStackContainersChanged(self, container):
|
||||
# The ExtrudersModel needs to be updated when the material-name or -color changes, because the user identifies extruders by material-name
|
||||
self._updateExtruders()
|
||||
# Update when there is an empty container or material change
|
||||
if container.getMetaDataEntry("type") == "material" or container.getMetaDataEntry("type") is None:
|
||||
# The ExtrudersModel needs to be updated when the material-name or -color changes, because the user identifies extruders by material-name
|
||||
self._updateExtruders()
|
||||
|
||||
|
||||
modelChanged = pyqtSignal()
|
||||
|
||||
def _updateExtruders(self):
|
||||
self._update_extruder_timer.start()
|
||||
|
||||
## Update the list of extruders.
|
||||
#
|
||||
# This should be called whenever the list of extruders changes.
|
||||
def _updateExtruders(self):
|
||||
@UM.FlameProfiler.profile
|
||||
def __updateExtruders(self):
|
||||
changed = False
|
||||
|
||||
if self.rowCount() != 0:
|
||||
changed = True
|
||||
|
||||
items = []
|
||||
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack:
|
||||
if self._add_global:
|
||||
material = global_container_stack.findContainer({ "type": "material" })
|
||||
material = global_container_stack.material
|
||||
color = material.getMetaDataEntry("color_code", default = self.defaultColors[0]) if material else self.defaultColors[0]
|
||||
item = {
|
||||
"id": global_container_stack.getId(),
|
||||
@ -147,9 +183,6 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
machine_extruder_count = global_container_stack.getProperty("machine_extruder_count", "value")
|
||||
manager = ExtruderManager.getInstance()
|
||||
for extruder in manager.getMachineExtruders(global_container_stack.getId()):
|
||||
extruder_name = extruder.getName()
|
||||
material = extruder.findContainer({ "type": "material" })
|
||||
variant = extruder.findContainer({"type": "variant"})
|
||||
position = extruder.getMetaDataEntry("position", default = "0") # Get the position
|
||||
try:
|
||||
position = int(position)
|
||||
@ -157,6 +190,9 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
position = -1
|
||||
if position >= machine_extruder_count:
|
||||
continue
|
||||
extruder_name = extruder.getName()
|
||||
material = extruder.material
|
||||
variant = extruder.variant
|
||||
|
||||
default_color = self.defaultColors[position] if position >= 0 and position < len(self.defaultColors) else self.defaultColors[0]
|
||||
color = material.getMetaDataEntry("color_code", default = default_color) if material else default_color
|
||||
@ -174,5 +210,16 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
|
||||
if changed:
|
||||
items.sort(key = lambda i: i["index"])
|
||||
# We need optional extruder to be last, so add it after we do sorting.
|
||||
# This way we can simply intrepret the -1 of the index as the last item (which it now always is)
|
||||
if self._add_optional_extruder:
|
||||
item = {
|
||||
"id": "",
|
||||
"name": "Not overridden",
|
||||
"color": "#ffffff",
|
||||
"index": -1,
|
||||
"definition": ""
|
||||
}
|
||||
items.append(item)
|
||||
self.setItems(items)
|
||||
self.modelChanged.emit()
|
||||
|
66
cura/Settings/GlobalStack.py
Normal file → Executable file
66
cura/Settings/GlobalStack.py
Normal file → Executable file
@ -1,19 +1,17 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from typing import Any
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSlot, pyqtSignal
|
||||
from PyQt5.QtCore import pyqtProperty
|
||||
|
||||
from UM.Decorators import override
|
||||
|
||||
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
|
||||
from UM.Settings.ContainerStack import ContainerStack, InvalidContainerStackError
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Settings.ContainerStack import ContainerStack
|
||||
from UM.Settings.SettingInstance import InstanceState
|
||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.Interfaces import ContainerInterface
|
||||
from UM.Logger import Logger
|
||||
|
||||
from . import Exceptions
|
||||
from .CuraContainerStack import CuraContainerStack
|
||||
@ -26,7 +24,7 @@ class GlobalStack(CuraContainerStack):
|
||||
|
||||
self.addMetaDataEntry("type", "machine") # For backward compatibility
|
||||
|
||||
self._extruders = []
|
||||
self._extruders = {}
|
||||
|
||||
# This property is used to track which settings we are calculating the "resolve" for
|
||||
# and if so, to bypass the resolve to prevent an infinite recursion that would occur
|
||||
@ -36,14 +34,25 @@ class GlobalStack(CuraContainerStack):
|
||||
## Get the list of extruders of this stack.
|
||||
#
|
||||
# \return The extruders registered with this stack.
|
||||
@pyqtProperty("QVariantList")
|
||||
def extruders(self) -> list:
|
||||
@pyqtProperty("QVariantMap")
|
||||
def extruders(self) -> Dict[str, "ExtruderStack"]:
|
||||
return self._extruders
|
||||
|
||||
@classmethod
|
||||
def getLoadingPriority(cls) -> int:
|
||||
return 2
|
||||
|
||||
def getConfigurationTypeFromSerialized(self, serialized: str) -> Optional[str]:
|
||||
configuration_type = None
|
||||
try:
|
||||
parser = self._readAndValidateSerialized(serialized)
|
||||
configuration_type = parser["metadata"].get("type")
|
||||
if configuration_type == "machine":
|
||||
configuration_type = "machine_stack"
|
||||
except Exception as e:
|
||||
Logger.log("e", "Could not get configuration type: %s", e)
|
||||
return configuration_type
|
||||
|
||||
## Add an extruder to the list of extruders of this stack.
|
||||
#
|
||||
# \param extruder The extruder to add.
|
||||
@ -53,9 +62,18 @@ class GlobalStack(CuraContainerStack):
|
||||
def addExtruder(self, extruder: ContainerStack) -> None:
|
||||
extruder_count = self.getProperty("machine_extruder_count", "value")
|
||||
if extruder_count and len(self._extruders) + 1 > extruder_count:
|
||||
raise Exceptions.TooManyExtrudersError("Tried to add extruder to {id} but its extruder count is {count}".format(id = self.id, count = extruder_count))
|
||||
Logger.log("w", "Adding extruder {meta} to {id} but its extruder count is {count}".format(id = self.id, count = extruder_count, meta = str(extruder.getMetaData())))
|
||||
return
|
||||
|
||||
self._extruders.append(extruder)
|
||||
position = extruder.getMetaDataEntry("position")
|
||||
if position is None:
|
||||
Logger.log("w", "No position defined for extruder {extruder}, cannot add it to stack {stack}", extruder = extruder.id, stack = self.id)
|
||||
return
|
||||
|
||||
if any(item.getId() == extruder.id for item in self._extruders.values()):
|
||||
Logger.log("w", "Extruder [%s] has already been added to this stack [%s]", extruder.id, self._id)
|
||||
return
|
||||
self._extruders[position] = extruder
|
||||
|
||||
## Overridden from ContainerStack
|
||||
#
|
||||
@ -73,6 +91,7 @@ class GlobalStack(CuraContainerStack):
|
||||
if not self.definition.findDefinitions(key = key):
|
||||
return None
|
||||
|
||||
# Handle the "resolve" property.
|
||||
if self._shouldResolve(key, property_name):
|
||||
self._resolving_settings.add(key)
|
||||
resolve = super().getProperty(key, "resolve")
|
||||
@ -80,6 +99,16 @@ class GlobalStack(CuraContainerStack):
|
||||
if resolve is not None:
|
||||
return resolve
|
||||
|
||||
# Handle the "limit_to_extruder" property.
|
||||
limit_to_extruder = super().getProperty(key, "limit_to_extruder")
|
||||
if limit_to_extruder is not None and limit_to_extruder != "-1" and limit_to_extruder in self._extruders:
|
||||
if super().getProperty(key, "settable_per_extruder"):
|
||||
result = self._extruders[str(limit_to_extruder)].getProperty(key, property_name)
|
||||
if result is not None:
|
||||
return result
|
||||
else:
|
||||
Logger.log("e", "Setting {setting} has limit_to_extruder but is not settable per extruder!", setting = key)
|
||||
|
||||
return super().getProperty(key, property_name)
|
||||
|
||||
## Overridden from ContainerStack
|
||||
@ -89,6 +118,21 @@ class GlobalStack(CuraContainerStack):
|
||||
def setNextStack(self, next_stack: ContainerStack) -> None:
|
||||
raise Exceptions.InvalidOperationError("Global stack cannot have a next stack!")
|
||||
|
||||
## Gets the approximate filament diameter that the machine requires.
|
||||
#
|
||||
# The approximate material diameter is the material diameter rounded to
|
||||
# the nearest millimetre.
|
||||
#
|
||||
# If the machine has no requirement for the diameter, -1 is returned.
|
||||
#
|
||||
# \return The approximate filament diameter for the printer, as a string.
|
||||
@pyqtProperty(str)
|
||||
def approximateMaterialDiameter(self) -> str:
|
||||
material_diameter = self.definition.getProperty("material_diameter", "value")
|
||||
if material_diameter is None:
|
||||
return "-1"
|
||||
return str(round(float(material_diameter))) #Round, then convert back to string.
|
||||
|
||||
# protected:
|
||||
|
||||
# Determine whether or not we should try to get the "resolve" property instead of the
|
||||
|
@ -16,16 +16,14 @@ from UM.Decorators import deprecated
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.ContainerStack import ContainerStack
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Settings.SettingDefinition import SettingDefinition
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
from UM.Settings.Validator import ValidatorState
|
||||
from UM.Signal import postponeSignals
|
||||
from UM.Signal import postponeSignals, CompressTechnique
|
||||
import UM.FlameProfiler
|
||||
|
||||
from cura.QualityManager import QualityManager
|
||||
from cura.PrinterOutputDevice import PrinterOutputDevice
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
|
||||
from .GlobalStack import GlobalStack
|
||||
from .CuraStackBuilder import CuraStackBuilder
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
@ -35,15 +33,28 @@ from typing import TYPE_CHECKING, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||
from cura.Settings.CuraContainerStack import CuraContainerStack
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
||||
import os
|
||||
|
||||
|
||||
class MachineManager(QObject):
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
self._active_container_stack = None # type: ContainerStack
|
||||
self._global_container_stack = None # type: ContainerStack
|
||||
self._active_container_stack = None # type: CuraContainerStack
|
||||
self._global_container_stack = None # type: GlobalStack
|
||||
|
||||
self._error_check_timer = QTimer()
|
||||
self._error_check_timer.setInterval(250)
|
||||
self._error_check_timer.setSingleShot(True)
|
||||
self._error_check_timer.timeout.connect(self._updateStacksHaveErrors)
|
||||
|
||||
self._instance_container_timer = QTimer()
|
||||
self._instance_container_timer.setInterval(250)
|
||||
self._instance_container_timer.setSingleShot(True)
|
||||
self._instance_container_timer.timeout.connect(self.__onInstanceContainersChanged)
|
||||
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||
## When the global container is changed, active material probably needs to be updated.
|
||||
@ -81,7 +92,7 @@ class MachineManager(QObject):
|
||||
self._printer_output_devices = []
|
||||
Application.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
|
||||
|
||||
if active_machine_id != "":
|
||||
if active_machine_id != "" and ContainerRegistry.getInstance().findContainerStacks(id = active_machine_id):
|
||||
# An active machine was saved, so restore it.
|
||||
self.setActiveMachine(active_machine_id)
|
||||
if self._global_container_stack and self._global_container_stack.getProperty("machine_extruder_count", "value") > 1:
|
||||
@ -94,11 +105,6 @@ class MachineManager(QObject):
|
||||
self._material_incompatible_message = Message(catalog.i18nc("@info:status",
|
||||
"The selected material is incompatible with the selected machine or configuration."))
|
||||
|
||||
self._error_check_timer = QTimer()
|
||||
self._error_check_timer.setInterval(250)
|
||||
self._error_check_timer.setSingleShot(True)
|
||||
self._error_check_timer.timeout.connect(self._updateStacksHaveErrors)
|
||||
|
||||
globalContainerChanged = pyqtSignal() # Emitted whenever the global stack is changed (ie: when changing between printers, changing a global profile, but not when changing a value)
|
||||
activeMaterialChanged = pyqtSignal()
|
||||
activeVariantChanged = pyqtSignal()
|
||||
@ -134,7 +140,7 @@ class MachineManager(QObject):
|
||||
return self._printer_output_devices
|
||||
|
||||
@pyqtProperty(int, constant=True)
|
||||
def totalNumberOfSettings(self):
|
||||
def totalNumberOfSettings(self) -> int:
|
||||
return len(ContainerRegistry.getInstance().findDefinitionContainers(id="fdmprinter")[0].getAllKeys())
|
||||
|
||||
def _onHotendIdChanged(self, index: Union[str, int], hotend_id: str) -> None:
|
||||
@ -158,7 +164,7 @@ class MachineManager(QObject):
|
||||
else:
|
||||
Logger.log("w", "No variant found for printer definition %s with id %s" % (self._global_container_stack.getBottom().getId(), hotend_id))
|
||||
|
||||
def _onMaterialIdChanged(self, index, material_id):
|
||||
def _onMaterialIdChanged(self, index: Union[str, int], material_id: str):
|
||||
if not self._global_container_stack:
|
||||
return
|
||||
|
||||
@ -215,6 +221,7 @@ class MachineManager(QObject):
|
||||
|
||||
if old_index is not None:
|
||||
extruder_manager.setActiveExtruderIndex(old_index)
|
||||
self._auto_materials_changed = {} #Processed all of them now.
|
||||
|
||||
def _autoUpdateHotends(self):
|
||||
extruder_manager = ExtruderManager.getInstance()
|
||||
@ -231,6 +238,7 @@ class MachineManager(QObject):
|
||||
|
||||
if old_index is not None:
|
||||
extruder_manager.setActiveExtruderIndex(old_index)
|
||||
self._auto_hotends_changed = {} #Processed all of them now.
|
||||
|
||||
def _onGlobalContainerChanged(self):
|
||||
if self._global_container_stack:
|
||||
@ -290,8 +298,7 @@ class MachineManager(QObject):
|
||||
|
||||
quality = self._global_container_stack.quality
|
||||
quality.nameChanged.connect(self._onQualityNameChanged)
|
||||
|
||||
self._updateStacksHaveErrors()
|
||||
self._error_check_timer.start()
|
||||
|
||||
## Update self._stacks_valid according to _checkStacksForErrors and emit if change.
|
||||
def _updateStacksHaveErrors(self):
|
||||
@ -308,23 +315,23 @@ class MachineManager(QObject):
|
||||
if not self._active_container_stack:
|
||||
self._active_container_stack = self._global_container_stack
|
||||
|
||||
self._updateStacksHaveErrors()
|
||||
self._error_check_timer.start()
|
||||
|
||||
if old_active_container_stack != self._active_container_stack:
|
||||
# Many methods and properties related to the active quality actually depend
|
||||
# on _active_container_stack. If it changes, then the properties change.
|
||||
self.activeQualityChanged.emit()
|
||||
|
||||
def _onInstanceContainersChanged(self, container):
|
||||
container_type = container.getMetaDataEntry("type")
|
||||
|
||||
def __onInstanceContainersChanged(self):
|
||||
self.activeQualityChanged.emit()
|
||||
self.activeVariantChanged.emit()
|
||||
self.activeMaterialChanged.emit()
|
||||
self.activeQualityChanged.emit()
|
||||
self._error_check_timer.start()
|
||||
|
||||
self._updateStacksHaveErrors()
|
||||
def _onInstanceContainersChanged(self, container):
|
||||
self._instance_container_timer.start()
|
||||
|
||||
def _onPropertyChanged(self, key, property_name):
|
||||
def _onPropertyChanged(self, key: str, property_name: str):
|
||||
if property_name == "value":
|
||||
# Notify UI items, such as the "changed" star in profile pull down menu.
|
||||
self.activeStackValueChanged.emit()
|
||||
@ -408,7 +415,7 @@ class MachineManager(QObject):
|
||||
## Delete a user setting from the global stack and all extruder stacks.
|
||||
# \param key \type{str} the name of the key to delete
|
||||
@pyqtSlot(str)
|
||||
def clearUserSettingAllCurrentStacks(self, key):
|
||||
def clearUserSettingAllCurrentStacks(self, key: str):
|
||||
if not self._global_container_stack:
|
||||
return
|
||||
|
||||
@ -464,8 +471,8 @@ class MachineManager(QObject):
|
||||
|
||||
return ""
|
||||
|
||||
@pyqtProperty("QObject", notify = globalContainerChanged)
|
||||
def activeMachine(self) -> GlobalStack:
|
||||
@pyqtProperty(QObject, notify = globalContainerChanged)
|
||||
def activeMachine(self) -> "GlobalStack":
|
||||
return self._global_container_stack
|
||||
|
||||
@pyqtProperty(str, notify = activeStackChanged)
|
||||
@ -525,6 +532,22 @@ class MachineManager(QObject):
|
||||
|
||||
return ""
|
||||
|
||||
@pyqtProperty("QVariantMap", notify = activeVariantChanged)
|
||||
def allActiveVariantIds(self):
|
||||
if not self._global_container_stack:
|
||||
return {}
|
||||
|
||||
result = {}
|
||||
|
||||
for stack in ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks():
|
||||
variant_container = stack.variant
|
||||
if not variant_container:
|
||||
continue
|
||||
|
||||
result[stack.getId()] = variant_container.getId()
|
||||
|
||||
return result
|
||||
|
||||
@pyqtProperty("QVariantMap", notify = activeMaterialChanged)
|
||||
def allActiveMaterialIds(self):
|
||||
if not self._global_container_stack:
|
||||
@ -548,7 +571,7 @@ class MachineManager(QObject):
|
||||
# \return The layer height of the currently active quality profile. If
|
||||
# there is no quality profile, this returns 0.
|
||||
@pyqtProperty(float, notify=activeQualityChanged)
|
||||
def activeQualityLayerHeight(self):
|
||||
def activeQualityLayerHeight(self) -> float:
|
||||
if not self._global_container_stack:
|
||||
return 0
|
||||
|
||||
@ -565,7 +588,7 @@ class MachineManager(QObject):
|
||||
value = value(self._global_container_stack)
|
||||
return value
|
||||
|
||||
return 0 #No quality profile.
|
||||
return 0 # No quality profile.
|
||||
|
||||
## Get the Material ID associated with the currently active material
|
||||
# \returns MaterialID (string) if found, empty string otherwise
|
||||
@ -587,7 +610,7 @@ class MachineManager(QObject):
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify=activeQualityChanged)
|
||||
def activeQualityName(self):
|
||||
def activeQualityName(self) -> str:
|
||||
if self._active_container_stack and self._global_container_stack:
|
||||
quality = self._global_container_stack.qualityChanges
|
||||
if quality and not isinstance(quality, type(self._empty_quality_changes_container)):
|
||||
@ -598,7 +621,7 @@ class MachineManager(QObject):
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify=activeQualityChanged)
|
||||
def activeQualityId(self):
|
||||
def activeQualityId(self) -> str:
|
||||
if self._active_container_stack:
|
||||
quality = self._active_container_stack.qualityChanges
|
||||
if quality and not isinstance(quality, type(self._empty_quality_changes_container)):
|
||||
@ -609,7 +632,7 @@ class MachineManager(QObject):
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify=activeQualityChanged)
|
||||
def globalQualityId(self):
|
||||
def globalQualityId(self) -> str:
|
||||
if self._global_container_stack:
|
||||
quality = self._global_container_stack.qualityChanges
|
||||
if quality and not isinstance(quality, type(self._empty_quality_changes_container)):
|
||||
@ -620,7 +643,7 @@ class MachineManager(QObject):
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = activeQualityChanged)
|
||||
def activeQualityType(self):
|
||||
def activeQualityType(self) -> str:
|
||||
if self._active_container_stack:
|
||||
quality = self._active_container_stack.quality
|
||||
if quality:
|
||||
@ -628,7 +651,7 @@ class MachineManager(QObject):
|
||||
return ""
|
||||
|
||||
@pyqtProperty(bool, notify = activeQualityChanged)
|
||||
def isActiveQualitySupported(self):
|
||||
def isActiveQualitySupported(self) -> bool:
|
||||
if self._active_container_stack:
|
||||
quality = self._active_container_stack.quality
|
||||
if quality:
|
||||
@ -642,7 +665,7 @@ class MachineManager(QObject):
|
||||
# \todo Ideally, this method would be named activeQualityId(), and the other one
|
||||
# would be named something like activeQualityOrQualityChanges() for consistency
|
||||
@pyqtProperty(str, notify = activeQualityChanged)
|
||||
def activeQualityContainerId(self):
|
||||
def activeQualityContainerId(self) -> str:
|
||||
# We're using the active stack instead of the global stack in case the list of qualities differs per extruder
|
||||
if self._global_container_stack:
|
||||
quality = self._active_container_stack.quality
|
||||
@ -651,7 +674,7 @@ class MachineManager(QObject):
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = activeQualityChanged)
|
||||
def activeQualityChangesId(self):
|
||||
def activeQualityChangesId(self) -> str:
|
||||
if self._active_container_stack:
|
||||
changes = self._active_container_stack.qualityChanges
|
||||
if changes and changes.getId() != "empty":
|
||||
@ -660,7 +683,7 @@ class MachineManager(QObject):
|
||||
|
||||
## Check if a container is read_only
|
||||
@pyqtSlot(str, result = bool)
|
||||
def isReadOnly(self, container_id) -> bool:
|
||||
def isReadOnly(self, container_id: str) -> bool:
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
|
||||
if not containers or not self._active_container_stack:
|
||||
return True
|
||||
@ -668,7 +691,7 @@ class MachineManager(QObject):
|
||||
|
||||
## Copy the value of the setting of the current extruder to all other extruders as well as the global container.
|
||||
@pyqtSlot(str)
|
||||
def copyValueToExtruders(self, key):
|
||||
def copyValueToExtruders(self, key: str):
|
||||
if not self._active_container_stack or self._global_container_stack.getProperty("machine_extruder_count", "value") <= 1:
|
||||
return
|
||||
|
||||
@ -682,8 +705,8 @@ class MachineManager(QObject):
|
||||
## Set the active material by switching out a container
|
||||
# Depending on from/to material+current variant, a quality profile is chosen and set.
|
||||
@pyqtSlot(str)
|
||||
def setActiveMaterial(self, material_id):
|
||||
with postponeSignals(*self._getContainerChangedSignals(), compress = True):
|
||||
def setActiveMaterial(self, material_id: str):
|
||||
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(id = material_id)
|
||||
if not containers or not self._active_container_stack:
|
||||
return
|
||||
@ -698,7 +721,7 @@ class MachineManager(QObject):
|
||||
Logger.log("w", "While trying to set the active material, no material was found to replace it.")
|
||||
return
|
||||
|
||||
if old_quality_changes and old_quality_changes.getId() == "empty_quality_changes":
|
||||
if old_quality_changes and isinstance(old_quality_changes, type(self._empty_quality_changes_container)):
|
||||
old_quality_changes = None
|
||||
|
||||
self.blurSettings.emit()
|
||||
@ -731,11 +754,12 @@ class MachineManager(QObject):
|
||||
candidate_quality = quality_manager.findQualityByQualityType(quality_type,
|
||||
quality_manager.getWholeMachineDefinition(machine_definition),
|
||||
[material_container])
|
||||
if not candidate_quality or candidate_quality.getId() == "empty_quality":
|
||||
|
||||
if not candidate_quality or isinstance(candidate_quality, type(self._empty_quality_changes_container)):
|
||||
Logger.log("d", "Attempting to find fallback quality")
|
||||
# Fall back to a quality (which must be compatible with all other extruders)
|
||||
new_qualities = quality_manager.findAllUsableQualitiesForMachineAndExtruders(
|
||||
self._global_container_stack, ExtruderManager.getInstance().getExtruderStacks())
|
||||
|
||||
if new_qualities:
|
||||
new_quality_id = new_qualities[0].getId() # Just pick the first available one
|
||||
else:
|
||||
@ -747,8 +771,8 @@ class MachineManager(QObject):
|
||||
self.setActiveQuality(new_quality_id)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setActiveVariant(self, variant_id):
|
||||
with postponeSignals(*self._getContainerChangedSignals(), compress = True):
|
||||
def setActiveVariant(self, variant_id: str):
|
||||
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(id = variant_id)
|
||||
if not containers or not self._active_container_stack:
|
||||
return
|
||||
@ -757,10 +781,9 @@ class MachineManager(QObject):
|
||||
old_material = self._active_container_stack.material
|
||||
if old_variant:
|
||||
self.blurSettings.emit()
|
||||
variant_index = self._active_container_stack.getContainerIndex(old_variant)
|
||||
self._active_container_stack.replaceContainer(variant_index, containers[0])
|
||||
Logger.log("d", "Active variant changed")
|
||||
preferred_material = None
|
||||
self._active_container_stack.variant = containers[0]
|
||||
Logger.log("d", "Active variant changed to {active_variant_id}".format(active_variant_id = containers[0].getId()))
|
||||
preferred_material_name = None
|
||||
if old_material:
|
||||
preferred_material_name = old_material.getName()
|
||||
|
||||
@ -771,8 +794,8 @@ class MachineManager(QObject):
|
||||
## set the active quality
|
||||
# \param quality_id The quality_id of either a quality or a quality_changes
|
||||
@pyqtSlot(str)
|
||||
def setActiveQuality(self, quality_id):
|
||||
with postponeSignals(*self._getContainerChangedSignals(), compress = True):
|
||||
def setActiveQuality(self, quality_id: str):
|
||||
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
||||
self.blurSettings.emit()
|
||||
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(id = quality_id)
|
||||
@ -808,8 +831,8 @@ class MachineManager(QObject):
|
||||
|
||||
name_changed_connect_stacks.append(stack_quality)
|
||||
name_changed_connect_stacks.append(stack_quality_changes)
|
||||
self._replaceQualityOrQualityChangesInStack(stack, stack_quality)
|
||||
self._replaceQualityOrQualityChangesInStack(stack, stack_quality_changes)
|
||||
self._replaceQualityOrQualityChangesInStack(stack, stack_quality, postpone_emit=True)
|
||||
self._replaceQualityOrQualityChangesInStack(stack, stack_quality_changes, postpone_emit=True)
|
||||
|
||||
# Send emits that are postponed in replaceContainer.
|
||||
# Here the stacks are finished replacing and every value can be resolved based on the current state.
|
||||
@ -829,7 +852,8 @@ class MachineManager(QObject):
|
||||
#
|
||||
# \param quality_name \type{str} the name of the quality.
|
||||
# \return \type{List[Dict]} with keys "stack", "quality" and "quality_changes".
|
||||
def determineQualityAndQualityChangesForQualityType(self, quality_type):
|
||||
@UM.FlameProfiler.profile
|
||||
def determineQualityAndQualityChangesForQualityType(self, quality_type: str):
|
||||
quality_manager = QualityManager.getInstance()
|
||||
result = []
|
||||
empty_quality_changes = self._empty_quality_changes_container
|
||||
@ -866,7 +890,7 @@ class MachineManager(QObject):
|
||||
#
|
||||
# \param quality_changes_name \type{str} the name of the quality changes.
|
||||
# \return \type{List[Dict]} with keys "stack", "quality" and "quality_changes".
|
||||
def _determineQualityAndQualityChangesForQualityChanges(self, quality_changes_name):
|
||||
def _determineQualityAndQualityChangesForQualityChanges(self, quality_changes_name: str):
|
||||
result = []
|
||||
quality_manager = QualityManager.getInstance()
|
||||
|
||||
@ -922,18 +946,18 @@ class MachineManager(QObject):
|
||||
|
||||
return result
|
||||
|
||||
def _replaceQualityOrQualityChangesInStack(self, stack, container, postpone_emit = False):
|
||||
def _replaceQualityOrQualityChangesInStack(self, stack: "CuraContainerStack", container: "InstanceContainer", postpone_emit = False):
|
||||
# Disconnect the signal handling from the old container.
|
||||
container_type = container.getMetaDataEntry("type")
|
||||
if container_type == "quality":
|
||||
stack.quality.nameChanged.disconnect(self._onQualityNameChanged)
|
||||
stack.setQuality(container)
|
||||
stack.setQuality(container, postpone_emit = postpone_emit)
|
||||
stack.qualityChanges.nameChanged.connect(self._onQualityNameChanged)
|
||||
elif container_type == "quality_changes" or container_type is None:
|
||||
# If the container is an empty container, we need to change the quality_changes.
|
||||
# Quality can never be set to empty.
|
||||
stack.qualityChanges.nameChanged.disconnect(self._onQualityNameChanged)
|
||||
stack.setQualityChanges(container)
|
||||
stack.setQualityChanges(container, postpone_emit = postpone_emit)
|
||||
stack.qualityChanges.nameChanged.connect(self._onQualityNameChanged)
|
||||
self._onQualityNameChanged()
|
||||
|
||||
@ -941,7 +965,7 @@ class MachineManager(QObject):
|
||||
Application.getInstance().discardOrKeepProfileChanges()
|
||||
|
||||
@pyqtProperty(str, notify = activeVariantChanged)
|
||||
def activeVariantName(self):
|
||||
def activeVariantName(self) -> str:
|
||||
if self._active_container_stack:
|
||||
variant = self._active_container_stack.variant
|
||||
if variant:
|
||||
@ -950,7 +974,7 @@ class MachineManager(QObject):
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = activeVariantChanged)
|
||||
def activeVariantId(self):
|
||||
def activeVariantId(self) -> str:
|
||||
if self._active_container_stack:
|
||||
variant = self._active_container_stack.variant
|
||||
if variant:
|
||||
@ -959,7 +983,7 @@ class MachineManager(QObject):
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
def activeDefinitionId(self):
|
||||
def activeDefinitionId(self) -> str:
|
||||
if self._global_container_stack:
|
||||
definition = self._global_container_stack.getBottom()
|
||||
if definition:
|
||||
@ -968,7 +992,7 @@ class MachineManager(QObject):
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify=globalContainerChanged)
|
||||
def activeDefinitionName(self):
|
||||
def activeDefinitionName(self) -> str:
|
||||
if self._global_container_stack:
|
||||
definition = self._global_container_stack.getBottom()
|
||||
if definition:
|
||||
@ -980,7 +1004,7 @@ class MachineManager(QObject):
|
||||
# \returns DefinitionID (string) if found, empty string otherwise
|
||||
# \sa getQualityDefinitionId
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
def activeQualityDefinitionId(self):
|
||||
def activeQualityDefinitionId(self) -> str:
|
||||
if self._global_container_stack:
|
||||
return self.getQualityDefinitionId(self._global_container_stack.getBottom())
|
||||
return ""
|
||||
@ -989,14 +1013,14 @@ class MachineManager(QObject):
|
||||
# This is normally the id of the definition itself, but machines can specify a different definition to inherit qualities from
|
||||
# \param definition (DefinitionContainer) machine definition
|
||||
# \returns DefinitionID (string) if found, empty string otherwise
|
||||
def getQualityDefinitionId(self, definition):
|
||||
def getQualityDefinitionId(self, definition: "DefinitionContainer") -> str:
|
||||
return QualityManager.getInstance().getParentMachineDefinition(definition).getId()
|
||||
|
||||
## Get the Variant ID to use to select quality profiles for the currently active variant
|
||||
# \returns VariantID (string) if found, empty string otherwise
|
||||
# \sa getQualityVariantId
|
||||
@pyqtProperty(str, notify = activeVariantChanged)
|
||||
def activeQualityVariantId(self):
|
||||
def activeQualityVariantId(self) -> str:
|
||||
if self._active_container_stack:
|
||||
variant = self._active_container_stack.variant
|
||||
if variant:
|
||||
@ -1007,9 +1031,9 @@ class MachineManager(QObject):
|
||||
# This is normally the id of the variant itself, but machines can specify a different definition
|
||||
# to inherit qualities from, which has consequences for the variant to use as well
|
||||
# \param definition (DefinitionContainer) machine definition
|
||||
# \param variant (DefinitionContainer) variant definition
|
||||
# \param variant (InstanceContainer) variant definition
|
||||
# \returns VariantID (string) if found, empty string otherwise
|
||||
def getQualityVariantId(self, definition, variant):
|
||||
def getQualityVariantId(self, definition: "DefinitionContainer", variant: "InstanceContainer") -> str:
|
||||
variant_id = variant.getId()
|
||||
definition_id = definition.getId()
|
||||
quality_definition_id = self.getQualityDefinitionId(definition)
|
||||
@ -1021,7 +1045,7 @@ class MachineManager(QObject):
|
||||
## Gets how the active definition calls variants
|
||||
# Caveat: per-definition-variant-title is currently not translated (though the fallback is)
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
def activeDefinitionVariantsName(self):
|
||||
def activeDefinitionVariantsName(self) -> str:
|
||||
fallback_title = catalog.i18nc("@label", "Nozzle")
|
||||
if self._global_container_stack:
|
||||
return self._global_container_stack.getBottom().getMetaDataEntry("variants_name", fallback_title)
|
||||
@ -1029,7 +1053,7 @@ class MachineManager(QObject):
|
||||
return fallback_title
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def renameMachine(self, machine_id, new_name):
|
||||
def renameMachine(self, machine_id: str, new_name: str):
|
||||
containers = ContainerRegistry.getInstance().findContainerStacks(id = machine_id)
|
||||
if containers:
|
||||
new_name = self._createUniqueName("machine", containers[0].getName(), new_name, containers[0].getBottom().getName())
|
||||
@ -1037,32 +1061,32 @@ class MachineManager(QObject):
|
||||
self.globalContainerChanged.emit()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def removeMachine(self, machine_id):
|
||||
def removeMachine(self, machine_id: str):
|
||||
# If the machine that is being removed is the currently active machine, set another machine as the active machine.
|
||||
activate_new_machine = (self._global_container_stack and self._global_container_stack.getId() == machine_id)
|
||||
|
||||
ExtruderManager.getInstance().removeMachineExtruders(machine_id)
|
||||
# activate a new machine before removing a machine because this is safer
|
||||
if activate_new_machine:
|
||||
machine_stacks = ContainerRegistry.getInstance().findContainerStacks(type = "machine")
|
||||
other_machine_stacks = [s for s in machine_stacks if s.getId() != machine_id]
|
||||
if other_machine_stacks:
|
||||
Application.getInstance().setGlobalContainerStack(other_machine_stacks[0])
|
||||
|
||||
ExtruderManager.getInstance().removeMachineExtruders(machine_id)
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(type = "user", machine = machine_id)
|
||||
for container in containers:
|
||||
ContainerRegistry.getInstance().removeContainer(container.getId())
|
||||
ContainerRegistry.getInstance().removeContainer(machine_id)
|
||||
|
||||
if activate_new_machine:
|
||||
stacks = ContainerRegistry.getInstance().findContainerStacks(type = "machine")
|
||||
if stacks:
|
||||
Application.getInstance().setGlobalContainerStack(stacks[0])
|
||||
|
||||
|
||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||
def hasMaterials(self):
|
||||
def hasMaterials(self) -> bool:
|
||||
if self._global_container_stack:
|
||||
return bool(self._global_container_stack.getMetaDataEntry("has_materials", False))
|
||||
|
||||
return False
|
||||
|
||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||
def hasVariants(self):
|
||||
def hasVariants(self) -> bool:
|
||||
if self._global_container_stack:
|
||||
return bool(self._global_container_stack.getMetaDataEntry("has_variants", False))
|
||||
|
||||
@ -1071,7 +1095,7 @@ class MachineManager(QObject):
|
||||
## Property to indicate if a machine has "specialized" material profiles.
|
||||
# Some machines have their own material profiles that "override" the default catch all profiles.
|
||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||
def filterMaterialsByMachine(self):
|
||||
def filterMaterialsByMachine(self) -> bool:
|
||||
if self._global_container_stack:
|
||||
return bool(self._global_container_stack.getMetaDataEntry("has_machine_materials", False))
|
||||
|
||||
@ -1080,7 +1104,7 @@ class MachineManager(QObject):
|
||||
## Property to indicate if a machine has "specialized" quality profiles.
|
||||
# Some machines have their own quality profiles that "override" the default catch all profiles.
|
||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||
def filterQualityByMachine(self):
|
||||
def filterQualityByMachine(self) -> bool:
|
||||
if self._global_container_stack:
|
||||
return bool(self._global_container_stack.getMetaDataEntry("has_machine_quality", False))
|
||||
return False
|
||||
@ -1089,7 +1113,7 @@ class MachineManager(QObject):
|
||||
# \param machine_id string machine id to get the definition ID of
|
||||
# \returns DefinitionID (string) if found, None otherwise
|
||||
@pyqtSlot(str, result = str)
|
||||
def getDefinitionByMachineId(self, machine_id):
|
||||
def getDefinitionByMachineId(self, machine_id: str) -> str:
|
||||
containers = ContainerRegistry.getInstance().findContainerStacks(id=machine_id)
|
||||
if containers:
|
||||
return containers[0].getBottom().getId()
|
||||
@ -1098,27 +1122,12 @@ class MachineManager(QObject):
|
||||
def createMachineManager(engine=None, script_engine=None):
|
||||
return MachineManager()
|
||||
|
||||
def _updateVariantContainer(self, definition: "DefinitionContainer"):
|
||||
if not definition.getMetaDataEntry("has_variants"):
|
||||
return self._empty_variant_container
|
||||
machine_definition_id = Application.getInstance().getMachineManager().getQualityDefinitionId(definition)
|
||||
containers = []
|
||||
preferred_variant = definition.getMetaDataEntry("preferred_variant")
|
||||
if preferred_variant:
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(type = "variant", definition = machine_definition_id, id = preferred_variant)
|
||||
if not containers:
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(type = "variant", definition = machine_definition_id)
|
||||
|
||||
if containers:
|
||||
return containers[0]
|
||||
|
||||
return self._empty_variant_container
|
||||
|
||||
@deprecated("Use ExtruderStack.material = ... and it won't be necessary", "2.7")
|
||||
def _updateMaterialContainer(self, definition: "DefinitionContainer", stack: "ContainerStack", variant_container: Optional["InstanceContainer"] = None, preferred_material_name: Optional[str] = None):
|
||||
if not definition.getMetaDataEntry("has_materials"):
|
||||
return self._empty_material_container
|
||||
|
||||
approximate_material_diameter = round(stack.getProperty("material_diameter", "value"))
|
||||
approximate_material_diameter = str(round(stack.getProperty("material_diameter", "value")))
|
||||
search_criteria = { "type": "material", "approximate_diameter": approximate_material_diameter }
|
||||
|
||||
if definition.getMetaDataEntry("has_machine_materials"):
|
||||
@ -1151,110 +1160,6 @@ class MachineManager(QObject):
|
||||
Logger.log("w", "Unable to find a material container with provided criteria, returning an empty one instead.")
|
||||
return self._empty_material_container
|
||||
|
||||
def _updateQualityContainer(self, definition: "DefinitionContainer", variant_container: "ContainerStack", material_container = None, preferred_quality_name: Optional[str] = None):
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
search_criteria = { "type": "quality" }
|
||||
|
||||
if definition.getMetaDataEntry("has_machine_quality"):
|
||||
search_criteria["definition"] = self.getQualityDefinitionId(definition)
|
||||
|
||||
if definition.getMetaDataEntry("has_materials") and material_container:
|
||||
search_criteria["material"] = material_container.id
|
||||
else:
|
||||
search_criteria["definition"] = "fdmprinter"
|
||||
|
||||
if preferred_quality_name and preferred_quality_name != "empty":
|
||||
search_criteria["name"] = preferred_quality_name
|
||||
else:
|
||||
preferred_quality = definition.getMetaDataEntry("preferred_quality")
|
||||
if preferred_quality:
|
||||
search_criteria["id"] = preferred_quality
|
||||
|
||||
containers = container_registry.findInstanceContainers(**search_criteria)
|
||||
if containers:
|
||||
return containers[0]
|
||||
|
||||
if "material" in search_criteria:
|
||||
# First check if we can solve our material not found problem by checking if we can find quality containers
|
||||
# that are assigned to the parents of this material profile.
|
||||
try:
|
||||
inherited_files = material_container.getInheritedFiles()
|
||||
except AttributeError: # Material_container does not support inheritance.
|
||||
inherited_files = []
|
||||
|
||||
if inherited_files:
|
||||
for inherited_file in inherited_files:
|
||||
# Extract the ID from the path we used to load the file.
|
||||
search_criteria["material"] = os.path.basename(inherited_file).split(".")[0]
|
||||
containers = container_registry.findInstanceContainers(**search_criteria)
|
||||
if containers:
|
||||
return containers[0]
|
||||
# We still weren't able to find a quality for this specific material.
|
||||
# Try to find qualities for a generic version of the material.
|
||||
material_search_criteria = { "type": "material", "material": material_container.getMetaDataEntry("material"), "color_name": "Generic"}
|
||||
if definition.getMetaDataEntry("has_machine_quality"):
|
||||
if material_container:
|
||||
material_search_criteria["definition"] = material_container.getDefinition().id
|
||||
|
||||
if definition.getMetaDataEntry("has_variants"):
|
||||
material_search_criteria["variant"] = material_container.getMetaDataEntry("variant")
|
||||
else:
|
||||
material_search_criteria["definition"] = self.getQualityDefinitionId(definition)
|
||||
|
||||
if definition.getMetaDataEntry("has_variants") and variant_container:
|
||||
material_search_criteria["variant"] = self.getQualityVariantId(definition, variant_container)
|
||||
else:
|
||||
material_search_criteria["definition"] = "fdmprinter"
|
||||
material_containers = container_registry.findInstanceContainers(**material_search_criteria)
|
||||
# Try all materials to see if there is a quality profile available.
|
||||
for material_container in material_containers:
|
||||
search_criteria["material"] = material_container.getId()
|
||||
|
||||
containers = container_registry.findInstanceContainers(**search_criteria)
|
||||
if containers:
|
||||
return containers[0]
|
||||
|
||||
if "name" in search_criteria or "id" in search_criteria:
|
||||
# If a quality by this name can not be found, try a wider set of search criteria
|
||||
search_criteria.pop("name", None)
|
||||
search_criteria.pop("id", None)
|
||||
|
||||
containers = container_registry.findInstanceContainers(**search_criteria)
|
||||
if containers:
|
||||
return containers[0]
|
||||
|
||||
# Notify user that we were unable to find a matching quality
|
||||
message = Message(catalog.i18nc("@info:status", "Unable to find a quality profile for this combination. Default settings will be used instead."))
|
||||
message.show()
|
||||
return self._empty_quality_container
|
||||
|
||||
## Finds a quality-changes container to use if any other container
|
||||
# changes.
|
||||
#
|
||||
# \param quality_type The quality type to find a quality-changes for.
|
||||
# \param preferred_quality_changes_name The name of the quality-changes to
|
||||
# pick, if any such quality-changes profile is available.
|
||||
def _updateQualityChangesContainer(self, quality_type, preferred_quality_changes_name = None):
|
||||
container_registry = ContainerRegistry.getInstance() # Cache.
|
||||
search_criteria = { "type": "quality_changes" }
|
||||
|
||||
search_criteria["quality"] = quality_type
|
||||
if preferred_quality_changes_name:
|
||||
search_criteria["name"] = preferred_quality_changes_name
|
||||
|
||||
# Try to search with the name in the criteria first, since we prefer to have the correct name.
|
||||
containers = container_registry.findInstanceContainers(**search_criteria)
|
||||
if containers: # Found one!
|
||||
return containers[0]
|
||||
|
||||
if "name" in search_criteria:
|
||||
del search_criteria["name"] # Not found, then drop the name requirement (if we had one) and search again.
|
||||
containers = container_registry.findInstanceContainers(**search_criteria)
|
||||
if containers:
|
||||
return containers[0]
|
||||
|
||||
return self._empty_quality_changes_container # Didn't find anything with the required quality_type.
|
||||
|
||||
def _onMachineNameChanged(self):
|
||||
self.globalContainerChanged.emit()
|
||||
|
||||
|
57
cura/Settings/MaterialManager.py
Normal file
57
cura/Settings/MaterialManager.py
Normal file
@ -0,0 +1,57 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot #To expose data to QML.
|
||||
|
||||
from cura.Settings.ContainerManager import ContainerManager
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message #To create a warning message about material diameter.
|
||||
from UM.i18n import i18nCatalog #Translated strings.
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
## Handles material-related data, processing requests to change them and
|
||||
# providing data for the GUI.
|
||||
#
|
||||
# TODO: Move material-related managing over from the machine manager to here.
|
||||
class MaterialManager(QObject):
|
||||
## Creates the global values for the material manager to use.
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
#Material diameter changed warning message.
|
||||
self._material_diameter_warning_message = Message(catalog.i18nc("@info:status Has a cancel button next to it.",
|
||||
"The selected material diameter causes the material to become incompatible with the current printer."))
|
||||
self._material_diameter_warning_message.addAction("Undo", catalog.i18nc("@action:button", "Undo"), None, catalog.i18nc("@action", "Undo changing the material diameter."))
|
||||
self._material_diameter_warning_message.actionTriggered.connect(self._materialWarningMessageAction)
|
||||
|
||||
## Creates an instance of the MaterialManager.
|
||||
#
|
||||
# This should only be called by PyQt to create the singleton instance of
|
||||
# this class.
|
||||
@staticmethod
|
||||
def createMaterialManager(engine = None, script_engine = None):
|
||||
return MaterialManager()
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def showMaterialWarningMessage(self, material_id, previous_diameter):
|
||||
self._material_diameter_warning_message.previous_diameter = previous_diameter #Make sure that the undo button can properly undo the action.
|
||||
self._material_diameter_warning_message.material_id = material_id
|
||||
self._material_diameter_warning_message.show()
|
||||
|
||||
## Called when clicking "undo" on the warning dialogue for disappeared
|
||||
# materials.
|
||||
#
|
||||
# This executes the undo action, restoring the material diameter.
|
||||
#
|
||||
# \param button The identifier of the button that was pressed.
|
||||
def _materialWarningMessageAction(self, message, button):
|
||||
if button == "Undo":
|
||||
container_manager = ContainerManager.getInstance()
|
||||
container_manager.setContainerMetaDataEntry(self._material_diameter_warning_message.material_id, "properties/diameter", self._material_diameter_warning_message.previous_diameter)
|
||||
approximate_previous_diameter = str(round(float(self._material_diameter_warning_message.previous_diameter)))
|
||||
container_manager.setContainerMetaDataEntry(self._material_diameter_warning_message.material_id, "approximate_diameter", approximate_previous_diameter)
|
||||
container_manager.setContainerProperty(self._material_diameter_warning_message.material_id, "material_diameter", "value", self._material_diameter_warning_message.previous_diameter);
|
||||
message.hide()
|
||||
else:
|
||||
Logger.log("w", "Unknown button action for material diameter warning message: {action}".format(action = button))
|
21
cura/Settings/MaterialsModel.py
Normal file
21
cura/Settings/MaterialsModel.py
Normal file
@ -0,0 +1,21 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry #To listen for changes to the materials.
|
||||
from UM.Settings.Models.InstanceContainersModel import InstanceContainersModel #We're extending this class.
|
||||
|
||||
## A model that shows a list of currently valid materials.
|
||||
class MaterialsModel(InstanceContainersModel):
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
ContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerMetaDataChanged)
|
||||
|
||||
## Called when the metadata of the container was changed.
|
||||
#
|
||||
# This makes sure that we only update when it was a material that changed.
|
||||
#
|
||||
# \param container The container whose metadata was changed.
|
||||
def _onContainerMetaDataChanged(self, container):
|
||||
if container.getMetaDataEntry("type") == "material": #Only need to update if a material was changed.
|
||||
self._update()
|
@ -40,6 +40,6 @@ class QualityAndUserProfilesModel(ProfilesModel):
|
||||
|
||||
# Filter the quality_change by the list of available quality_types
|
||||
quality_type_set = set([x.getMetaDataEntry("quality_type") for x in quality_list])
|
||||
filtered_quality_changes = [qc for qc in quality_changes_list if qc.getMetaDataEntry("quality_type") in quality_type_set]
|
||||
filtered_quality_changes = [qc for qc in quality_changes_list if qc.getMetaDataEntry("quality_type") in quality_type_set and qc.getMetaDataEntry("extruder") is None]
|
||||
|
||||
return quality_list + filtered_quality_changes
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
|
||||
@ -35,7 +35,7 @@ class SettingInheritanceManager(QObject):
|
||||
## Get the keys of all children settings with an override.
|
||||
@pyqtSlot(str, result = "QStringList")
|
||||
def getChildrenKeysWithOverride(self, key):
|
||||
definitions = self._global_container_stack.getBottom().findDefinitions(key=key)
|
||||
definitions = self._global_container_stack.definition.findDefinitions(key=key)
|
||||
if not definitions:
|
||||
Logger.log("w", "Could not find definition for key [%s]", key)
|
||||
return []
|
||||
@ -55,7 +55,7 @@ class SettingInheritanceManager(QObject):
|
||||
Logger.log("w", "Unable to find extruder for current machine with index %s", extruder_index)
|
||||
return []
|
||||
|
||||
definitions = self._global_container_stack.getBottom().findDefinitions(key=key)
|
||||
definitions = self._global_container_stack.definition.findDefinitions(key=key)
|
||||
if not definitions:
|
||||
Logger.log("w", "Could not find definition for key [%s] (2)", key)
|
||||
return []
|
||||
@ -93,7 +93,7 @@ class SettingInheritanceManager(QObject):
|
||||
|
||||
def _onPropertyChanged(self, key, property_name):
|
||||
if (property_name == "value" or property_name == "enabled") and self._global_container_stack:
|
||||
definitions = self._global_container_stack.getBottom().findDefinitions(key = key)
|
||||
definitions = self._global_container_stack.definition.findDefinitions(key = key)
|
||||
if not definitions:
|
||||
return
|
||||
|
||||
@ -198,6 +198,10 @@ class SettingInheritanceManager(QObject):
|
||||
def _update(self):
|
||||
self._settings_with_inheritance_warning = [] # Reset previous data.
|
||||
|
||||
# Make sure that the GlobalStack is not None. sometimes the globalContainerChanged signal gets here late.
|
||||
if self._global_container_stack is None:
|
||||
return
|
||||
|
||||
# Check all setting keys that we know of and see if they are overridden.
|
||||
for setting_key in self._global_container_stack.getAllKeys():
|
||||
override = self._settingIsOverwritingInheritance(setting_key)
|
||||
@ -205,7 +209,7 @@ class SettingInheritanceManager(QObject):
|
||||
self._settings_with_inheritance_warning.append(setting_key)
|
||||
|
||||
# Check all the categories if any of their children have their inheritance overwritten.
|
||||
for category in self._global_container_stack.getBottom().findDefinitions(type = "category"):
|
||||
for category in self._global_container_stack.definition.findDefinitions(type = "category"):
|
||||
if self._recursiveCheck(category):
|
||||
self._settings_with_inheritance_warning.append(category.key)
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
from UM.Application import Application
|
||||
|
||||
@ -33,4 +33,9 @@ class UserProfilesModel(ProfilesModel):
|
||||
quality_type_set = set([x.getMetaDataEntry("quality_type") for x in quality_list])
|
||||
filtered_quality_changes = [qc for qc in quality_changes_list if qc.getMetaDataEntry("quality_type") in quality_type_set]
|
||||
|
||||
#Only display the global quality changes.
|
||||
#Otherwise you get multiple copies of every quality changes profile.
|
||||
#The actual profile switching goes by profile name (not ID), and as long as the names are consistent, switching to any of the profiles will cause all stacks to switch.
|
||||
filtered_quality_changes = list(filter(lambda quality_changes: quality_changes.getMetaDataEntry("extruder") is None, filtered_quality_changes))
|
||||
|
||||
return filtered_quality_changes
|
||||
|
@ -3,6 +3,7 @@ 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):
|
||||
super().__init__()
|
||||
self._z_offset = 0
|
||||
|
||||
def setZOffset(self, offset):
|
||||
|
@ -5,6 +5,7 @@
|
||||
import os
|
||||
import sys
|
||||
import platform
|
||||
import faulthandler
|
||||
|
||||
from UM.Platform import Platform
|
||||
|
||||
@ -53,12 +54,14 @@ import Arcus #@UnusedImport
|
||||
import cura.CuraApplication
|
||||
import cura.Settings.CuraContainerRegistry
|
||||
|
||||
if Platform.isWindows() and hasattr(sys, "frozen"):
|
||||
if hasattr(sys, "frozen"):
|
||||
dirpath = os.path.expanduser("~/AppData/Local/cura/")
|
||||
os.makedirs(dirpath, exist_ok = True)
|
||||
sys.stdout = open(os.path.join(dirpath, "stdout.log"), "w")
|
||||
sys.stderr = open(os.path.join(dirpath, "stderr.log"), "w")
|
||||
|
||||
faulthandler.enable()
|
||||
|
||||
# Force an instance of CuraContainerRegistry to be created and reused later.
|
||||
cura.Settings.CuraContainerRegistry.CuraContainerRegistry.getInstance()
|
||||
|
||||
|
654
plugins/3MFReader/ThreeMFWorkspaceReader.py
Normal file → Executable file
654
plugins/3MFReader/ThreeMFWorkspaceReader.py
Normal file → Executable file
@ -1,3 +1,6 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.Workspace.WorkspaceReader import WorkspaceReader
|
||||
from UM.Application import Application
|
||||
|
||||
@ -15,7 +18,10 @@ from .WorkspaceDialog import WorkspaceDialog
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from cura.Settings.ExtruderStack import ExtruderStack
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
||||
from configparser import ConfigParser
|
||||
import zipfile
|
||||
import io
|
||||
import configparser
|
||||
@ -31,10 +37,20 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
self._dialog = WorkspaceDialog()
|
||||
self._3mf_mesh_reader = None
|
||||
self._container_registry = ContainerRegistry.getInstance()
|
||||
self._definition_container_suffix = ContainerRegistry.getMimeTypeForContainer(DefinitionContainer).preferredSuffix
|
||||
|
||||
# suffixes registered with the MineTypes don't start with a dot '.'
|
||||
self._definition_container_suffix = "." + ContainerRegistry.getMimeTypeForContainer(DefinitionContainer).preferredSuffix
|
||||
self._material_container_suffix = None # We have to wait until all other plugins are loaded before we can set it
|
||||
self._instance_container_suffix = ContainerRegistry.getMimeTypeForContainer(InstanceContainer).preferredSuffix
|
||||
self._container_stack_suffix = ContainerRegistry.getMimeTypeForContainer(ContainerStack).preferredSuffix
|
||||
self._instance_container_suffix = "." + ContainerRegistry.getMimeTypeForContainer(InstanceContainer).preferredSuffix
|
||||
self._container_stack_suffix = "." + ContainerRegistry.getMimeTypeForContainer(ContainerStack).preferredSuffix
|
||||
self._extruder_stack_suffix = "." + ContainerRegistry.getMimeTypeForContainer(ExtruderStack).preferredSuffix
|
||||
self._global_stack_suffix = "." + ContainerRegistry.getMimeTypeForContainer(GlobalStack).preferredSuffix
|
||||
|
||||
# Certain instance container types are ignored because we make the assumption that only we make those types
|
||||
# of containers. They are:
|
||||
# - quality
|
||||
# - variant
|
||||
self._ignored_instance_container_types = {"quality", "variant"}
|
||||
|
||||
self._resolve_strategies = {}
|
||||
|
||||
@ -47,6 +63,49 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
self._id_mapping[old_id] = self._container_registry.uniqueName(old_id)
|
||||
return self._id_mapping[old_id]
|
||||
|
||||
## Separates the given file list into a list of GlobalStack files and a list of ExtruderStack files.
|
||||
#
|
||||
# In old versions, extruder stack files have the same suffix as container stack files ".stack.cfg".
|
||||
#
|
||||
def _determineGlobalAndExtruderStackFiles(self, project_file_name, file_list):
|
||||
archive = zipfile.ZipFile(project_file_name, "r")
|
||||
|
||||
global_stack_file_list = [name for name in file_list if name.endswith(self._global_stack_suffix)]
|
||||
extruder_stack_file_list = [name for name in file_list if name.endswith(self._extruder_stack_suffix)]
|
||||
|
||||
# separate container stack files and extruder stack files
|
||||
files_to_determine = [name for name in file_list if name.endswith(self._container_stack_suffix)]
|
||||
for file_name in files_to_determine:
|
||||
# FIXME: HACK!
|
||||
# We need to know the type of the stack file, but we can only know it if we deserialize it.
|
||||
# The default ContainerStack.deserialize() will connect signals, which is not desired in this case.
|
||||
# Since we know that the stack files are INI files, so we directly use the ConfigParser to parse them.
|
||||
serialized = archive.open(file_name).read().decode("utf-8")
|
||||
stack_config = ConfigParser()
|
||||
stack_config.read_string(serialized)
|
||||
|
||||
# sanity check
|
||||
if not stack_config.has_option("metadata", "type"):
|
||||
Logger.log("e", "%s in %s doesn't seem to be valid stack file", file_name, project_file_name)
|
||||
continue
|
||||
|
||||
stack_type = stack_config.get("metadata", "type")
|
||||
if stack_type == "extruder_train":
|
||||
extruder_stack_file_list.append(file_name)
|
||||
elif stack_type == "machine":
|
||||
global_stack_file_list.append(file_name)
|
||||
else:
|
||||
Logger.log("w", "Unknown container stack type '%s' from %s in %s",
|
||||
stack_type, file_name, project_file_name)
|
||||
|
||||
if len(global_stack_file_list) != 1:
|
||||
raise RuntimeError("More than one global stack file found: [%s]" % str(global_stack_file_list))
|
||||
|
||||
return global_stack_file_list[0], extruder_stack_file_list
|
||||
|
||||
## read some info so we can make decisions
|
||||
# \param file_name
|
||||
# \param show_dialog In case we use preRead() to check if a file is a valid project file, we don't want to show a dialog.
|
||||
def preRead(self, file_name, show_dialog=True, *args, **kwargs):
|
||||
self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name)
|
||||
if self._3mf_mesh_reader and self._3mf_mesh_reader.preRead(file_name) == WorkspaceReader.PreReadResult.accepted:
|
||||
@ -59,51 +118,52 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
machine_type = ""
|
||||
variant_type_name = i18n_catalog.i18nc("@label", "Nozzle")
|
||||
|
||||
num_extruders = 0
|
||||
# Check if there are any conflicts, so we can ask the user.
|
||||
archive = zipfile.ZipFile(file_name, "r")
|
||||
cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")]
|
||||
container_stack_files = [name for name in cura_file_names if name.endswith(self._container_stack_suffix)]
|
||||
self._resolve_strategies = {"machine": None, "quality_changes": None, "material": None}
|
||||
machine_conflict = False
|
||||
quality_changes_conflict = False
|
||||
for container_stack_file in container_stack_files:
|
||||
container_id = self._stripFileToId(container_stack_file)
|
||||
serialized = archive.open(container_stack_file).read().decode("utf-8")
|
||||
if machine_name == "":
|
||||
machine_name = self._getMachineNameFromSerializedStack(serialized)
|
||||
stacks = self._container_registry.findContainerStacks(id=container_id)
|
||||
if stacks:
|
||||
# Check if there are any changes at all in any of the container stacks.
|
||||
id_list = self._getContainerIdListFromSerialized(serialized)
|
||||
for index, container_id in enumerate(id_list):
|
||||
if stacks[0].getContainer(index).getId() != container_id:
|
||||
machine_conflict = True
|
||||
Job.yieldThread()
|
||||
|
||||
# A few lists of containers in this project files.
|
||||
# When loading the global stack file, it may be associated with those containers, which may or may not be
|
||||
# in Cura already, so we need to provide them as alternative search lists.
|
||||
definition_container_list = []
|
||||
instance_container_list = []
|
||||
material_container_list = []
|
||||
|
||||
#
|
||||
# Read definition containers
|
||||
#
|
||||
machine_definition_container_count = 0
|
||||
extruder_definition_container_count = 0
|
||||
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
|
||||
for definition_container_file in definition_container_files:
|
||||
container_id = self._stripFileToId(definition_container_file)
|
||||
for each_definition_container_file in definition_container_files:
|
||||
container_id = self._stripFileToId(each_definition_container_file)
|
||||
definitions = self._container_registry.findDefinitionContainers(id=container_id)
|
||||
|
||||
if not definitions:
|
||||
definition_container = DefinitionContainer(container_id)
|
||||
definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"))
|
||||
definition_container.deserialize(archive.open(each_definition_container_file).read().decode("utf-8"))
|
||||
|
||||
else:
|
||||
definition_container = definitions[0]
|
||||
definition_container_list.append(definition_container)
|
||||
|
||||
if definition_container.getMetaDataEntry("type") != "extruder":
|
||||
definition_container_type = definition_container.getMetaDataEntry("type")
|
||||
if definition_container_type == "machine":
|
||||
machine_type = definition_container.getName()
|
||||
variant_type_name = definition_container.getMetaDataEntry("variants_name", variant_type_name)
|
||||
|
||||
machine_definition_container_count += 1
|
||||
elif definition_container_type == "extruder":
|
||||
extruder_definition_container_count += 1
|
||||
else:
|
||||
num_extruders += 1
|
||||
Logger.log("w", "Unknown definition container type %s for %s",
|
||||
definition_container_type, each_definition_container_file)
|
||||
Job.yieldThread()
|
||||
|
||||
if num_extruders == 0:
|
||||
num_extruders = 1 # No extruder stacks found, which means there is one extruder
|
||||
|
||||
extruders = num_extruders * [""]
|
||||
# sanity check
|
||||
if machine_definition_container_count != 1:
|
||||
msg = "Expecting one machine definition container but got %s" % machine_definition_container_count
|
||||
Logger.log("e", msg)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
material_labels = []
|
||||
material_conflict = False
|
||||
@ -119,18 +179,25 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
if materials and not materials[0].isReadOnly(): # Only non readonly materials can be in conflict
|
||||
material_conflict = True
|
||||
Job.yieldThread()
|
||||
|
||||
# Check if any quality_changes instance container is in conflict.
|
||||
instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)]
|
||||
quality_name = ""
|
||||
quality_type = ""
|
||||
num_settings_overriden_by_quality_changes = 0 # How many settings are changed by the quality changes
|
||||
num_settings_overriden_by_definition_changes = 0 # How many settings are changed by the definition changes
|
||||
num_user_settings = 0
|
||||
for instance_container_file in instance_container_files:
|
||||
container_id = self._stripFileToId(instance_container_file)
|
||||
quality_changes_conflict = False
|
||||
definition_changes_conflict = False
|
||||
|
||||
for each_instance_container_file in instance_container_files:
|
||||
container_id = self._stripFileToId(each_instance_container_file)
|
||||
instance_container = InstanceContainer(container_id)
|
||||
|
||||
# Deserialize InstanceContainer by converting read data from bytes to string
|
||||
instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8"))
|
||||
instance_container.deserialize(archive.open(each_instance_container_file).read().decode("utf-8"))
|
||||
instance_container_list.append(instance_container)
|
||||
|
||||
container_type = instance_container.getMetaDataEntry("type")
|
||||
if container_type == "quality_changes":
|
||||
quality_name = instance_container.getName()
|
||||
@ -141,16 +208,41 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
# Check if there really is a conflict by comparing the values
|
||||
if quality_changes[0] != instance_container:
|
||||
quality_changes_conflict = True
|
||||
elif container_type == "quality":
|
||||
# If the quality name is not set (either by quality or changes, set it now)
|
||||
# Quality changes should always override this (as they are "on top")
|
||||
if quality_name == "":
|
||||
quality_name = instance_container.getName()
|
||||
quality_type = instance_container.getName()
|
||||
elif container_type == "definition_changes":
|
||||
definition_name = instance_container.getName()
|
||||
num_settings_overriden_by_definition_changes += len(instance_container._instances)
|
||||
definition_changes = self._container_registry.findDefinitionContainers(id = container_id)
|
||||
if definition_changes:
|
||||
if definition_changes[0] != instance_container:
|
||||
definition_changes_conflict = True
|
||||
elif container_type == "user":
|
||||
num_user_settings += len(instance_container._instances)
|
||||
elif container_type in self._ignored_instance_container_types:
|
||||
# Ignore certain instance container types
|
||||
Logger.log("w", "Ignoring instance container [%s] with type [%s]", container_id, container_type)
|
||||
continue
|
||||
|
||||
Job.yieldThread()
|
||||
|
||||
# Load ContainerStack files and ExtruderStack files
|
||||
global_stack_file, extruder_stack_files = self._determineGlobalAndExtruderStackFiles(
|
||||
file_name, cura_file_names)
|
||||
self._resolve_strategies = {"machine": None, "quality_changes": None, "material": None}
|
||||
machine_conflict = False
|
||||
for container_stack_file in [global_stack_file] + extruder_stack_files:
|
||||
container_id = self._stripFileToId(container_stack_file)
|
||||
serialized = archive.open(container_stack_file).read().decode("utf-8")
|
||||
if machine_name == "":
|
||||
machine_name = self._getMachineNameFromSerializedStack(serialized)
|
||||
stacks = self._container_registry.findContainerStacks(id = container_id)
|
||||
if stacks:
|
||||
# Check if there are any changes at all in any of the container stacks.
|
||||
id_list = self._getContainerIdListFromSerialized(serialized)
|
||||
for index, container_id in enumerate(id_list):
|
||||
if stacks[0].getContainer(index).getId() != container_id:
|
||||
machine_conflict = True
|
||||
Job.yieldThread()
|
||||
|
||||
num_visible_settings = 0
|
||||
try:
|
||||
temp_preferences = Preferences()
|
||||
@ -171,9 +263,17 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
if not show_dialog:
|
||||
return WorkspaceReader.PreReadResult.accepted
|
||||
|
||||
# prepare data for the dialog
|
||||
num_extruders = extruder_definition_container_count
|
||||
if num_extruders == 0:
|
||||
num_extruders = 1 # No extruder stacks found, which means there is one extruder
|
||||
|
||||
extruders = num_extruders * [""]
|
||||
|
||||
# Show the dialog, informing the user what is about to happen.
|
||||
self._dialog.setMachineConflict(machine_conflict)
|
||||
self._dialog.setQualityChangesConflict(quality_changes_conflict)
|
||||
self._dialog.setDefinitionChangesConflict(definition_changes_conflict)
|
||||
self._dialog.setMaterialConflict(material_conflict)
|
||||
self._dialog.setNumVisibleSettings(num_visible_settings)
|
||||
self._dialog.setQualityName(quality_name)
|
||||
@ -196,9 +296,47 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
return WorkspaceReader.PreReadResult.cancelled
|
||||
|
||||
self._resolve_strategies = self._dialog.getResult()
|
||||
#
|
||||
# There can be 3 resolve strategies coming from the dialog:
|
||||
# - new: create a new container
|
||||
# - override: override the existing container
|
||||
# - None: There is no conflict, which means containers with the same IDs may or may not be there already.
|
||||
# If they are there, there is no conflict between the them.
|
||||
# In this case, you can either create a new one, or safely override the existing one.
|
||||
#
|
||||
# Default values
|
||||
for k, v in self._resolve_strategies.items():
|
||||
if v is None:
|
||||
self._resolve_strategies[k] = "new"
|
||||
|
||||
return WorkspaceReader.PreReadResult.accepted
|
||||
|
||||
## Overrides an ExtruderStack in the given GlobalStack and returns the new ExtruderStack.
|
||||
def _overrideExtruderStack(self, global_stack, extruder_file_content):
|
||||
# get extruder position first
|
||||
extruder_config = configparser.ConfigParser()
|
||||
extruder_config.read_string(extruder_file_content)
|
||||
if not extruder_config.has_option("metadata", "position"):
|
||||
msg = "Could not find 'metadata/position' in extruder stack file"
|
||||
Logger.log("e", "Could not find 'metadata/position' in extruder stack file")
|
||||
raise RuntimeError(msg)
|
||||
extruder_position = extruder_config.get("metadata", "position")
|
||||
|
||||
extruder_stack = global_stack.extruders[extruder_position]
|
||||
|
||||
# override the given extruder stack
|
||||
extruder_stack.deserialize(extruder_file_content)
|
||||
|
||||
# return the new ExtruderStack
|
||||
return extruder_stack
|
||||
|
||||
## Read the project file
|
||||
# Add all the definitions / materials / quality changes that do not exist yet. Then it loads
|
||||
# all the stacks into the container registry. In some cases it will reuse the container for the global stack.
|
||||
# It handles old style project files containing .stack.cfg as well as new style project files
|
||||
# containing global.cfg / extruder.cfg
|
||||
#
|
||||
# \param file_name
|
||||
def read(self, file_name):
|
||||
archive = zipfile.ZipFile(file_name, "r")
|
||||
|
||||
@ -232,6 +370,35 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
# We do this so that if something goes wrong, it's easier to clean up.
|
||||
containers_to_add = []
|
||||
|
||||
global_stack_file, extruder_stack_files = self._determineGlobalAndExtruderStackFiles(file_name, cura_file_names)
|
||||
|
||||
global_stack = None
|
||||
extruder_stacks = []
|
||||
extruder_stacks_added = []
|
||||
container_stacks_added = []
|
||||
|
||||
containers_added = []
|
||||
|
||||
global_stack_id_original = self._stripFileToId(global_stack_file)
|
||||
global_stack_id_new = global_stack_id_original
|
||||
global_stack_need_rename = False
|
||||
|
||||
extruder_stack_id_map = {} # new and old ExtruderStack IDs map
|
||||
if self._resolve_strategies["machine"] == "new":
|
||||
# We need a new id if the id already exists
|
||||
if self._container_registry.findContainerStacks(id = global_stack_id_original):
|
||||
global_stack_id_new = self.getNewId(global_stack_id_original)
|
||||
global_stack_need_rename = True
|
||||
|
||||
for each_extruder_stack_file in extruder_stack_files:
|
||||
old_container_id = self._stripFileToId(each_extruder_stack_file)
|
||||
new_container_id = old_container_id
|
||||
if self._container_registry.findContainerStacks(id = old_container_id):
|
||||
# get a new name for this extruder
|
||||
new_container_id = self.getNewId(old_container_id)
|
||||
|
||||
extruder_stack_id_map[old_container_id] = new_container_id
|
||||
|
||||
# TODO: For the moment we use pretty naive existence checking. If the ID is the same, we assume in quite a few
|
||||
# TODO: cases that the container loaded is the same (most notable in materials & definitions).
|
||||
# TODO: It might be possible that we need to add smarter checking in the future.
|
||||
@ -240,7 +407,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
|
||||
for definition_container_file in definition_container_files:
|
||||
container_id = self._stripFileToId(definition_container_file)
|
||||
definitions = self._container_registry.findDefinitionContainers(id=container_id)
|
||||
definitions = self._container_registry.findDefinitionContainers(id = container_id)
|
||||
if not definitions:
|
||||
definition_container = DefinitionContainer(container_id)
|
||||
definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"))
|
||||
@ -257,222 +424,403 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
|
||||
for material_container_file in material_container_files:
|
||||
container_id = self._stripFileToId(material_container_file)
|
||||
materials = self._container_registry.findInstanceContainers(id=container_id)
|
||||
materials = self._container_registry.findInstanceContainers(id = container_id)
|
||||
|
||||
if not materials:
|
||||
material_container = xml_material_profile(container_id)
|
||||
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"))
|
||||
containers_to_add.append(material_container)
|
||||
else:
|
||||
if not materials[0].isReadOnly(): # Only create new materials if they are not read only.
|
||||
material_container = materials[0]
|
||||
if not material_container.isReadOnly(): # Only create new materials if they are not read only.
|
||||
if self._resolve_strategies["material"] == "override":
|
||||
materials[0].deserialize(archive.open(material_container_file).read().decode("utf-8"))
|
||||
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"))
|
||||
elif self._resolve_strategies["material"] == "new":
|
||||
# Note that we *must* deserialize it with a new ID, as multiple containers will be
|
||||
# auto created & added.
|
||||
material_container = xml_material_profile(self.getNewId(container_id))
|
||||
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"))
|
||||
containers_to_add.append(material_container)
|
||||
material_containers.append(material_container)
|
||||
|
||||
material_containers.append(material_container)
|
||||
Job.yieldThread()
|
||||
|
||||
Logger.log("d", "Workspace loading is checking instance containers...")
|
||||
# Get quality_changes and user profiles saved in the workspace
|
||||
instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)]
|
||||
user_instance_containers = []
|
||||
quality_changes_instance_containers = []
|
||||
quality_and_definition_changes_instance_containers = []
|
||||
for instance_container_file in instance_container_files:
|
||||
container_id = self._stripFileToId(instance_container_file)
|
||||
serialized = archive.open(instance_container_file).read().decode("utf-8")
|
||||
|
||||
# HACK! we ignore "quality" and "variant" instance containers!
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read_string(serialized)
|
||||
if not parser.has_option("metadata", "type"):
|
||||
Logger.log("w", "Cannot find metadata/type in %s, ignoring it", instance_container_file)
|
||||
continue
|
||||
if parser.get("metadata", "type") in self._ignored_instance_container_types:
|
||||
continue
|
||||
|
||||
instance_container = InstanceContainer(container_id)
|
||||
|
||||
# Deserialize InstanceContainer by converting read data from bytes to string
|
||||
instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8"))
|
||||
instance_container.deserialize(serialized)
|
||||
container_type = instance_container.getMetaDataEntry("type")
|
||||
Job.yieldThread()
|
||||
if container_type == "user":
|
||||
|
||||
#
|
||||
# IMPORTANT:
|
||||
# If an instance container (or maybe other type of container) exists, and user chooses "Create New",
|
||||
# we need to rename this container and all references to it, and changing those references are VERY
|
||||
# HARD.
|
||||
#
|
||||
if container_type in self._ignored_instance_container_types:
|
||||
# Ignore certain instance container types
|
||||
Logger.log("w", "Ignoring instance container [%s] with type [%s]", container_id, container_type)
|
||||
continue
|
||||
elif container_type == "user":
|
||||
# Check if quality changes already exists.
|
||||
user_containers = self._container_registry.findInstanceContainers(id=container_id)
|
||||
user_containers = self._container_registry.findInstanceContainers(id = container_id)
|
||||
if not user_containers:
|
||||
containers_to_add.append(instance_container)
|
||||
else:
|
||||
if self._resolve_strategies["machine"] == "override" or self._resolve_strategies["machine"] is None:
|
||||
user_containers[0].deserialize(archive.open(instance_container_file).read().decode("utf-8"))
|
||||
instance_container = user_containers[0]
|
||||
instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8"))
|
||||
instance_container.setDirty(True)
|
||||
elif self._resolve_strategies["machine"] == "new":
|
||||
# The machine is going to get a spiffy new name, so ensure that the id's of user settings match.
|
||||
extruder_id = instance_container.getMetaDataEntry("extruder", None)
|
||||
if extruder_id:
|
||||
new_id = self.getNewId(extruder_id) + "_current_settings"
|
||||
old_extruder_id = instance_container.getMetaDataEntry("extruder", None)
|
||||
if old_extruder_id:
|
||||
new_extruder_id = extruder_stack_id_map[old_extruder_id]
|
||||
new_id = new_extruder_id + "_current_settings"
|
||||
instance_container._id = new_id
|
||||
instance_container.setName(new_id)
|
||||
instance_container.setMetaDataEntry("extruder", self.getNewId(extruder_id))
|
||||
instance_container.setMetaDataEntry("extruder", new_extruder_id)
|
||||
containers_to_add.append(instance_container)
|
||||
|
||||
machine_id = instance_container.getMetaDataEntry("machine", None)
|
||||
if machine_id:
|
||||
new_id = self.getNewId(machine_id) + "_current_settings"
|
||||
new_machine_id = self.getNewId(machine_id)
|
||||
new_id = new_machine_id + "_current_settings"
|
||||
instance_container._id = new_id
|
||||
instance_container.setName(new_id)
|
||||
instance_container.setMetaDataEntry("machine", self.getNewId(machine_id))
|
||||
instance_container.setMetaDataEntry("machine", new_machine_id)
|
||||
containers_to_add.append(instance_container)
|
||||
user_instance_containers.append(instance_container)
|
||||
elif container_type == "quality_changes":
|
||||
elif container_type in ("quality_changes", "definition_changes"):
|
||||
# Check if quality changes already exists.
|
||||
quality_changes = self._container_registry.findInstanceContainers(id = container_id)
|
||||
if not quality_changes:
|
||||
changes_containers = self._container_registry.findInstanceContainers(id = container_id)
|
||||
if not changes_containers:
|
||||
# no existing containers with the same ID, so we can safely add the new one
|
||||
containers_to_add.append(instance_container)
|
||||
else:
|
||||
if self._resolve_strategies["quality_changes"] == "override":
|
||||
quality_changes[0].deserialize(archive.open(instance_container_file).read().decode("utf-8"))
|
||||
elif self._resolve_strategies["quality_changes"] is None:
|
||||
# we have found existing container with the same ID, so we need to resolve according to the
|
||||
# selected strategy.
|
||||
if self._resolve_strategies[container_type] == "override":
|
||||
instance_container = changes_containers[0]
|
||||
instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8"))
|
||||
instance_container.setDirty(True)
|
||||
|
||||
elif self._resolve_strategies[container_type] == "new":
|
||||
# TODO: how should we handle the case "new" for quality_changes and definition_changes?
|
||||
|
||||
instance_container.setName(self._container_registry.uniqueName(instance_container.getName()))
|
||||
new_changes_container_id = self.getNewId(instance_container.getId())
|
||||
instance_container._id = new_changes_container_id
|
||||
|
||||
# TODO: we don't know the following is correct or not, need to verify
|
||||
# AND REFACTOR!!!
|
||||
if self._resolve_strategies["machine"] == "new":
|
||||
# The machine is going to get a spiffy new name, so ensure that the id's of user settings match.
|
||||
old_extruder_id = instance_container.getMetaDataEntry("extruder", None)
|
||||
if old_extruder_id:
|
||||
new_extruder_id = extruder_stack_id_map[old_extruder_id]
|
||||
instance_container.setMetaDataEntry("extruder", new_extruder_id)
|
||||
|
||||
machine_id = instance_container.getMetaDataEntry("machine", None)
|
||||
if machine_id:
|
||||
new_machine_id = self.getNewId(machine_id)
|
||||
instance_container.setMetaDataEntry("machine", new_machine_id)
|
||||
|
||||
containers_to_add.append(instance_container)
|
||||
|
||||
elif self._resolve_strategies[container_type] is None:
|
||||
# The ID already exists, but nothing in the values changed, so do nothing.
|
||||
pass
|
||||
quality_changes_instance_containers.append(instance_container)
|
||||
quality_and_definition_changes_instance_containers.append(instance_container)
|
||||
else:
|
||||
continue
|
||||
existing_container = self._container_registry.findInstanceContainers(id = container_id)
|
||||
if not existing_container:
|
||||
containers_to_add.append(instance_container)
|
||||
if global_stack_need_rename:
|
||||
if instance_container.getMetaDataEntry("machine"):
|
||||
instance_container.setMetaDataEntry("machine", global_stack_id_new)
|
||||
|
||||
# Add all the containers right before we try to add / serialize the stack
|
||||
for container in containers_to_add:
|
||||
self._container_registry.addContainer(container)
|
||||
container.setDirty(True)
|
||||
containers_added.append(container)
|
||||
|
||||
# Get the stack(s) saved in the workspace.
|
||||
Logger.log("d", "Workspace loading is checking stacks containers...")
|
||||
container_stack_files = [name for name in cura_file_names if name.endswith(self._container_stack_suffix)]
|
||||
global_stack = None
|
||||
extruder_stacks = []
|
||||
container_stacks_added = []
|
||||
try:
|
||||
for container_stack_file in container_stack_files:
|
||||
container_id = self._stripFileToId(container_stack_file)
|
||||
|
||||
# Check if a stack by this ID already exists;
|
||||
container_stacks = self._container_registry.findContainerStacks(id=container_id)
|
||||
# --
|
||||
# load global stack file
|
||||
try:
|
||||
# Check if a stack by this ID already exists;
|
||||
container_stacks = self._container_registry.findContainerStacks(id = global_stack_id_original)
|
||||
if container_stacks:
|
||||
stack = container_stacks[0]
|
||||
|
||||
if self._resolve_strategies["machine"] == "override":
|
||||
# TODO: HACK
|
||||
# There is a machine, check if it has authentication data. If so, keep that data.
|
||||
network_authentication_id = container_stacks[0].getMetaDataEntry("network_authentication_id")
|
||||
network_authentication_key = container_stacks[0].getMetaDataEntry("network_authentication_key")
|
||||
container_stacks[0].deserialize(archive.open(global_stack_file).read().decode("utf-8"))
|
||||
if network_authentication_id:
|
||||
container_stacks[0].addMetaDataEntry("network_authentication_id", network_authentication_id)
|
||||
if network_authentication_key:
|
||||
container_stacks[0].addMetaDataEntry("network_authentication_key", network_authentication_key)
|
||||
elif self._resolve_strategies["machine"] == "new":
|
||||
stack = GlobalStack(global_stack_id_new)
|
||||
stack.deserialize(archive.open(global_stack_file).read().decode("utf-8"))
|
||||
|
||||
# Ensure a unique ID and name
|
||||
stack._id = global_stack_id_new
|
||||
|
||||
# Extruder stacks are "bound" to a machine. If we add the machine as a new one, the id of the
|
||||
# bound machine also needs to change.
|
||||
if stack.getMetaDataEntry("machine", None):
|
||||
stack.setMetaDataEntry("machine", global_stack_id_new)
|
||||
|
||||
# Only machines need a new name, stacks may be non-unique
|
||||
stack.setName(self._container_registry.uniqueName(stack.getName()))
|
||||
container_stacks_added.append(stack)
|
||||
self._container_registry.addContainer(stack)
|
||||
else:
|
||||
Logger.log("w", "Resolve strategy of %s for machine is not supported", self._resolve_strategies["machine"])
|
||||
else:
|
||||
# no existing container stack, so we create a new one
|
||||
stack = GlobalStack(global_stack_id_new)
|
||||
# Deserialize stack by converting read data from bytes to string
|
||||
stack.deserialize(archive.open(global_stack_file).read().decode("utf-8"))
|
||||
container_stacks_added.append(stack)
|
||||
self._container_registry.addContainer(stack)
|
||||
containers_added.append(stack)
|
||||
|
||||
global_stack = stack
|
||||
Job.yieldThread()
|
||||
except:
|
||||
Logger.logException("w", "We failed to serialize the stack. Trying to clean up.")
|
||||
# Something went really wrong. Try to remove any data that we added.
|
||||
for container in containers_added:
|
||||
self._container_registry.removeContainer(container.getId())
|
||||
return
|
||||
|
||||
# --
|
||||
# load extruder stack files
|
||||
try:
|
||||
for index, extruder_stack_file in enumerate(extruder_stack_files):
|
||||
container_id = self._stripFileToId(extruder_stack_file)
|
||||
extruder_file_content = archive.open(extruder_stack_file, "r").read().decode("utf-8")
|
||||
|
||||
container_stacks = self._container_registry.findContainerStacks(id = container_id)
|
||||
if container_stacks:
|
||||
# this container stack already exists, try to resolve
|
||||
stack = container_stacks[0]
|
||||
|
||||
if self._resolve_strategies["machine"] == "override":
|
||||
# TODO: HACK
|
||||
# There is a machine, check if it has authenticationd data. If so, keep that data.
|
||||
network_authentication_id = container_stacks[0].getMetaDataEntry("network_authentication_id")
|
||||
network_authentication_key = container_stacks[0].getMetaDataEntry("network_authentication_key")
|
||||
container_stacks[0].deserialize(archive.open(container_stack_file).read().decode("utf-8"))
|
||||
if network_authentication_id:
|
||||
container_stacks[0].addMetaDataEntry("network_authentication_id", network_authentication_id)
|
||||
if network_authentication_key:
|
||||
container_stacks[0].addMetaDataEntry("network_authentication_key", network_authentication_key)
|
||||
# NOTE: This is the same code as those in the lower part
|
||||
# deserialize new extruder stack over the current ones
|
||||
stack = self._overrideExtruderStack(global_stack, extruder_file_content)
|
||||
|
||||
elif self._resolve_strategies["machine"] == "new":
|
||||
new_id = self.getNewId(container_id)
|
||||
stack = ContainerStack(new_id)
|
||||
stack.deserialize(archive.open(container_stack_file).read().decode("utf-8"))
|
||||
# create a new extruder stack from this one
|
||||
new_id = extruder_stack_id_map[container_id]
|
||||
stack = ExtruderStack(new_id)
|
||||
|
||||
# HACK: the global stack can have a new name, so we need to make sure that this extruder stack
|
||||
# references to the new name instead of the old one. Normally, this can be done after
|
||||
# deserialize() by setting the metadata, but in the case of ExtruderStack, deserialize()
|
||||
# also does addExtruder() to its machine stack, so we have to make sure that it's pointing
|
||||
# to the right machine BEFORE deserialization.
|
||||
extruder_config = configparser.ConfigParser()
|
||||
extruder_config.read_string(extruder_file_content)
|
||||
extruder_config.set("metadata", "machine", global_stack_id_new)
|
||||
tmp_string_io = io.StringIO()
|
||||
extruder_config.write(tmp_string_io)
|
||||
extruder_file_content = tmp_string_io.getvalue()
|
||||
|
||||
stack.deserialize(extruder_file_content)
|
||||
|
||||
# Ensure a unique ID and name
|
||||
stack._id = new_id
|
||||
|
||||
# Extruder stacks are "bound" to a machine. If we add the machine as a new one, the id of the
|
||||
# bound machine also needs to change.
|
||||
if stack.getMetaDataEntry("machine", None):
|
||||
stack.setMetaDataEntry("machine", self.getNewId(stack.getMetaDataEntry("machine")))
|
||||
|
||||
if stack.getMetaDataEntry("type") != "extruder_train":
|
||||
# Only machines need a new name, stacks may be non-unique
|
||||
stack.setName(self._container_registry.uniqueName(stack.getName()))
|
||||
container_stacks_added.append(stack)
|
||||
self._container_registry.addContainer(stack)
|
||||
else:
|
||||
Logger.log("w", "Resolve strategy of %s for machine is not supported", self._resolve_strategies["machine"])
|
||||
extruder_stacks_added.append(stack)
|
||||
containers_added.append(stack)
|
||||
else:
|
||||
stack = ContainerStack(container_id)
|
||||
# Deserialize stack by converting read data from bytes to string
|
||||
stack.deserialize(archive.open(container_stack_file).read().decode("utf-8"))
|
||||
container_stacks_added.append(stack)
|
||||
self._container_registry.addContainer(stack)
|
||||
# No extruder stack with the same ID can be found
|
||||
if self._resolve_strategies["machine"] == "override":
|
||||
# deserialize new extruder stack over the current ones
|
||||
stack = self._overrideExtruderStack(global_stack, extruder_file_content)
|
||||
|
||||
if stack.getMetaDataEntry("type") == "extruder_train":
|
||||
extruder_stacks.append(stack)
|
||||
else:
|
||||
global_stack = stack
|
||||
Job.yieldThread()
|
||||
elif self._resolve_strategies["machine"] == "new":
|
||||
# container not found, create a new one
|
||||
stack = ExtruderStack(container_id)
|
||||
|
||||
# HACK: the global stack can have a new name, so we need to make sure that this extruder stack
|
||||
# references to the new name instead of the old one. Normally, this can be done after
|
||||
# deserialize() by setting the metadata, but in the case of ExtruderStack, deserialize()
|
||||
# also does addExtruder() to its machine stack, so we have to make sure that it's pointing
|
||||
# to the right machine BEFORE deserialization.
|
||||
extruder_config = configparser.ConfigParser()
|
||||
extruder_config.read_string(extruder_file_content)
|
||||
extruder_config.set("metadata", "machine", global_stack_id_new)
|
||||
tmp_string_io = io.StringIO()
|
||||
extruder_config.write(tmp_string_io)
|
||||
extruder_file_content = tmp_string_io.getvalue()
|
||||
|
||||
stack.deserialize(extruder_file_content)
|
||||
self._container_registry.addContainer(stack)
|
||||
extruder_stacks_added.append(stack)
|
||||
containers_added.append(stack)
|
||||
else:
|
||||
Logger.log("w", "Unknown resolve strategy: %s" % str(self._resolve_strategies["machine"]))
|
||||
|
||||
extruder_stacks.append(stack)
|
||||
except:
|
||||
Logger.logException("w", "We failed to serialize the stack. Trying to clean up.")
|
||||
# Something went really wrong. Try to remove any data that we added.
|
||||
for container in containers_to_add:
|
||||
self._container_registry.getInstance().removeContainer(container.getId())
|
||||
|
||||
for container in container_stacks_added:
|
||||
self._container_registry.getInstance().removeContainer(container.getId())
|
||||
|
||||
return None
|
||||
for container in containers_added:
|
||||
self._container_registry.removeContainer(container.getId())
|
||||
return
|
||||
|
||||
#
|
||||
# Replacing the old containers if resolve is "new".
|
||||
# When resolve is "new", some containers will get renamed, so all the other containers that reference to those
|
||||
# MUST get updated too.
|
||||
#
|
||||
if self._resolve_strategies["machine"] == "new":
|
||||
# A new machine was made, but it was serialized with the wrong user container. Fix that now.
|
||||
for container in user_instance_containers:
|
||||
# replacing the container ID for user instance containers for the extruders
|
||||
extruder_id = container.getMetaDataEntry("extruder", None)
|
||||
if extruder_id:
|
||||
for extruder in extruder_stacks:
|
||||
if extruder.getId() == extruder_id:
|
||||
extruder.replaceContainer(0, container)
|
||||
extruder.userChanges = container
|
||||
continue
|
||||
|
||||
# replacing the container ID for user instance containers for the machine
|
||||
machine_id = container.getMetaDataEntry("machine", None)
|
||||
if machine_id:
|
||||
if global_stack.getId() == machine_id:
|
||||
global_stack.replaceContainer(0, container)
|
||||
global_stack.userChanges = container
|
||||
continue
|
||||
|
||||
if self._resolve_strategies["quality_changes"] == "new":
|
||||
# Quality changes needs to get a new ID, added to registry and to the right stacks
|
||||
for container in quality_changes_instance_containers:
|
||||
old_id = container.getId()
|
||||
container.setName(self._container_registry.uniqueName(container.getName()))
|
||||
# We're not really supposed to change the ID in normal cases, but this is an exception.
|
||||
container._id = self.getNewId(container.getId())
|
||||
for changes_container_type in ("quality_changes", "definition_changes"):
|
||||
if self._resolve_strategies[changes_container_type] == "new":
|
||||
# Quality changes needs to get a new ID, added to registry and to the right stacks
|
||||
for each_changes_container in quality_and_definition_changes_instance_containers:
|
||||
# NOTE: The renaming and giving new IDs are possibly redundant because they are done in the
|
||||
# instance container loading part.
|
||||
new_id = each_changes_container.getId()
|
||||
|
||||
# The container was not added yet, as it didn't have an unique ID. It does now, so add it.
|
||||
self._container_registry.addContainer(container)
|
||||
# Find the old (current) changes container in the global stack
|
||||
if changes_container_type == "quality_changes":
|
||||
old_container = global_stack.qualityChanges
|
||||
elif changes_container_type == "definition_changes":
|
||||
old_container = global_stack.definitionChanges
|
||||
|
||||
# Replace the quality changes container
|
||||
old_container = global_stack.findContainer({"type": "quality_changes"})
|
||||
if old_container.getId() == old_id:
|
||||
quality_changes_index = global_stack.getContainerIndex(old_container)
|
||||
global_stack.replaceContainer(quality_changes_index, container)
|
||||
continue
|
||||
# sanity checks
|
||||
# NOTE: The following cases SHOULD NOT happen!!!!
|
||||
if not old_container:
|
||||
Logger.log("e", "We try to get [%s] from the global stack [%s] but we got None instead!",
|
||||
changes_container_type, global_stack.getId())
|
||||
|
||||
for stack in extruder_stacks:
|
||||
old_container = stack.findContainer({"type": "quality_changes"})
|
||||
if old_container.getId() == old_id:
|
||||
quality_changes_index = stack.getContainerIndex(old_container)
|
||||
stack.replaceContainer(quality_changes_index, container)
|
||||
# Replace the quality/definition changes container if it's in the GlobalStack
|
||||
# NOTE: we can get an empty container here, but the IDs will not match,
|
||||
# so this comparison is fine.
|
||||
if self._id_mapping.get(old_container.getId()) == new_id:
|
||||
if changes_container_type == "quality_changes":
|
||||
global_stack.qualityChanges = each_changes_container
|
||||
elif changes_container_type == "definition_changes":
|
||||
global_stack.definitionChanges = each_changes_container
|
||||
continue
|
||||
|
||||
# Replace the quality/definition changes container if it's in one of the ExtruderStacks
|
||||
for each_extruder_stack in extruder_stacks:
|
||||
changes_container = None
|
||||
if changes_container_type == "quality_changes":
|
||||
changes_container = each_extruder_stack.qualityChanges
|
||||
elif changes_container_type == "definition_changes":
|
||||
changes_container = each_extruder_stack.definitionChanges
|
||||
|
||||
# sanity checks
|
||||
# NOTE: The following cases SHOULD NOT happen!!!!
|
||||
if not changes_container:
|
||||
Logger.log("e", "We try to get [%s] from the extruder stack [%s] but we got None instead!",
|
||||
changes_container_type, each_extruder_stack.getId())
|
||||
|
||||
# NOTE: we can get an empty container here, but the IDs will not match,
|
||||
# so this comparison is fine.
|
||||
if self._id_mapping.get(changes_container.getId()) == new_id:
|
||||
if changes_container_type == "quality_changes":
|
||||
each_extruder_stack.qualityChanges = each_changes_container
|
||||
elif changes_container_type == "definition_changes":
|
||||
each_extruder_stack.definitionChanges = each_changes_container
|
||||
|
||||
if self._resolve_strategies["material"] == "new":
|
||||
for material in material_containers:
|
||||
old_material = global_stack.findContainer({"type": "material"})
|
||||
if old_material.getId() in self._id_mapping:
|
||||
material_index = global_stack.getContainerIndex(old_material)
|
||||
global_stack.replaceContainer(material_index, material)
|
||||
for each_material in material_containers:
|
||||
old_material = global_stack.material
|
||||
|
||||
# check if the old material container has been renamed to this material container ID
|
||||
# if the container hasn't been renamed, we do nothing.
|
||||
new_id = self._id_mapping.get(old_material.getId())
|
||||
if new_id is None or new_id != each_material.getId():
|
||||
continue
|
||||
|
||||
for stack in extruder_stacks:
|
||||
old_material = stack.findContainer({"type": "material"})
|
||||
if old_material.getId() in self._id_mapping:
|
||||
material_index = stack.getContainerIndex(old_material)
|
||||
stack.replaceContainer(material_index, material)
|
||||
if old_material.getId() in self._id_mapping:
|
||||
global_stack.material = each_material
|
||||
|
||||
for each_extruder_stack in extruder_stacks:
|
||||
old_material = each_extruder_stack.material
|
||||
|
||||
# check if the old material container has been renamed to this material container ID
|
||||
# if the container hasn't been renamed, we do nothing.
|
||||
new_id = self._id_mapping.get(old_material.getId())
|
||||
if new_id is None or new_id != each_material.getId():
|
||||
continue
|
||||
|
||||
for stack in extruder_stacks:
|
||||
ExtruderManager.getInstance().registerExtruder(stack, global_stack.getId())
|
||||
if old_material.getId() in self._id_mapping:
|
||||
each_extruder_stack.material = each_material
|
||||
|
||||
if extruder_stacks:
|
||||
for stack in extruder_stacks:
|
||||
ExtruderManager.getInstance().registerExtruder(stack, global_stack.getId())
|
||||
else:
|
||||
# Machine has no extruders, but it needs to be registered with the extruder manager.
|
||||
ExtruderManager.getInstance().registerExtruder(None, global_stack.getId())
|
||||
|
||||
Logger.log("d", "Workspace loading is notifying rest of the code of changes...")
|
||||
|
||||
# Notify everything/one that is to notify about changes.
|
||||
global_stack.containersChanged.emit(global_stack.getTop())
|
||||
|
||||
for stack in extruder_stacks:
|
||||
stack.setNextStack(global_stack)
|
||||
stack.containersChanged.emit(stack.getTop())
|
||||
if self._resolve_strategies["machine"] == "new":
|
||||
for stack in extruder_stacks:
|
||||
stack.setNextStack(global_stack)
|
||||
stack.containersChanged.emit(stack.getTop())
|
||||
|
||||
# Actually change the active machine.
|
||||
Application.getInstance().setGlobalContainerStack(global_stack)
|
||||
|
||||
# Notify everything/one that is to notify about changes.
|
||||
global_stack.containersChanged.emit(global_stack.getTop())
|
||||
|
||||
# Load all the nodes / meshdata of the workspace
|
||||
nodes = self._3mf_mesh_reader.read(file_name)
|
||||
if nodes is None:
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt, QUrl, pyqtSignal, QObject, pyqtProperty, QCoreApplication
|
||||
from PyQt5.QtCore import QUrl, pyqtSignal, QObject, pyqtProperty, QCoreApplication
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from PyQt5.QtQml import QQmlComponent, QQmlContext
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
@ -29,11 +29,13 @@ class WorkspaceDialog(QObject):
|
||||
self._default_strategy = "override"
|
||||
self._result = {"machine": self._default_strategy,
|
||||
"quality_changes": self._default_strategy,
|
||||
"definition_changes": self._default_strategy,
|
||||
"material": self._default_strategy}
|
||||
self._visible = False
|
||||
self.showDialogSignal.connect(self.__show)
|
||||
|
||||
self._has_quality_changes_conflict = False
|
||||
self._has_definition_changes_conflict = False
|
||||
self._has_machine_conflict = False
|
||||
self._has_material_conflict = False
|
||||
self._num_visible_settings = 0
|
||||
@ -51,6 +53,7 @@ class WorkspaceDialog(QObject):
|
||||
|
||||
machineConflictChanged = pyqtSignal()
|
||||
qualityChangesConflictChanged = pyqtSignal()
|
||||
definitionChangesConflictChanged = pyqtSignal()
|
||||
materialConflictChanged = pyqtSignal()
|
||||
numVisibleSettingsChanged = pyqtSignal()
|
||||
activeModeChanged = pyqtSignal()
|
||||
@ -185,6 +188,10 @@ class WorkspaceDialog(QObject):
|
||||
def qualityChangesConflict(self):
|
||||
return self._has_quality_changes_conflict
|
||||
|
||||
@pyqtProperty(bool, notify=definitionChangesConflictChanged)
|
||||
def definitionChangesConflict(self):
|
||||
return self._has_definition_changes_conflict
|
||||
|
||||
@pyqtProperty(bool, notify=materialConflictChanged)
|
||||
def materialConflict(self):
|
||||
return self._has_material_conflict
|
||||
@ -214,11 +221,18 @@ class WorkspaceDialog(QObject):
|
||||
self._has_quality_changes_conflict = quality_changes_conflict
|
||||
self.qualityChangesConflictChanged.emit()
|
||||
|
||||
def setDefinitionChangesConflict(self, definition_changes_conflict):
|
||||
if self._has_definition_changes_conflict != definition_changes_conflict:
|
||||
self._has_definition_changes_conflict = definition_changes_conflict
|
||||
self.definitionChangesConflictChanged.emit()
|
||||
|
||||
def getResult(self):
|
||||
if "machine" in self._result and not self._has_machine_conflict:
|
||||
self._result["machine"] = None
|
||||
if "quality_changes" in self._result and not self._has_quality_changes_conflict:
|
||||
self._result["quality_changes"] = None
|
||||
if "definition_changes" in self._result and not self._has_definition_changes_conflict:
|
||||
self._result["definition_changes"] = None
|
||||
if "material" in self._result and not self._has_material_conflict:
|
||||
self._result["material"] = None
|
||||
return self._result
|
||||
@ -240,6 +254,7 @@ class WorkspaceDialog(QObject):
|
||||
# Reset the result
|
||||
self._result = {"machine": self._default_strategy,
|
||||
"quality_changes": self._default_strategy,
|
||||
"definition_changes": self._default_strategy,
|
||||
"material": self._default_strategy}
|
||||
self._visible = True
|
||||
self.showDialogSignal.emit()
|
||||
|
@ -12,15 +12,12 @@ UM.Dialog
|
||||
{
|
||||
title: catalog.i18nc("@title:window", "Open Project")
|
||||
|
||||
width: 550 * Screen.devicePixelRatio
|
||||
minimumWidth: 550 * Screen.devicePixelRatio
|
||||
maximumWidth: minimumWidth
|
||||
width: 500
|
||||
height: 400
|
||||
|
||||
property int comboboxHeight: 15
|
||||
property int spacerHeight: 10
|
||||
|
||||
height: 400 * Screen.devicePixelRatio
|
||||
minimumHeight: 400 * Screen.devicePixelRatio
|
||||
maximumHeight: minimumHeight
|
||||
property int comboboxHeight: 15 * Screen.devicePixelRatio
|
||||
property int spacerHeight: 10 * Screen.devicePixelRatio
|
||||
onClosing: manager.notifyClosed()
|
||||
onVisibleChanged:
|
||||
{
|
||||
@ -34,7 +31,7 @@ UM.Dialog
|
||||
Item
|
||||
{
|
||||
anchors.fill: parent
|
||||
anchors.margins: 20 * Screen.devicePixelRatio
|
||||
anchors.margins: 20
|
||||
|
||||
UM.I18nCatalog
|
||||
{
|
||||
@ -376,7 +373,6 @@ UM.Dialog
|
||||
enabled: true
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: ok_button.left
|
||||
anchors.bottomMargin: - 0.5 * height
|
||||
anchors.rightMargin:2
|
||||
}
|
||||
Button
|
||||
@ -384,7 +380,6 @@ UM.Dialog
|
||||
id: ok_button
|
||||
text: catalog.i18nc("@action:button","Open");
|
||||
onClicked: { manager.closeBackend(); manager.onOkButtonClicked() }
|
||||
anchors.bottomMargin: - 0.5 * height
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
}
|
||||
|
@ -16,21 +16,13 @@ from UM.Platform import Platform
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData() -> Dict:
|
||||
# Workarround for osx not supporting double file extensions correclty.
|
||||
# Workarround for osx not supporting double file extensions correctly.
|
||||
if Platform.isOSX():
|
||||
workspace_extension = "3mf"
|
||||
else:
|
||||
workspace_extension = "curaproject.3mf"
|
||||
|
||||
metaData = {
|
||||
"plugin": {
|
||||
"name": catalog.i18nc("@label", "3MF Reader"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Provides support for reading 3MF files."),
|
||||
"api": 3
|
||||
}
|
||||
}
|
||||
metaData = {}
|
||||
if "3MFReader.ThreeMFReader" in sys.modules:
|
||||
metaData["mesh_reader"] = [
|
||||
{
|
||||
|
8
plugins/3MFReader/plugin.json
Normal file
8
plugins/3MFReader/plugin.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "3MF Reader",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides support for reading 3MF files.",
|
||||
"api": 4,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
@ -7,6 +7,7 @@ from cura.Settings.ExtruderManager import ExtruderManager
|
||||
import zipfile
|
||||
from io import StringIO
|
||||
import copy
|
||||
import configparser
|
||||
|
||||
|
||||
class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||
@ -48,6 +49,16 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||
Preferences.getInstance().writeToFile(preferences_string)
|
||||
archive.writestr(preferences_file, preferences_string.getvalue())
|
||||
|
||||
# Save Cura version
|
||||
version_file = zipfile.ZipInfo("Cura/version.ini")
|
||||
version_config_parser = configparser.ConfigParser()
|
||||
version_config_parser.add_section("versions")
|
||||
version_config_parser.set("versions", "cura_version", Application.getStaticVersion())
|
||||
|
||||
version_file_string = StringIO()
|
||||
version_config_parser.write(version_file_string)
|
||||
archive.writestr(version_file, version_file_string.getvalue())
|
||||
|
||||
# Close the archive & reset states.
|
||||
archive.close()
|
||||
mesh_writer.setStoreArchive(False)
|
||||
|
@ -10,19 +10,18 @@ except ImportError:
|
||||
from . import ThreeMFWorkspaceWriter
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Platform import Platform
|
||||
|
||||
i18n_catalog = i18nCatalog("uranium")
|
||||
|
||||
def getMetaData():
|
||||
metaData = {
|
||||
"plugin": {
|
||||
"name": i18n_catalog.i18nc("@label", "3MF Writer"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"description": i18n_catalog.i18nc("@info:whatsthis", "Provides support for writing 3MF files."),
|
||||
"api": 3
|
||||
}
|
||||
}
|
||||
# Workarround for osx not supporting double file extensions correctly.
|
||||
if Platform.isOSX():
|
||||
workspace_extension = "3mf"
|
||||
else:
|
||||
workspace_extension = "curaproject.3mf"
|
||||
|
||||
metaData = {}
|
||||
|
||||
if "3MFWriter.ThreeMFWriter" in sys.modules:
|
||||
metaData["mesh_writer"] = {
|
||||
@ -35,7 +34,7 @@ def getMetaData():
|
||||
}
|
||||
metaData["workspace_writer"] = {
|
||||
"output": [{
|
||||
"extension": "curaproject.3mf",
|
||||
"extension": workspace_extension,
|
||||
"description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"),
|
||||
"mime_type": "application/x-curaproject+xml",
|
||||
"mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode
|
||||
|
8
plugins/3MFWriter/plugin.json
Normal file
8
plugins/3MFWriter/plugin.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "3MF Writer",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides support for writing 3MF files.",
|
||||
"api": 4,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
@ -7,15 +7,7 @@ from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": catalog.i18nc("@label", "Auto Save"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Automatically saves Preferences, Machines and Profiles after changes."),
|
||||
"api": 3
|
||||
},
|
||||
}
|
||||
return {}
|
||||
|
||||
def register(app):
|
||||
return { "extension": AutoSave.AutoSave() }
|
||||
|
8
plugins/AutoSave/plugin.json
Normal file
8
plugins/AutoSave/plugin.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Auto Save",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Automatically saves Preferences, Machines and Profiles after changes.",
|
||||
"api": 4,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
@ -1,3 +1,75 @@
|
||||
[2.6.1]
|
||||
*New profiles
|
||||
The Polypropylene material is added and supported with the Ultimaker 3. Support for CPE+ and PC with 0.8mm nozzles is added as well.
|
||||
|
||||
[2.6.0]
|
||||
*Cura versions
|
||||
Cura 2.6 has local version folders, which means the new version won’t overwrite the existing configuration and profiles from older versions, but can create a new folder instead. You can now safely check out new beta versions and, if necessary, start up an older version without the danger of losing your profiles.
|
||||
|
||||
*Better support adhesion
|
||||
We’ve added extra support settings to allow the creation of improved support profiles with better PVA/PLA adhesion. The Support Interface settings, such as speed and density, are now split up into Support Roof and Support Floor settings.
|
||||
|
||||
*Multi-extrusion support for custom FDM printers
|
||||
Custom third-party printers and Ultimaker modifications now have multi-extrusion support. Thanks to Aldo Hoeben for this feature.
|
||||
|
||||
*Model auto-arrange
|
||||
We’ve improved placing multiple models or multiplying the same ones, making it easier to arrange your build plate. If there’s not enough build plate space or the model is placed beyond the build plate, you can rectify this by selecting ‘Arrange all models’ in the context menu or by pressing Command+R (MacOS) or Ctrl+R (Windows and Linux). Cura 2.6 will then find a better solution for model positioning.
|
||||
|
||||
*Gradual infill
|
||||
You can now find the Gradual Infill button in Recommended mode. This setting makes the infill concentrated near the top of the model – so that we can save time and material for the lower parts of the model. This functionality is especially useful when printing with flexible materials.
|
||||
|
||||
*Support meshes
|
||||
It’s now possible to load an extra model that will be used as a support structure.
|
||||
|
||||
*Mold
|
||||
This is a bit of an experimental improvement. Users can use it to print a mold from a 3D model, which can be cast afterwards with the material that you would like your model to have.
|
||||
|
||||
*Towers for tiny overhangs
|
||||
We’ve added a new support option allowing users to achieve more reliable results by creating towers to support even the smallest overhangs.
|
||||
|
||||
*Cutting meshes
|
||||
Easily transform any model into a dual-extrusion print by applying a pattern for the second extruder. All areas of the original model, which also fall inside the pattern model, will be printed by the extruder selected for the pattern.
|
||||
|
||||
*Extruder per model selection via the context menu or extruder buttons
|
||||
You can now select the necessary extruder in the right-click menu or extruder buttons. This is a quicker and more user-friendly process. The material color for each extruder will also be represented in the extruder icons.
|
||||
|
||||
*Custom toggle
|
||||
We have made the interface a little bit cleaner and more user-friendly for switching from Recommended to Custom mode.
|
||||
|
||||
*Plugin installer
|
||||
It used to be fairly tricky to install new plugins. We have now added a button to select and install new plugins with ease – you will find it in Preferences.
|
||||
|
||||
*Project-based menu
|
||||
It’s a lot simpler to save and open files, and Cura will know if it’s a project, model, or gcode.
|
||||
|
||||
*Theme picker
|
||||
If you have a custom theme, you can now apply it more easily in the preferences screen.
|
||||
|
||||
*Time estimates per feature
|
||||
You can hover over the print time estimate in the lower right corner to see how the printing time is divided over the printing features (walls, infill, etc.). Thanks to 14bitVoid for this feature.
|
||||
|
||||
*Invert the direction of camera zoom
|
||||
We’ve added an option to invert mouse direction for a better user experience.
|
||||
|
||||
*Olsson block upgrade
|
||||
Ultimaker 2 users can now specify if they have the Olsson block installed on their machine. Thanks to Aldo Hoeben for this feature.
|
||||
|
||||
*OctoPrint plugin
|
||||
Cura 2.6 allows users to send prints to OctoPrint. Thanks to Aldo Hoeben for this feature.
|
||||
|
||||
*Bug fixes
|
||||
- Post Processing plugin
|
||||
- Font rendering
|
||||
- Progress bar
|
||||
- Support Bottom Distance issues
|
||||
|
||||
*3rd party printers
|
||||
- MAKEIT
|
||||
- Alya
|
||||
- Peopoly Moai
|
||||
- Rigid3D Zero
|
||||
- 3D maker
|
||||
|
||||
[2.5.0]
|
||||
*Improved speed
|
||||
We’ve made changing printers, profiles, materials, and print cores even faster. 3MF processing is also much faster now. Opening a 3MF file now takes one tenth of the time.
|
||||
@ -79,7 +151,7 @@ The initial and final printing temperatures reduce the amount of oozing during P
|
||||
Initial and final printing temperature settings have been tuned for higher quality results. For all materials the initial print temperature is 5 degrees above the default value.
|
||||
|
||||
*Printing temperature of the materials
|
||||
The printing temperature of the materials in the material profiles is now the same as the printing temperature for the Normal Quality profile.
|
||||
The printing temperature of the materials in the material profiles is now the same as the printing temperature for the Fine profile.
|
||||
|
||||
*Improved PLA-PVA layer adhesion
|
||||
The PVA jerk and acceleration have been optimized to improve the layer adhesion between PVA and PLA.
|
||||
|
@ -7,15 +7,7 @@ from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": catalog.i18nc("@label", "Changelog"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Shows changes since latest checked version."),
|
||||
"api": 3
|
||||
}
|
||||
}
|
||||
return {}
|
||||
|
||||
def register(app):
|
||||
return {"extension": ChangeLog.ChangeLog()}
|
||||
|
8
plugins/ChangeLogPlugin/plugin.json
Normal file
8
plugins/ChangeLogPlugin/plugin.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Changelog",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Shows changes since latest checked version.",
|
||||
"api": 4,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
@ -90,9 +90,21 @@ message GCodeLayer {
|
||||
}
|
||||
|
||||
|
||||
message PrintTimeMaterialEstimates { // The print time for the whole print and material estimates for the extruder
|
||||
float time = 1; // Total time estimate
|
||||
repeated MaterialEstimates materialEstimates = 2; // materialEstimates data
|
||||
message PrintTimeMaterialEstimates { // The print time for each feature and material estimates for the extruder
|
||||
// Time estimate in each feature
|
||||
float time_none = 1;
|
||||
float time_inset_0 = 2;
|
||||
float time_inset_x = 3;
|
||||
float time_skin = 4;
|
||||
float time_support = 5;
|
||||
float time_skirt = 6;
|
||||
float time_infill = 7;
|
||||
float time_support_infill = 8;
|
||||
float time_travel = 9;
|
||||
float time_retract = 10;
|
||||
float time_support_interface = 11;
|
||||
|
||||
repeated MaterialEstimates materialEstimates = 12; // materialEstimates data
|
||||
}
|
||||
|
||||
message MaterialEstimates {
|
||||
@ -121,4 +133,4 @@ message GCodePrefix {
|
||||
}
|
||||
|
||||
message SlicingFinished {
|
||||
}
|
||||
}
|
||||
|
@ -187,7 +187,19 @@ class CuraEngineBackend(QObject, Backend):
|
||||
Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.")
|
||||
return
|
||||
|
||||
self.printDurationMessage.emit(0, [0])
|
||||
self.printDurationMessage.emit({
|
||||
"none": 0,
|
||||
"inset_0": 0,
|
||||
"inset_x": 0,
|
||||
"skin": 0,
|
||||
"support": 0,
|
||||
"skirt": 0,
|
||||
"infill": 0,
|
||||
"support_infill": 0,
|
||||
"travel": 0,
|
||||
"retract": 0,
|
||||
"support_interface": 0
|
||||
}, [0])
|
||||
|
||||
self._stored_layer_data = []
|
||||
self._stored_optimized_layer_data = []
|
||||
@ -273,9 +285,15 @@ class CuraEngineBackend(QObject, Backend):
|
||||
if not extruders:
|
||||
error_keys = self._global_container_stack.getErrorKeys()
|
||||
error_labels = set()
|
||||
definition_container = self._global_container_stack.getBottom()
|
||||
for key in error_keys:
|
||||
error_labels.add(definition_container.findDefinitions(key = key)[0].label)
|
||||
for stack in [self._global_container_stack] + extruders: #Search all container stacks for the definition of this setting. Some are only in an extruder stack.
|
||||
definitions = stack.getBottom().findDefinitions(key = key)
|
||||
if definitions:
|
||||
break #Found it! No need to continue search.
|
||||
else: #No stack has a definition for this setting.
|
||||
Logger.log("w", "When checking settings for errors, unable to find definition for key: {key}".format(key = key))
|
||||
continue
|
||||
error_labels.add(definitions[0].label)
|
||||
|
||||
error_labels = ", ".join(error_labels)
|
||||
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current settings. The following settings have errors: {0}".format(error_labels)))
|
||||
@ -475,13 +493,26 @@ class CuraEngineBackend(QObject, Backend):
|
||||
|
||||
## Called when a print time message is received from the engine.
|
||||
#
|
||||
# \param message The protobuff message containing the print time and
|
||||
# \param message The protobuf message containing the print time per feature and
|
||||
# material amount per extruder
|
||||
def _onPrintTimeMaterialEstimates(self, message):
|
||||
material_amounts = []
|
||||
for index in range(message.repeatedMessageCount("materialEstimates")):
|
||||
material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount)
|
||||
self.printDurationMessage.emit(message.time, material_amounts)
|
||||
feature_times = {
|
||||
"none": message.time_none,
|
||||
"inset_0": message.time_inset_0,
|
||||
"inset_x": message.time_inset_x,
|
||||
"skin": message.time_skin,
|
||||
"support": message.time_support,
|
||||
"skirt": message.time_skirt,
|
||||
"infill": message.time_infill,
|
||||
"support_infill": message.time_support_infill,
|
||||
"travel": message.time_travel,
|
||||
"retract": message.time_retract,
|
||||
"support_interface": message.time_support_interface
|
||||
}
|
||||
self.printDurationMessage.emit(feature_times, material_amounts)
|
||||
|
||||
## Creates a new socket connection.
|
||||
def _createSocket(self):
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
#Copyright (c) 2017 Ultimaker B.V.
|
||||
#Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
import gc
|
||||
|
||||
@ -31,6 +31,9 @@ catalog = i18nCatalog("cura")
|
||||
#
|
||||
# \param color_code html color code, i.e. "#FF0000" -> red
|
||||
def colorCodeToRGBA(color_code):
|
||||
if color_code is None:
|
||||
Logger.log("w", "Unable to convert color code, returning default")
|
||||
return [0, 0, 0, 1]
|
||||
return [
|
||||
int(color_code[1:3], 16) / 255,
|
||||
int(color_code[3:5], 16) / 255,
|
||||
@ -170,19 +173,14 @@ class ProcessSlicedLayersJob(Job):
|
||||
if extruders:
|
||||
material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32)
|
||||
for extruder in extruders:
|
||||
material = extruder.findContainer({"type": "material"})
|
||||
position = int(extruder.getMetaDataEntry("position", default="0")) # Get the position
|
||||
color_code = material.getMetaDataEntry("color_code")
|
||||
color_code = extruder.material.getMetaDataEntry("color_code", default="#e0e000")
|
||||
color = colorCodeToRGBA(color_code)
|
||||
material_color_map[position, :] = color
|
||||
else:
|
||||
# Single extruder via global stack.
|
||||
material_color_map = numpy.zeros((1, 4), dtype=numpy.float32)
|
||||
material = global_container_stack.findContainer({"type": "material"})
|
||||
color_code = "#e0e000"
|
||||
if material:
|
||||
if material.getMetaDataEntry("color_code") is not None:
|
||||
color_code = material.getMetaDataEntry("color_code")
|
||||
color_code = global_container_stack.material.getMetaDataEntry("color_code", default="#e0e000")
|
||||
color = colorCodeToRGBA(color_code)
|
||||
material_color_map[0, :] = color
|
||||
|
||||
|
@ -44,6 +44,14 @@ class GcodeStartEndFormatter(Formatter):
|
||||
|
||||
## Job class that builds up the message of scene data to send to CuraEngine.
|
||||
class StartSliceJob(Job):
|
||||
## Meshes that are sent to the engine regardless of being outside of the
|
||||
# build volume.
|
||||
#
|
||||
# If these settings are True for any mesh, the build volume is ignored.
|
||||
# Note that Support Mesh is not in here because it actually generates
|
||||
# g-code in the volume of the mesh.
|
||||
_not_printed_mesh_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh"}
|
||||
|
||||
def __init__(self, slice_message):
|
||||
super().__init__()
|
||||
|
||||
@ -132,7 +140,8 @@ class StartSliceJob(Job):
|
||||
temp_list = []
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
|
||||
if not getattr(node, "_outside_buildarea", False):
|
||||
if not getattr(node, "_outside_buildarea", False)\
|
||||
or (node.callDecoration("getStack") and any(node.callDecoration("getStack").getProperty(setting, "value") for setting in self._not_printed_mesh_settings)):
|
||||
temp_list.append(node)
|
||||
Job.yieldThread()
|
||||
|
||||
@ -149,8 +158,13 @@ class StartSliceJob(Job):
|
||||
self._buildGlobalSettingsMessage(stack)
|
||||
self._buildGlobalInheritsStackMessage(stack)
|
||||
|
||||
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
|
||||
self._buildExtruderMessage(extruder_stack)
|
||||
# Only add extruder stacks if there are multiple extruders
|
||||
# Single extruder machines only use the global stack to store setting values
|
||||
if stack.getProperty("machine_extruder_count", "value") > 1:
|
||||
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
|
||||
self._buildExtruderMessage(extruder_stack)
|
||||
else:
|
||||
self._buildExtruderMessageFromGlobalStack(stack)
|
||||
|
||||
for group in object_groups:
|
||||
group_message = self._slice_message.addRepeatedMessage("object_lists")
|
||||
@ -212,7 +226,7 @@ class StartSliceJob(Job):
|
||||
|
||||
for key in stack.getAllKeys():
|
||||
# Do not send settings that are not settable_per_extruder.
|
||||
if stack.getProperty(key, "settable_per_extruder") == False:
|
||||
if not stack.getProperty(key, "settable_per_extruder"):
|
||||
continue
|
||||
setting = message.getMessage("settings").addRepeatedMessage("settings")
|
||||
setting.name = key
|
||||
@ -223,6 +237,19 @@ class StartSliceJob(Job):
|
||||
setting.value = str(stack.getProperty(key, "value")).encode("utf-8")
|
||||
Job.yieldThread()
|
||||
|
||||
## Create extruder message from global stack
|
||||
def _buildExtruderMessageFromGlobalStack(self, stack):
|
||||
message = self._slice_message.addRepeatedMessage("extruders")
|
||||
|
||||
for key in stack.getAllKeys():
|
||||
# Do not send settings that are not settable_per_extruder.
|
||||
if not stack.getProperty(key, "settable_per_extruder"):
|
||||
continue
|
||||
setting = message.getMessage("settings").addRepeatedMessage("settings")
|
||||
setting.name = key
|
||||
setting.value = str(stack.getProperty(key, "value")).encode("utf-8")
|
||||
Job.yieldThread()
|
||||
|
||||
## Sends all global settings to the engine.
|
||||
#
|
||||
# The settings are taken from the global stack. This does not include any
|
||||
|
@ -8,14 +8,7 @@ from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": catalog.i18nc("@label", "CuraEngine Backend"),
|
||||
"author": "Ultimaker",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Provides the link to the CuraEngine slicing backend."),
|
||||
"api": 3
|
||||
}
|
||||
}
|
||||
return {}
|
||||
|
||||
def register(app):
|
||||
return { "backend": CuraEngineBackend.CuraEngineBackend() }
|
||||
|
8
plugins/CuraEngineBackend/plugin.json
Normal file
8
plugins/CuraEngineBackend/plugin.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "CuraEngine Backend",
|
||||
"author": "Ultimaker B.V.",
|
||||
"description": "Provides the link to the CuraEngine slicing backend.",
|
||||
"api": 4,
|
||||
"version": "1.0.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
@ -8,13 +8,6 @@ catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": catalog.i18nc("@label", "Cura Profile Reader"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Provides support for importing Cura profiles."),
|
||||
"api": 3
|
||||
},
|
||||
"profile_reader": [
|
||||
{
|
||||
"extension": "curaprofile",
|
||||
|
8
plugins/CuraProfileReader/plugin.json
Normal file
8
plugins/CuraProfileReader/plugin.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Cura Profile Reader",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides support for importing Cura profiles.",
|
||||
"api": 4,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
@ -8,13 +8,6 @@ catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": catalog.i18nc("@label", "Cura Profile Writer"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Provides support for exporting Cura profiles."),
|
||||
"api": 3
|
||||
},
|
||||
"profile_writer": [
|
||||
{
|
||||
"extension": "curaprofile",
|
||||
|
8
plugins/CuraProfileWriter/plugin.json
Normal file
8
plugins/CuraProfileWriter/plugin.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Cura Profile Writer",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides support for exporting Cura profiles.",
|
||||
"api": 4,
|
||||
"i18n-catalog":"cura"
|
||||
}
|
@ -56,7 +56,7 @@ class GCodeProfileReader(ProfileReader):
|
||||
# TODO: Consider moving settings to the start?
|
||||
serialized = "" # Will be filled with the serialized profile.
|
||||
try:
|
||||
with open(file_name) as f:
|
||||
with open(file_name, "r") as f:
|
||||
for line in f:
|
||||
if line.startswith(prefix):
|
||||
# Remove the prefix and the newline from the line and add it to the rest.
|
||||
@ -66,9 +66,13 @@ class GCodeProfileReader(ProfileReader):
|
||||
return None
|
||||
|
||||
serialized = unescapeGcodeComment(serialized)
|
||||
Logger.log("i", "Serialized the following from %s: %s" %(file_name, repr(serialized)))
|
||||
|
||||
json_data = json.loads(serialized)
|
||||
# serialized data can be invalid JSON
|
||||
try:
|
||||
json_data = json.loads(serialized)
|
||||
except Exception as e:
|
||||
Logger.log("e", "Could not parse serialized JSON data from GCode %s, error: %s", file_name, e)
|
||||
return None
|
||||
|
||||
profiles = []
|
||||
global_profile = readQualityProfileFromString(json_data["global_quality"])
|
||||
|
@ -8,13 +8,6 @@ catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": catalog.i18nc("@label", "GCode Profile Reader"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Provides support for importing profiles from g-code files."),
|
||||
"api": 3
|
||||
},
|
||||
"profile_reader": [
|
||||
{
|
||||
"extension": "gcode",
|
||||
|
8
plugins/GCodeProfileReader/plugin.json
Normal file
8
plugins/GCodeProfileReader/plugin.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "GCode Profile Reader",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides support for importing profiles from g-code files.",
|
||||
"api": 4,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
@ -8,13 +8,6 @@ i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": i18n_catalog.i18nc("@label", "G-code Reader"),
|
||||
"author": "Victor Larchenko",
|
||||
"version": "1.0",
|
||||
"description": i18n_catalog.i18nc("@info:whatsthis", "Allows loading and displaying G-code files."),
|
||||
"api": 3
|
||||
},
|
||||
"mesh_reader": [
|
||||
{
|
||||
"extension": "gcode",
|
||||
|
8
plugins/GCodeReader/plugin.json
Normal file
8
plugins/GCodeReader/plugin.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "G-code Reader",
|
||||
"author": "Victor Larchenko",
|
||||
"version": "1.0.0",
|
||||
"description": "Allows loading and displaying G-code files.",
|
||||
"api": 4,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.Mesh.MeshWriter import MeshWriter
|
||||
@ -85,6 +85,7 @@ class GCodeWriter(MeshWriter):
|
||||
|
||||
for key in instance_container1.getAllKeys():
|
||||
flat_container.setProperty(key, "value", instance_container1.getProperty(key, "value"))
|
||||
|
||||
return flat_container
|
||||
|
||||
|
||||
@ -100,26 +101,32 @@ class GCodeWriter(MeshWriter):
|
||||
prefix = ";SETTING_" + str(GCodeWriter.version) + " " # The prefix to put before each line.
|
||||
prefix_length = len(prefix)
|
||||
|
||||
container_with_profile = stack.findContainer({"type": "quality_changes"})
|
||||
container_with_profile = stack.qualityChanges
|
||||
if not container_with_profile:
|
||||
Logger.log("e", "No valid quality profile found, not writing settings to GCode!")
|
||||
return ""
|
||||
|
||||
flat_global_container = self._createFlattenedContainerInstance(stack.getTop(), container_with_profile)
|
||||
# If the quality changes is not set, we need to set type manually
|
||||
if flat_global_container.getMetaDataEntry("type", None) is None:
|
||||
flat_global_container.addMetaDataEntry("type", "quality_changes")
|
||||
|
||||
# Ensure that quality_type is set. (Can happen if we have empty quality changes).
|
||||
if flat_global_container.getMetaDataEntry("quality_type", None) is None:
|
||||
flat_global_container.addMetaDataEntry("quality_type", stack.findContainer({"type": "quality"}).getMetaDataEntry("quality_type", "normal"))
|
||||
flat_global_container.addMetaDataEntry("quality_type", stack.quality.getMetaDataEntry("quality_type", "normal"))
|
||||
|
||||
serialized = flat_global_container.serialize()
|
||||
data = {"global_quality": serialized}
|
||||
|
||||
for extruder in sorted(ExtruderManager.getInstance().getMachineExtruders(stack.getId()), key = lambda k: k.getMetaDataEntry("position")):
|
||||
extruder_quality = extruder.findContainer({"type": "quality_changes"})
|
||||
extruder_quality = extruder.qualityChanges
|
||||
if not extruder_quality:
|
||||
Logger.log("w", "No extruder quality profile found, not writing quality for extruder %s to file!", extruder.getId())
|
||||
continue
|
||||
flat_extruder_quality = self._createFlattenedContainerInstance(extruder.getTop(), extruder_quality)
|
||||
# If the quality changes is not set, we need to set type manually
|
||||
if flat_extruder_quality.getMetaDataEntry("type", None) is None:
|
||||
flat_extruder_quality.addMetaDataEntry("type", "quality_changes")
|
||||
|
||||
# Ensure that extruder is set. (Can happen if we have empty quality changes).
|
||||
if flat_extruder_quality.getMetaDataEntry("extruder", None) is None:
|
||||
@ -127,7 +134,7 @@ class GCodeWriter(MeshWriter):
|
||||
|
||||
# Ensure that quality_type is set. (Can happen if we have empty quality changes).
|
||||
if flat_extruder_quality.getMetaDataEntry("quality_type", None) is None:
|
||||
flat_extruder_quality.addMetaDataEntry("quality_type", extruder.findContainer({"type": "quality"}).getMetaDataEntry("quality_type", "normal"))
|
||||
flat_extruder_quality.addMetaDataEntry("quality_type", extruder.quality.getMetaDataEntry("quality_type", "normal"))
|
||||
extruder_serialized = flat_extruder_quality.serialize()
|
||||
data.setdefault("extruder_quality", []).append(extruder_serialized)
|
||||
|
||||
|
@ -8,13 +8,7 @@ catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": catalog.i18nc("@label", "GCode Writer"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Writes GCode to a file."),
|
||||
"api": 3
|
||||
},
|
||||
|
||||
|
||||
"mesh_writer": {
|
||||
"output": [{
|
||||
|
8
plugins/GCodeWriter/plugin.json
Normal file
8
plugins/GCodeWriter/plugin.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "GCode Writer",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Writes GCode to a file.",
|
||||
"api": 4,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
@ -8,13 +8,6 @@ i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": i18n_catalog.i18nc("@label", "Image Reader"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"description": i18n_catalog.i18nc("@info:whatsthis", "Enables ability to generate printable geometry from 2D image files."),
|
||||
"api": 3
|
||||
},
|
||||
"mesh_reader": [
|
||||
{
|
||||
"extension": "jpg",
|
||||
|
8
plugins/ImageReader/plugin.json
Normal file
8
plugins/ImageReader/plugin.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Image Reader",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Enables ability to generate printable geometry from 2D image files.",
|
||||
"api": 4,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
@ -82,12 +82,12 @@ class LayerPass(RenderPass):
|
||||
start = 0
|
||||
end = 0
|
||||
element_counts = layer_data.getElementCounts()
|
||||
for layer, counts in element_counts.items():
|
||||
for layer in sorted(element_counts.keys()):
|
||||
if layer > self._layer_view._current_layer_num:
|
||||
break
|
||||
if self._layer_view._minimum_layer_num > layer:
|
||||
start += counts
|
||||
end += counts
|
||||
start += element_counts[layer]
|
||||
end += element_counts[layer]
|
||||
|
||||
# This uses glDrawRangeElements internally to only draw a certain range of lines.
|
||||
batch = RenderBatch(self._layer_shader, type = RenderBatch.RenderType.Solid, mode = RenderBatch.RenderMode.Lines, range = (start, end))
|
||||
|
@ -11,6 +11,7 @@ import Cura 1.0 as Cura
|
||||
|
||||
Item
|
||||
{
|
||||
id: base
|
||||
width: {
|
||||
if (UM.LayerView.compatibilityMode) {
|
||||
return UM.Theme.getSize("layerview_menu_size_compatibility").width;
|
||||
@ -25,8 +26,12 @@ Item
|
||||
return UM.Theme.getSize("layerview_menu_size").height + UM.LayerView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height)
|
||||
}
|
||||
}
|
||||
property var buttonTarget: {
|
||||
var force_binding = parent.y; // ensure this gets reevaluated when the panel moves
|
||||
return base.mapFromItem(parent.parent, parent.buttonTarget.x, parent.buttonTarget.y);
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
UM.PointingRectangle {
|
||||
id: layerViewMenu
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
@ -34,6 +39,11 @@ Item
|
||||
height: parent.height
|
||||
z: slider.z - 1
|
||||
color: UM.Theme.getColor("tool_panel_background")
|
||||
borderWidth: UM.Theme.getSize("default_lining").width
|
||||
borderColor: UM.Theme.getColor("lining")
|
||||
|
||||
target: parent.buttonTarget
|
||||
arrowSize: UM.Theme.getSize("default_arrow").width
|
||||
|
||||
ColumnLayout {
|
||||
id: view_settings
|
||||
@ -522,26 +532,19 @@ Item
|
||||
target: Qt.point(0, slider.activeHandle.y + slider.activeHandle.height / 2)
|
||||
arrowSize: UM.Theme.getSize("default_arrow").width
|
||||
|
||||
height: (Math.floor(UM.Theme.getSize("slider_handle").height + UM.Theme.getSize("default_margin").height) / 2) * 2 // Make sure height has an integer middle so drawing a pointy border is easier
|
||||
height: UM.Theme.getSize("slider_handle").height + UM.Theme.getSize("default_margin").height
|
||||
width: valueLabel.width + UM.Theme.getSize("default_margin").width
|
||||
Behavior on height { NumberAnimation { duration: 50; } }
|
||||
|
||||
color: UM.Theme.getColor("lining");
|
||||
color: UM.Theme.getColor("tool_panel_background")
|
||||
borderColor: UM.Theme.getColor("lining")
|
||||
borderWidth: UM.Theme.getSize("default_lining").width
|
||||
|
||||
visible: slider.layersVisible
|
||||
|
||||
UM.PointingRectangle
|
||||
MouseArea //Catch all mouse events (so scene doesnt handle them)
|
||||
{
|
||||
color: UM.Theme.getColor("tool_panel_background")
|
||||
target: Qt.point(0, height / 2 + UM.Theme.getSize("default_lining").width)
|
||||
arrowSize: UM.Theme.getSize("default_arrow").width
|
||||
anchors.fill: parent
|
||||
anchors.margins: UM.Theme.getSize("default_lining").width
|
||||
|
||||
MouseArea //Catch all mouse events (so scene doesnt handle them)
|
||||
{
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
|
||||
TextField
|
||||
|
@ -9,13 +9,6 @@ catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": catalog.i18nc("@label", "Layer View"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Provides the Layer view."),
|
||||
"api": 3
|
||||
},
|
||||
"view": {
|
||||
"name": catalog.i18nc("@item:inlistbox", "Layers"),
|
||||
"view_panel": "LayerView.qml",
|
||||
|
@ -130,9 +130,9 @@ geometry41core =
|
||||
// fixed size for movements
|
||||
size_x = 0.05;
|
||||
} else {
|
||||
size_x = v_line_dim[0].x / 2 + 0.01; // radius, and make it nicely overlapping
|
||||
size_x = v_line_dim[1].x / 2 + 0.01; // radius, and make it nicely overlapping
|
||||
}
|
||||
size_y = v_line_dim[0].y / 2 + 0.01;
|
||||
size_y = v_line_dim[1].y / 2 + 0.01;
|
||||
|
||||
g_vertex_delta = gl_in[1].gl_Position - gl_in[0].gl_Position;
|
||||
g_vertex_normal_horz_head = normalize(vec3(-g_vertex_delta.x, -g_vertex_delta.y, -g_vertex_delta.z));
|
||||
|
8
plugins/LayerView/plugin.json
Normal file
8
plugins/LayerView/plugin.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Layer View",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides the Layer view.",
|
||||
"api": 4,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
@ -8,13 +8,6 @@ catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": catalog.i18nc("@label", "Legacy Cura Profile Reader"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Provides support for importing profiles from legacy Cura versions."),
|
||||
"api": 3
|
||||
},
|
||||
"profile_reader": [
|
||||
{
|
||||
"extension": "ini",
|
||||
|
8
plugins/LegacyProfileReader/plugin.json
Normal file
8
plugins/LegacyProfileReader/plugin.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Legacy Cura Profile Reader",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides support for importing profiles from legacy Cura versions.",
|
||||
"api": 4,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
@ -14,7 +14,7 @@ from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Logger import Logger
|
||||
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
|
||||
import UM.i18n
|
||||
@ -69,8 +69,9 @@ class MachineSettingsAction(MachineAction):
|
||||
self._container_index = container_index
|
||||
self.containerIndexChanged.emit()
|
||||
|
||||
# Disable autoslicing while the machineaction is showing
|
||||
self._backend.disableTimer()
|
||||
# Disable auto-slicing while the MachineAction is showing
|
||||
if self._backend: # This sometimes triggers before backend is loaded.
|
||||
self._backend.disableTimer()
|
||||
|
||||
@pyqtSlot()
|
||||
def onFinishAction(self):
|
||||
@ -99,6 +100,7 @@ class MachineSettingsAction(MachineAction):
|
||||
definition = container_stack.getBottom()
|
||||
definition_changes_container.setDefinition(definition)
|
||||
definition_changes_container.addMetaDataEntry("type", "definition_changes")
|
||||
definition_changes_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion)
|
||||
|
||||
self._container_registry.addContainer(definition_changes_container)
|
||||
container_stack.definitionChanges = definition_changes_container
|
||||
@ -153,7 +155,7 @@ class MachineSettingsAction(MachineAction):
|
||||
if machine_manager.hasMaterials:
|
||||
extruder_material_id = machine_manager.allActiveMaterialIds[extruder_manager.extruderIds["0"]]
|
||||
if machine_manager.hasVariants:
|
||||
extruder_variant_id = machine_manager.activeVariantIds[0]
|
||||
extruder_variant_id = machine_manager.allActiveVariantIds[extruder_manager.extruderIds["0"]]
|
||||
|
||||
# Copy any settable_per_extruder setting value from the extruders to the global stack
|
||||
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||
@ -251,7 +253,7 @@ class MachineSettingsAction(MachineAction):
|
||||
if definition.getProperty("machine_gcode_flavor", "value") == "UltiGCode" and not definition.getMetaDataEntry("has_materials", False):
|
||||
has_materials = self._global_container_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode"
|
||||
|
||||
material_container = self._global_container_stack.findContainer({"type": "material"})
|
||||
material_container = self._global_container_stack.material
|
||||
material_index = self._global_container_stack.getContainerIndex(material_container)
|
||||
|
||||
if has_materials:
|
||||
@ -272,7 +274,6 @@ class MachineSettingsAction(MachineAction):
|
||||
if "has_materials" in self._global_container_stack.getMetaData():
|
||||
self._global_container_stack.removeMetaDataEntry("has_materials")
|
||||
|
||||
empty_material = self._container_registry.findInstanceContainers(id = "empty_material")[0]
|
||||
self._global_container_stack.replaceContainer(material_index, empty_material)
|
||||
self._global_container_stack.material = ContainerRegistry.getInstance().getEmptyInstanceContainer()
|
||||
|
||||
Application.getInstance().globalContainerStackChanged.emit()
|
||||
|
@ -16,20 +16,14 @@ Cura.MachineAction
|
||||
property var extrudersModel: Cura.ExtrudersModel{}
|
||||
property int extruderTabsCount: 0
|
||||
|
||||
Component.onCompleted:
|
||||
Connections
|
||||
{
|
||||
// Populate extruder tabs after a short delay, because otherwise the tabs that are added when
|
||||
// the dialog is created are stuck.
|
||||
extruderTabsCountDelay.start();
|
||||
}
|
||||
|
||||
Timer
|
||||
{
|
||||
id: extruderTabsCountDelay
|
||||
repeat: false
|
||||
interval: 1
|
||||
|
||||
onTriggered: base.extruderTabsCount = (machineExtruderCountProvider.properties.value > 1) ? parseInt(machineExtruderCountProvider.properties.value) : 0
|
||||
target: base.extrudersModel
|
||||
onModelChanged:
|
||||
{
|
||||
var extruderCount = base.extrudersModel.rowCount();
|
||||
base.extruderTabsCount = extruderCount > 1 ? extruderCount : 0;
|
||||
}
|
||||
}
|
||||
|
||||
Connections
|
||||
@ -347,7 +341,6 @@ Cura.MachineAction
|
||||
sourceComponent: numericTextFieldWithUnit
|
||||
property var propertyProvider: gantryHeightProvider
|
||||
property string unit: catalog.i18nc("@label", "mm")
|
||||
property bool forceUpdateOnChange: false
|
||||
}
|
||||
|
||||
Item { width: UM.Theme.getSize("default_margin").width; height: UM.Theme.getSize("default_margin").height }
|
||||
@ -375,14 +368,9 @@ Cura.MachineAction
|
||||
}
|
||||
}
|
||||
currentIndex: machineExtruderCountProvider.properties.value - 1
|
||||
Component.onCompleted:
|
||||
{
|
||||
manager.setMachineExtruderCount(1);
|
||||
}
|
||||
onActivated:
|
||||
{
|
||||
manager.setMachineExtruderCount(index + 1);
|
||||
base.extruderTabsCount = (index > 0) ? index + 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -396,7 +384,6 @@ Cura.MachineAction
|
||||
sourceComponent: numericTextFieldWithUnit
|
||||
property var propertyProvider: materialDiameterProvider
|
||||
property string unit: catalog.i18nc("@label", "mm")
|
||||
property bool forceUpdateOnChange: false
|
||||
}
|
||||
Label
|
||||
{
|
||||
@ -410,7 +397,6 @@ Cura.MachineAction
|
||||
sourceComponent: numericTextFieldWithUnit
|
||||
property var propertyProvider: machineNozzleSizeProvider
|
||||
property string unit: catalog.i18nc("@label", "mm")
|
||||
property bool forceUpdateOnChange: false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -561,7 +547,6 @@ Cura.MachineAction
|
||||
sourceComponent: numericTextFieldWithUnit
|
||||
property var propertyProvider: extruderNozzleSizeProvider
|
||||
property string unit: catalog.i18nc("@label", "mm")
|
||||
property bool forceUpdateOnChange: false
|
||||
}
|
||||
|
||||
Label
|
||||
@ -575,6 +560,7 @@ Cura.MachineAction
|
||||
property var propertyProvider: extruderOffsetXProvider
|
||||
property string unit: catalog.i18nc("@label", "mm")
|
||||
property bool forceUpdateOnChange: true
|
||||
property bool allowNegative: true
|
||||
}
|
||||
Label
|
||||
{
|
||||
@ -587,6 +573,7 @@ Cura.MachineAction
|
||||
property var propertyProvider: extruderOffsetYProvider
|
||||
property string unit: catalog.i18nc("@label", "mm")
|
||||
property bool forceUpdateOnChange: true
|
||||
property bool allowNegative: true
|
||||
}
|
||||
}
|
||||
|
||||
@ -666,17 +653,21 @@ Cura.MachineAction
|
||||
Item {
|
||||
height: textField.height
|
||||
width: textField.width
|
||||
|
||||
property bool _allowNegative: (typeof(allowNegative) === 'undefined') ? false : allowNegative
|
||||
property bool _forceUpdateOnChange: (typeof(forceUpdateOnChange) === 'undefined') ? false: forceUpdateOnChange
|
||||
|
||||
TextField
|
||||
{
|
||||
id: textField
|
||||
text: (propertyProvider.properties.value) ? propertyProvider.properties.value : ""
|
||||
validator: RegExpValidator { regExp: /[0-9\.]{0,6}/ }
|
||||
validator: RegExpValidator { regExp: _allowNegative ? /-?[0-9\.]{0,6}/ : /[0-9\.]{0,6}/ }
|
||||
onEditingFinished:
|
||||
{
|
||||
if (propertyProvider && text != propertyProvider.properties.value)
|
||||
{
|
||||
propertyProvider.setPropertyValue("value", text);
|
||||
if(forceUpdateOnChange)
|
||||
if(_forceUpdateOnChange)
|
||||
{
|
||||
var extruderIndex = ExtruderManager.activeExtruderIndex;
|
||||
manager.forceUpdate();
|
||||
|
@ -7,15 +7,7 @@ from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": catalog.i18nc("@label", "Machine Settings action"),
|
||||
"author": "fieldOfView",
|
||||
"version": "1.0",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Provides a way to change machine settings (such as build volume, nozzle size, etc)"),
|
||||
"api": 3
|
||||
}
|
||||
}
|
||||
return {}
|
||||
|
||||
def register(app):
|
||||
return { "machine_action": MachineSettingsAction.MachineSettingsAction() }
|
||||
|
8
plugins/MachineSettingsAction/plugin.json
Normal file
8
plugins/MachineSettingsAction/plugin.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Machine Settings action",
|
||||
"author": "fieldOfView",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides a way to change machine settings (such as build volume, nozzle size, etc)",
|
||||
"api": 4,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
@ -63,17 +63,20 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
|
||||
stack_nr = -1
|
||||
stack = None
|
||||
# Check from what stack we should copy the raw property of the setting from.
|
||||
if definition.limit_to_extruder != "-1" and self._stack.getProperty("machine_extruder_count", "value") > 1:
|
||||
# A limit to extruder function was set and it's a multi extrusion machine. Check what stack we do need to use.
|
||||
stack_nr = str(int(round(float(self._stack.getProperty(item, "limit_to_extruder")))))
|
||||
if self._stack.getProperty("machine_extruder_count", "value") > 1:
|
||||
if definition.limit_to_extruder != "-1":
|
||||
# A limit to extruder function was set and it's a multi extrusion machine. Check what stack we do need to use.
|
||||
stack_nr = str(int(round(float(self._stack.getProperty(item, "limit_to_extruder")))))
|
||||
|
||||
# Check if the found stack_number is in the extruder list of extruders.
|
||||
if stack_nr not in ExtruderManager.getInstance().extruderIds and self._stack.getProperty("extruder_nr", "value") is not None:
|
||||
stack_nr = -1
|
||||
# Check if the found stack_number is in the extruder list of extruders.
|
||||
if stack_nr not in ExtruderManager.getInstance().extruderIds and self._stack.getProperty("extruder_nr", "value") is not None:
|
||||
stack_nr = -1
|
||||
|
||||
# Use the found stack number to get the right stack to copy the value from.
|
||||
if stack_nr in ExtruderManager.getInstance().extruderIds:
|
||||
stack = ContainerRegistry.getInstance().findContainerStacks(id = ExtruderManager.getInstance().extruderIds[stack_nr])[0]
|
||||
# Use the found stack number to get the right stack to copy the value from.
|
||||
if stack_nr in ExtruderManager.getInstance().extruderIds:
|
||||
stack = ContainerRegistry.getInstance().findContainerStacks(id = ExtruderManager.getInstance().extruderIds[stack_nr])[0]
|
||||
else:
|
||||
stack = self._stack
|
||||
|
||||
# Use the raw property to set the value (so the inheritance doesn't break)
|
||||
if stack is not None:
|
||||
|
@ -94,6 +94,8 @@ Item {
|
||||
return settingComboBox
|
||||
case "extruder":
|
||||
return settingExtruder
|
||||
case "optional_extruder":
|
||||
return settingOptionalExtruder
|
||||
case "bool":
|
||||
return settingCheckBox
|
||||
case "str":
|
||||
@ -141,14 +143,6 @@ Item {
|
||||
storeIndex: 0
|
||||
removeUnusedValue: false
|
||||
}
|
||||
|
||||
// If the extruder by which the object needs to be printed is changed, ensure that the
|
||||
// display is also notified of the fact.
|
||||
Connections
|
||||
{
|
||||
target: extruderSelector
|
||||
onActivated: provider.forcePropertiesChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -350,6 +344,13 @@ Item {
|
||||
Cura.SettingExtruder { }
|
||||
}
|
||||
|
||||
Component
|
||||
{
|
||||
id: settingOptionalExtruder
|
||||
|
||||
Cura.SettingOptionalExtruder { }
|
||||
}
|
||||
|
||||
Component
|
||||
{
|
||||
id: settingCheckBox;
|
||||
|
@ -10,13 +10,6 @@ i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": i18n_catalog.i18nc("@label", "Per Model Settings Tool"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"description": i18n_catalog.i18nc("@info:whatsthis", "Provides the Per Model Settings."),
|
||||
"api": 3
|
||||
},
|
||||
"tool": {
|
||||
"name": i18n_catalog.i18nc("@label", "Per Model Settings"),
|
||||
"description": i18n_catalog.i18nc("@info:tooltip", "Configure Per Model Settings"),
|
||||
|
8
plugins/PerObjectSettingsTool/plugin.json
Normal file
8
plugins/PerObjectSettingsTool/plugin.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Per Model Settings Tool",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides the Per Model Settings.",
|
||||
"api": 4,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
249
plugins/PluginBrowser/PluginBrowser.py
Normal file
249
plugins/PluginBrowser/PluginBrowser.py
Normal file
@ -0,0 +1,249 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# PluginBrowser is released under the terms of the AGPLv3 or higher.
|
||||
from UM.Extension import Extension
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.Application import Application
|
||||
from UM.Version import Version
|
||||
|
||||
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
||||
from PyQt5.QtCore import QUrl, QObject, Qt, pyqtProperty, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtQml import QQmlComponent, QQmlContext
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class PluginBrowser(QObject, Extension):
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
self.addMenuItem(i18n_catalog.i18n("Browse plugins"), self.browsePlugins)
|
||||
self._api_version = 1
|
||||
self._api_url = "http://software.ultimaker.com/cura/v%s/" % self._api_version
|
||||
|
||||
self._plugin_list_request = None
|
||||
self._download_plugin_request = None
|
||||
|
||||
self._download_plugin_reply = None
|
||||
|
||||
self._network_manager = None
|
||||
|
||||
self._plugins_metadata = []
|
||||
self._plugins_model = None
|
||||
|
||||
self._qml_component = None
|
||||
self._qml_context = None
|
||||
self._dialog = None
|
||||
self._download_progress = 0
|
||||
|
||||
self._is_downloading = False
|
||||
|
||||
self._request_header = [b"User-Agent", str.encode("%s - %s" % (Application.getInstance().getApplicationName(), Application.getInstance().getVersion()))]
|
||||
|
||||
# Installed plugins are really installed after reboot. In order to prevent the user from downloading the
|
||||
# same file over and over again, we keep track of the upgraded plugins.
|
||||
self._newly_installed_plugin_ids = []
|
||||
|
||||
|
||||
pluginsMetadataChanged = pyqtSignal()
|
||||
onDownloadProgressChanged = pyqtSignal()
|
||||
onIsDownloadingChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(bool, notify = onIsDownloadingChanged)
|
||||
def isDownloading(self):
|
||||
return self._is_downloading
|
||||
|
||||
def browsePlugins(self):
|
||||
self._createNetworkManager()
|
||||
self.requestPluginList()
|
||||
|
||||
if not self._dialog:
|
||||
self._createDialog()
|
||||
self._dialog.show()
|
||||
|
||||
@pyqtSlot()
|
||||
def requestPluginList(self):
|
||||
Logger.log("i", "Requesting plugin list")
|
||||
url = QUrl(self._api_url + "plugins")
|
||||
self._plugin_list_request = QNetworkRequest(url)
|
||||
self._plugin_list_request.setRawHeader(*self._request_header)
|
||||
self._network_manager.get(self._plugin_list_request)
|
||||
|
||||
def _createDialog(self):
|
||||
Logger.log("d", "PluginBrowser")
|
||||
|
||||
path = QUrl.fromLocalFile(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "PluginBrowser.qml"))
|
||||
self._qml_component = QQmlComponent(Application.getInstance()._engine, path)
|
||||
|
||||
# We need access to engine (although technically we can't)
|
||||
self._qml_context = QQmlContext(Application.getInstance()._engine.rootContext())
|
||||
self._qml_context.setContextProperty("manager", self)
|
||||
self._dialog = self._qml_component.create(self._qml_context)
|
||||
if self._dialog is None:
|
||||
Logger.log("e", "QQmlComponent status %s", self._qml_component.status())
|
||||
Logger.log("e", "QQmlComponent errorString %s", self._qml_component.errorString())
|
||||
|
||||
def setIsDownloading(self, is_downloading):
|
||||
if self._is_downloading != is_downloading:
|
||||
self._is_downloading = is_downloading
|
||||
self.onIsDownloadingChanged.emit()
|
||||
|
||||
def _onDownloadPluginProgress(self, bytes_sent, bytes_total):
|
||||
if bytes_total > 0:
|
||||
new_progress = bytes_sent / bytes_total * 100
|
||||
self.setDownloadProgress(new_progress)
|
||||
if new_progress == 100.0:
|
||||
self.setIsDownloading(False)
|
||||
self._download_plugin_reply.downloadProgress.disconnect(self._onDownloadPluginProgress)
|
||||
|
||||
# must not delete the temporary file on Windows
|
||||
self._temp_plugin_file = tempfile.NamedTemporaryFile(mode = "w+b", suffix = ".curaplugin", delete = False)
|
||||
location = self._temp_plugin_file.name
|
||||
|
||||
# write first and close, otherwise on Windows, it cannot read the file
|
||||
self._temp_plugin_file.write(self._download_plugin_reply.readAll())
|
||||
self._temp_plugin_file.close()
|
||||
|
||||
# open as read
|
||||
if not location.startswith("/"):
|
||||
location = "/" + location # Ensure that it starts with a /, as otherwise it doesn't work on windows.
|
||||
result = PluginRegistry.getInstance().installPlugin("file://" + location)
|
||||
|
||||
self._newly_installed_plugin_ids.append(result["id"])
|
||||
self.pluginsMetadataChanged.emit()
|
||||
|
||||
Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Plugin browser"), result["message"])
|
||||
|
||||
self._temp_plugin_file.close() # Plugin was installed, delete temp file
|
||||
|
||||
@pyqtProperty(int, notify = onDownloadProgressChanged)
|
||||
def downloadProgress(self):
|
||||
return self._download_progress
|
||||
|
||||
def setDownloadProgress(self, progress):
|
||||
if progress != self._download_progress:
|
||||
self._download_progress = progress
|
||||
self.onDownloadProgressChanged.emit()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def downloadAndInstallPlugin(self, url):
|
||||
Logger.log("i", "Attempting to download & install plugin from %s", url)
|
||||
url = QUrl(url)
|
||||
self._download_plugin_request = QNetworkRequest(url)
|
||||
self._download_plugin_request.setRawHeader(*self._request_header)
|
||||
self._download_plugin_reply = self._network_manager.get(self._download_plugin_request)
|
||||
self.setDownloadProgress(0)
|
||||
self.setIsDownloading(True)
|
||||
self._download_plugin_reply.downloadProgress.connect(self._onDownloadPluginProgress)
|
||||
|
||||
@pyqtSlot()
|
||||
def cancelDownload(self):
|
||||
Logger.log("i", "user cancelled the download of a plugin")
|
||||
self._download_plugin_reply.abort()
|
||||
self._download_plugin_reply.downloadProgress.disconnect(self._onDownloadPluginProgress)
|
||||
self._download_plugin_reply = None
|
||||
self._download_plugin_request = None
|
||||
|
||||
self.setDownloadProgress(0)
|
||||
self.setIsDownloading(False)
|
||||
|
||||
@pyqtProperty(QObject, notify=pluginsMetadataChanged)
|
||||
def pluginsModel(self):
|
||||
if self._plugins_model is None:
|
||||
self._plugins_model = ListModel()
|
||||
self._plugins_model.addRoleName(Qt.UserRole + 1, "name")
|
||||
self._plugins_model.addRoleName(Qt.UserRole + 2, "version")
|
||||
self._plugins_model.addRoleName(Qt.UserRole + 3, "short_description")
|
||||
self._plugins_model.addRoleName(Qt.UserRole + 4, "author")
|
||||
self._plugins_model.addRoleName(Qt.UserRole + 5, "already_installed")
|
||||
self._plugins_model.addRoleName(Qt.UserRole + 6, "file_location")
|
||||
self._plugins_model.addRoleName(Qt.UserRole + 7, "can_upgrade")
|
||||
else:
|
||||
self._plugins_model.clear()
|
||||
items = []
|
||||
for metadata in self._plugins_metadata:
|
||||
items.append({
|
||||
"name": metadata["label"],
|
||||
"version": metadata["version"],
|
||||
"short_description": metadata["short_description"],
|
||||
"author": metadata["author"],
|
||||
"already_installed": self._checkAlreadyInstalled(metadata["id"]),
|
||||
"file_location": metadata["file_location"],
|
||||
"can_upgrade": self._checkCanUpgrade(metadata["id"], metadata["version"])
|
||||
})
|
||||
self._plugins_model.setItems(items)
|
||||
return self._plugins_model
|
||||
|
||||
def _checkCanUpgrade(self, id, version):
|
||||
plugin_registry = PluginRegistry.getInstance()
|
||||
metadata = plugin_registry.getMetaData(id)
|
||||
if metadata != {}:
|
||||
if id in self._newly_installed_plugin_ids:
|
||||
return False # We already updated this plugin.
|
||||
current_version = Version(metadata["plugin"]["version"])
|
||||
new_version = Version(version)
|
||||
if new_version > current_version:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _checkAlreadyInstalled(self, id):
|
||||
plugin_registry = PluginRegistry.getInstance()
|
||||
metadata = plugin_registry.getMetaData(id)
|
||||
if metadata != {}:
|
||||
return True
|
||||
else:
|
||||
if id in self._newly_installed_plugin_ids:
|
||||
return True # We already installed this plugin, but the registry just doesn't know it yet.
|
||||
return False
|
||||
|
||||
def _onRequestFinished(self, reply):
|
||||
reply_url = reply.url().toString()
|
||||
if reply.error() == QNetworkReply.TimeoutError:
|
||||
Logger.log("w", "Got a timeout.")
|
||||
# Reset everything.
|
||||
self.setDownloadProgress(0)
|
||||
self.setIsDownloading(False)
|
||||
if self._download_plugin_reply:
|
||||
self._download_plugin_reply.downloadProgress.disconnect(self._onDownloadPluginProgress)
|
||||
self._download_plugin_reply.abort()
|
||||
self._download_plugin_reply = None
|
||||
return
|
||||
elif reply.error() == QNetworkReply.HostNotFoundError:
|
||||
Logger.log("w", "Unable to reach server.")
|
||||
return
|
||||
|
||||
if reply.operation() == QNetworkAccessManager.GetOperation:
|
||||
if reply_url == self._api_url + "plugins":
|
||||
try:
|
||||
json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||
self._plugins_metadata = json_data
|
||||
self.pluginsMetadataChanged.emit()
|
||||
except json.decoder.JSONDecodeError:
|
||||
Logger.log("w", "Received an invalid print job state message: Not valid JSON.")
|
||||
return
|
||||
else:
|
||||
# Ignore any operation that is not a get operation
|
||||
pass
|
||||
|
||||
def _onNetworkAccesibleChanged(self, accessible):
|
||||
if accessible == 0:
|
||||
self.setDownloadProgress(0)
|
||||
self.setIsDownloading(False)
|
||||
if self._download_plugin_reply:
|
||||
self._download_plugin_reply.downloadProgress.disconnect(self._onDownloadPluginProgress)
|
||||
self._download_plugin_reply.abort()
|
||||
self._download_plugin_reply = None
|
||||
|
||||
def _createNetworkManager(self):
|
||||
if self._network_manager:
|
||||
self._network_manager.finished.disconnect(self._onRequestFinished)
|
||||
self._network_manager.networkAccessibleChanged.disconnect(self._onNetworkAccesibleChanged)
|
||||
|
||||
self._network_manager = QNetworkAccessManager()
|
||||
self._network_manager.finished.connect(self._onRequestFinished)
|
||||
self._network_manager.networkAccessibleChanged.connect(self._onNetworkAccesibleChanged)
|
182
plugins/PluginBrowser/PluginBrowser.qml
Normal file
182
plugins/PluginBrowser/PluginBrowser.qml
Normal file
@ -0,0 +1,182 @@
|
||||
import UM 1.1 as UM
|
||||
import QtQuick 2.2
|
||||
import QtQuick.Dialogs 1.1
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Controls 1.1
|
||||
|
||||
UM.Dialog
|
||||
{
|
||||
id: base
|
||||
|
||||
title: catalog.i18nc("@title:window", "Find & Update plugins")
|
||||
width: 600
|
||||
height: 450
|
||||
Item
|
||||
{
|
||||
anchors.fill: parent
|
||||
Item
|
||||
{
|
||||
id: topBar
|
||||
height: childrenRect.height;
|
||||
width: parent.width
|
||||
Label
|
||||
{
|
||||
id: introText
|
||||
text: catalog.i18nc("@label", "Here you can find a list of Third Party plugins.")
|
||||
width: parent.width
|
||||
height: 30
|
||||
}
|
||||
|
||||
Button
|
||||
{
|
||||
id: refresh
|
||||
text: catalog.i18nc("@action:button", "Refresh")
|
||||
onClicked: manager.requestPluginList()
|
||||
anchors.right: parent.right
|
||||
enabled: !manager.isDownloading
|
||||
}
|
||||
}
|
||||
ScrollView
|
||||
{
|
||||
width: parent.width
|
||||
anchors.top: topBar.bottom
|
||||
anchors.bottom: bottomBar.top
|
||||
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
|
||||
frameVisible: true
|
||||
ListView
|
||||
{
|
||||
id: pluginList
|
||||
model: manager.pluginsModel
|
||||
anchors.fill: parent
|
||||
|
||||
property var activePlugin
|
||||
delegate: pluginDelegate
|
||||
}
|
||||
}
|
||||
Item
|
||||
{
|
||||
id: bottomBar
|
||||
width: parent.width
|
||||
height: closeButton.height
|
||||
anchors.bottom:parent.bottom
|
||||
anchors.left: parent.left
|
||||
ProgressBar
|
||||
{
|
||||
id: progressbar
|
||||
anchors.bottom: parent.bottom
|
||||
minimumValue: 0;
|
||||
maximumValue: 100
|
||||
anchors.left:parent.left
|
||||
anchors.right: closeButton.left
|
||||
anchors.rightMargin: UM.Theme.getSize("default_margin").width
|
||||
value: manager.isDownloading ? manager.downloadProgress : 0
|
||||
}
|
||||
|
||||
Button
|
||||
{
|
||||
id: closeButton
|
||||
text: catalog.i18nc("@action:button", "Close")
|
||||
iconName: "dialog-close"
|
||||
onClicked:
|
||||
{
|
||||
if (manager.isDownloading)
|
||||
{
|
||||
manager.cancelDownload()
|
||||
}
|
||||
base.close();
|
||||
}
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
}
|
||||
}
|
||||
|
||||
Item
|
||||
{
|
||||
SystemPalette { id: palette }
|
||||
Component
|
||||
{
|
||||
id: pluginDelegate
|
||||
Rectangle
|
||||
{
|
||||
width: pluginList.width;
|
||||
height: texts.height;
|
||||
color: index % 2 ? palette.base : palette.alternateBase
|
||||
Column
|
||||
{
|
||||
id: texts
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: UM.Theme.getSize("default_margin").width
|
||||
anchors.right: downloadButton.left
|
||||
anchors.rightMargin: UM.Theme.getSize("default_margin").width
|
||||
Label
|
||||
{
|
||||
text: "<b>" + model.name + "</b> - " + model.author
|
||||
width: contentWidth
|
||||
height: contentHeight + UM.Theme.getSize("default_margin").height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
text: model.short_description
|
||||
width: parent.width
|
||||
height: contentHeight + UM.Theme.getSize("default_margin").height
|
||||
wrapMode: Text.WordWrap
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
Button
|
||||
{
|
||||
id: downloadButton
|
||||
text:
|
||||
{
|
||||
if (manager.isDownloading && pluginList.activePlugin == model)
|
||||
{
|
||||
return catalog.i18nc("@action:button", "Cancel");
|
||||
}
|
||||
else if (model.already_installed)
|
||||
{
|
||||
if (model.can_upgrade)
|
||||
{
|
||||
return catalog.i18nc("@action:button", "Upgrade");
|
||||
}
|
||||
return catalog.i18nc("@action:button", "Installed");
|
||||
}
|
||||
return catalog.i18nc("@action:button", "Download");
|
||||
}
|
||||
onClicked:
|
||||
{
|
||||
if(!manager.isDownloading)
|
||||
{
|
||||
pluginList.activePlugin = model;
|
||||
manager.downloadAndInstallPlugin(model.file_location);
|
||||
}
|
||||
else
|
||||
{
|
||||
manager.cancelDownload();
|
||||
}
|
||||
}
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: UM.Theme.getSize("default_margin").width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
enabled:
|
||||
{
|
||||
if (manager.isDownloading)
|
||||
{
|
||||
return (pluginList.activePlugin == model);
|
||||
}
|
||||
else
|
||||
{
|
||||
return (!model.already_installed || model.can_upgrade);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
UM.I18nCatalog { id: catalog; name:"cura" }
|
||||
}
|
||||
}
|
12
plugins/PluginBrowser/__init__.py
Normal file
12
plugins/PluginBrowser/__init__.py
Normal file
@ -0,0 +1,12 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# PluginBrowser is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from . import PluginBrowser
|
||||
|
||||
|
||||
def getMetaData():
|
||||
return {}
|
||||
|
||||
|
||||
def register(app):
|
||||
return {"extension": PluginBrowser.PluginBrowser()}
|
7
plugins/PluginBrowser/plugin.json
Normal file
7
plugins/PluginBrowser/plugin.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "Plugin Browser",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"api": 4,
|
||||
"description": "Find, manage and install new plugins."
|
||||
}
|
@ -1,19 +1,18 @@
|
||||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Copyright (c) 2013 David Braam
|
||||
# Uranium is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
from . import RemovableDrivePlugin
|
||||
|
||||
import string
|
||||
import ctypes # type: ignore
|
||||
from ctypes import wintypes # Using ctypes.wintypes in the code below does not seem to work
|
||||
import ctypes
|
||||
from ctypes import wintypes # Using ctypes.wintypes in the code below does not seem to work
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
# Ignore windows error popups. Fixes the whole "Can't open drive X" when user has an SD card reader.
|
||||
ctypes.windll.kernel32.SetErrorMode(1)
|
||||
|
||||
# WinAPI Constants that we need
|
||||
# Hardcoded here due to stupid WinDLL stuff that does not give us access to these values.
|
||||
DRIVE_REMOVABLE = 2 # [CodeStyle: Windows Enum value]
|
||||
|
@ -8,13 +8,6 @@ catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": catalog.i18nc("@label", "Removable Drive Output Device Plugin"),
|
||||
"author": "Ultimaker B.V.",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Provides removable drive hotplugging and writing support."),
|
||||
"version": "1.0",
|
||||
"api": 3
|
||||
}
|
||||
}
|
||||
|
||||
def register(app):
|
||||
|
8
plugins/RemovableDriveOutputDevice/plugin.json
Normal file
8
plugins/RemovableDriveOutputDevice/plugin.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Removable Drive Output Device Plugin",
|
||||
"author": "Ultimaker B.V.",
|
||||
"description": "Provides removable drive hotplugging and writing support.",
|
||||
"version": "1.0.0",
|
||||
"api": 4,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
@ -1,69 +1,37 @@
|
||||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
from typing import Any
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
|
||||
from UM.Extension import Extension
|
||||
from UM.Application import Application
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Message import Message
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Platform import Platform
|
||||
|
||||
import time
|
||||
|
||||
from UM.Qt.Duration import DurationFormat
|
||||
from UM.Job import Job
|
||||
|
||||
from .SliceInfoJob import SliceInfoJob
|
||||
|
||||
import platform
|
||||
import math
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import ssl
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
class SliceInfoJob(Job):
|
||||
data = None # type: Any
|
||||
url = None # type: str
|
||||
|
||||
def __init__(self, url, data):
|
||||
super().__init__()
|
||||
self.url = url
|
||||
self.data = data
|
||||
|
||||
def run(self):
|
||||
if not self.url or not self.data:
|
||||
Logger.log("e", "URL or DATA for sending slice info was not set!")
|
||||
return
|
||||
|
||||
# Submit data
|
||||
kwoptions = {"data" : self.data,
|
||||
"timeout" : 5
|
||||
}
|
||||
|
||||
if Platform.isOSX():
|
||||
kwoptions["context"] = ssl._create_unverified_context()
|
||||
|
||||
Logger.log("d", "Sending anonymous slice info to [%s]...", self.url)
|
||||
|
||||
try:
|
||||
f = urllib.request.urlopen(self.url, **kwoptions)
|
||||
Logger.log("i", "Sent anonymous slice info.")
|
||||
f.close()
|
||||
except urllib.error.HTTPError as http_exception:
|
||||
Logger.log("e", "An HTTP error occurred while trying to send slice information: %s" % http_exception)
|
||||
except Exception as e: # We don't want any exception to cause problems
|
||||
Logger.log("e", "An exception occurred while trying to send slice information: %s" % e)
|
||||
|
||||
## This Extension runs in the background and sends several bits of information to the Ultimaker servers.
|
||||
# The data is only sent when the user in question gave permission to do so. All data is anonymous and
|
||||
# no model files are being sent (Just a SHA256 hash of the model).
|
||||
class SliceInfo(Extension):
|
||||
info_url = "https://stats.youmagine.com/curastats/slice"
|
||||
info_url = "http://stats.ultimaker.com/api/cura"
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@ -85,61 +53,139 @@ class SliceInfo(Extension):
|
||||
try:
|
||||
if not Preferences.getInstance().getValue("info/send_slice_info"):
|
||||
Logger.log("d", "'info/send_slice_info' is turned off.")
|
||||
return # Do nothing, user does not want to send data
|
||||
|
||||
# Listing all files placed on the buildplate
|
||||
modelhashes = []
|
||||
for node in DepthFirstIterator(CuraApplication.getInstance().getController().getScene().getRoot()):
|
||||
if node.callDecoration("isSliceable"):
|
||||
modelhashes.append(node.getMeshData().getHash())
|
||||
|
||||
# Creating md5sums and formatting them as discussed on JIRA
|
||||
modelhash_formatted = ",".join(modelhashes)
|
||||
return # Do nothing, user does not want to send data
|
||||
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
|
||||
# Get total material used (in mm^3)
|
||||
print_information = Application.getInstance().getPrintInformation()
|
||||
material_radius = 0.5 * global_container_stack.getProperty("material_diameter", "value")
|
||||
|
||||
# Send material per extruder
|
||||
material_used = [str(math.pi * material_radius * material_radius * material_length) for material_length in print_information.materialLengths]
|
||||
material_used = ",".join(material_used)
|
||||
data = dict() # The data that we're going to submit.
|
||||
data["time_stamp"] = time.time()
|
||||
data["schema_version"] = 0
|
||||
data["cura_version"] = Application.getInstance().getVersion()
|
||||
|
||||
containers = { "": global_container_stack.serialize() }
|
||||
for container in global_container_stack.getContainers():
|
||||
container_id = container.getId()
|
||||
try:
|
||||
container_serialized = container.serialize()
|
||||
except NotImplementedError:
|
||||
Logger.log("w", "Container %s could not be serialized!", container_id)
|
||||
continue
|
||||
if container_serialized:
|
||||
containers[container_id] = container_serialized
|
||||
else:
|
||||
Logger.log("i", "No data found in %s to be serialized!", container_id)
|
||||
active_mode = Preferences.getInstance().getValue("cura/active_mode")
|
||||
if active_mode == 0:
|
||||
data["active_mode"] = "recommended"
|
||||
else:
|
||||
data["active_mode"] = "custom"
|
||||
|
||||
# Bundle the collected data
|
||||
submitted_data = {
|
||||
"processor": platform.processor(),
|
||||
"machine": platform.machine(),
|
||||
"platform": platform.platform(),
|
||||
"settings": json.dumps(containers), # bundle of containers with their serialized contents
|
||||
"version": Application.getInstance().getVersion(),
|
||||
"modelhash": modelhash_formatted,
|
||||
"printtime": print_information.currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601),
|
||||
"filament": material_used,
|
||||
"language": Preferences.getInstance().getValue("general/language"),
|
||||
}
|
||||
data["machine_settings_changed_by_user"] = global_container_stack.definitionChanges.getId() != "empty"
|
||||
data["language"] = Preferences.getInstance().getValue("general/language")
|
||||
data["os"] = {"type": platform.system(), "version": platform.version()}
|
||||
|
||||
data["active_machine"] = {"definition_id": global_container_stack.definition.getId(), "manufacturer": global_container_stack.definition.getMetaData().get("manufacturer","")}
|
||||
|
||||
data["extruders"] = []
|
||||
extruders = list(ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId()))
|
||||
extruders = sorted(extruders, key = lambda extruder: extruder.getMetaDataEntry("position"))
|
||||
|
||||
if not extruders:
|
||||
extruders = [global_container_stack]
|
||||
|
||||
for extruder in extruders:
|
||||
extruder_dict = dict()
|
||||
extruder_dict["active"] = ExtruderManager.getInstance().getActiveExtruderStack() == extruder
|
||||
extruder_dict["material"] = {"GUID": extruder.material.getMetaData().get("GUID", ""),
|
||||
"type": extruder.material.getMetaData().get("material", ""),
|
||||
"brand": extruder.material.getMetaData().get("brand", "")
|
||||
}
|
||||
extruder_dict["material_used"] = print_information.materialLengths[int(extruder.getMetaDataEntry("position", "0"))]
|
||||
extruder_dict["variant"] = extruder.variant.getName()
|
||||
extruder_dict["nozzle_size"] = extruder.getProperty("machine_nozzle_size", "value")
|
||||
|
||||
extruder_settings = dict()
|
||||
extruder_settings["wall_line_count"] = extruder.getProperty("wall_line_count", "value")
|
||||
extruder_settings["retraction_enable"] = extruder.getProperty("retraction_enable", "value")
|
||||
extruder_settings["infill_sparse_density"] = extruder.getProperty("infill_sparse_density", "value")
|
||||
extruder_settings["infill_pattern"] = extruder.getProperty("infill_pattern", "value")
|
||||
extruder_settings["gradual_infill_steps"] = extruder.getProperty("gradual_infill_steps", "value")
|
||||
extruder_settings["default_material_print_temperature"] = extruder.getProperty("default_material_print_temperature", "value")
|
||||
extruder_settings["material_print_temperature"] = extruder.getProperty("material_print_temperature", "value")
|
||||
extruder_dict["extruder_settings"] = extruder_settings
|
||||
data["extruders"].append(extruder_dict)
|
||||
|
||||
data["quality_profile"] = global_container_stack.quality.getMetaData().get("quality_type")
|
||||
|
||||
data["models"] = []
|
||||
# Listing all files placed on the build plate
|
||||
for node in DepthFirstIterator(CuraApplication.getInstance().getController().getScene().getRoot()):
|
||||
if node.callDecoration("isSliceable"):
|
||||
model = dict()
|
||||
model["hash"] = node.getMeshData().getHash()
|
||||
bounding_box = node.getBoundingBox()
|
||||
model["bounding_box"] = {"minimum": {"x": bounding_box.minimum.x,
|
||||
"y": bounding_box.minimum.y,
|
||||
"z": bounding_box.minimum.z},
|
||||
"maximum": {"x": bounding_box.maximum.x,
|
||||
"y": bounding_box.maximum.y,
|
||||
"z": bounding_box.maximum.z}}
|
||||
model["transformation"] = {"data": str(node.getWorldTransformation().getData()).replace("\n", "")}
|
||||
extruder_position = node.callDecoration("getActiveExtruderPosition")
|
||||
model["extruder"] = 0 if extruder_position is None else int(extruder_position)
|
||||
|
||||
model_settings = dict()
|
||||
model_stack = node.callDecoration("getStack")
|
||||
if model_stack:
|
||||
model_settings["support_enabled"] = model_stack.getProperty("support_enable", "value")
|
||||
model_settings["support_extruder_nr"] = int(model_stack.getProperty("support_extruder_nr", "value"))
|
||||
|
||||
# Mesh modifiers;
|
||||
model_settings["infill_mesh"] = model_stack.getProperty("infill_mesh", "value")
|
||||
model_settings["cutting_mesh"] = model_stack.getProperty("cutting_mesh", "value")
|
||||
model_settings["support_mesh"] = model_stack.getProperty("support_mesh", "value")
|
||||
model_settings["anti_overhang_mesh"] = model_stack.getProperty("anti_overhang_mesh", "value")
|
||||
|
||||
model_settings["wall_line_count"] = model_stack.getProperty("wall_line_count", "value")
|
||||
model_settings["retraction_enable"] = model_stack.getProperty("retraction_enable", "value")
|
||||
|
||||
# Infill settings
|
||||
model_settings["infill_sparse_density"] = model_stack.getProperty("infill_sparse_density", "value")
|
||||
model_settings["infill_pattern"] = model_stack.getProperty("infill_pattern", "value")
|
||||
model_settings["gradual_infill_steps"] = model_stack.getProperty("gradual_infill_steps", "value")
|
||||
|
||||
model["model_settings"] = model_settings
|
||||
|
||||
data["models"].append(model)
|
||||
|
||||
print_times = print_information.printTimesPerFeature
|
||||
data["print_times"] = {"travel": int(print_times["travel"].getDisplayString(DurationFormat.Format.Seconds)),
|
||||
"support": int(print_times["support"].getDisplayString(DurationFormat.Format.Seconds)),
|
||||
"infill": int(print_times["infill"].getDisplayString(DurationFormat.Format.Seconds)),
|
||||
"total": int(print_information.currentPrintTime.getDisplayString(DurationFormat.Format.Seconds))}
|
||||
|
||||
print_settings = dict()
|
||||
print_settings["layer_height"] = global_container_stack.getProperty("layer_height", "value")
|
||||
|
||||
# Support settings
|
||||
print_settings["support_enabled"] = global_container_stack.getProperty("support_enable", "value")
|
||||
print_settings["support_extruder_nr"] = int(global_container_stack.getProperty("support_extruder_nr", "value"))
|
||||
|
||||
# Platform adhesion settings
|
||||
print_settings["adhesion_type"] = global_container_stack.getProperty("adhesion_type", "value")
|
||||
|
||||
# Shell settings
|
||||
print_settings["wall_line_count"] = global_container_stack.getProperty("wall_line_count", "value")
|
||||
print_settings["retraction_enable"] = global_container_stack.getProperty("retraction_enable", "value")
|
||||
|
||||
# Prime tower settings
|
||||
print_settings["prime_tower_enable"] = global_container_stack.getProperty("prime_tower_enable", "value")
|
||||
|
||||
# Infill settings
|
||||
print_settings["infill_sparse_density"] = global_container_stack.getProperty("infill_sparse_density", "value")
|
||||
print_settings["infill_pattern"] = global_container_stack.getProperty("infill_pattern", "value")
|
||||
print_settings["gradual_infill_steps"] = global_container_stack.getProperty("gradual_infill_steps", "value")
|
||||
|
||||
print_settings["print_sequence"] = global_container_stack.getProperty("print_sequence", "value")
|
||||
|
||||
data["print_settings"] = print_settings
|
||||
|
||||
# Convert data to bytes
|
||||
submitted_data = urllib.parse.urlencode(submitted_data)
|
||||
binary_data = submitted_data.encode("utf-8")
|
||||
binary_data = json.dumps(data).encode("utf-8")
|
||||
|
||||
# Sending slice info non-blocking
|
||||
reportJob = SliceInfoJob(self.info_url, binary_data)
|
||||
reportJob.start()
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
# We really can't afford to have a mistake here, as this would break the sending of g-code to a device
|
||||
# (Either saving or directly to a printer). The functionality of the slice data is not *that* important.
|
||||
Logger.log("e", "Exception raised while sending slice info: %s" %(repr(e))) # But we should be notified about these problems of course.
|
||||
Logger.logException("e", "Exception raised while sending slice info.") # But we should be notified about these problems of course.
|
||||
|
38
plugins/SliceInfoPlugin/SliceInfoJob.py
Normal file
38
plugins/SliceInfoPlugin/SliceInfoJob.py
Normal file
@ -0,0 +1,38 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
from UM.Job import Job
|
||||
from UM.Logger import Logger
|
||||
from UM.Platform import Platform
|
||||
|
||||
import ssl
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
|
||||
class SliceInfoJob(Job):
|
||||
def __init__(self, url, data):
|
||||
super().__init__()
|
||||
self._url = url
|
||||
self._data = data
|
||||
|
||||
def run(self):
|
||||
if not self._url or not self._data:
|
||||
Logger.log("e", "URL or DATA for sending slice info was not set!")
|
||||
return
|
||||
|
||||
# Submit data
|
||||
kwoptions = {"data" : self._data, "timeout" : 5}
|
||||
|
||||
if Platform.isOSX():
|
||||
kwoptions["context"] = ssl._create_unverified_context()
|
||||
|
||||
Logger.log("i", "Sending anonymous slice info to [%s]...", self._url)
|
||||
|
||||
try:
|
||||
f = urllib.request.urlopen(self._url, **kwoptions)
|
||||
Logger.log("i", "Sent anonymous slice info.")
|
||||
f.close()
|
||||
except urllib.error.HTTPError:
|
||||
Logger.logException("e", "An HTTP error occurred while trying to send slice information")
|
||||
except Exception: # We don't want any exception to cause problems
|
||||
Logger.logException("e", "An exception occurred while trying to send slice information")
|
@ -6,13 +6,6 @@ catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": catalog.i18nc("@label", "Slice info"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Submits anonymous slice info. Can be disabled through preferences."),
|
||||
"api": 3
|
||||
}
|
||||
}
|
||||
|
||||
def register(app):
|
||||
|
8
plugins/SliceInfoPlugin/plugin.json
Normal file
8
plugins/SliceInfoPlugin/plugin.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Slice info",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Submits anonymous slice info. Can be disabled through preferences.",
|
||||
"api": 4,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
@ -7,7 +7,7 @@ from UM.Scene.Selection import Selection
|
||||
from UM.Resources import Resources
|
||||
from UM.Application import Application
|
||||
from UM.Preferences import Preferences
|
||||
from UM.View.Renderer import Renderer
|
||||
from UM.View.RenderBatch import RenderBatch
|
||||
from UM.Settings.Validator import ValidatorState
|
||||
from UM.Math.Color import Color
|
||||
from UM.View.GL.OpenGL import OpenGL
|
||||
@ -118,7 +118,7 @@ class SolidView(View):
|
||||
else:
|
||||
renderer.queueNode(node, material = self._enabled_shader, uniforms = uniforms)
|
||||
if node.callDecoration("isGroup") and Selection.isSelected(node):
|
||||
renderer.queueNode(scene.getRoot(), mesh = node.getBoundingBoxMesh(), mode = Renderer.RenderLines)
|
||||
renderer.queueNode(scene.getRoot(), mesh = node.getBoundingBoxMesh(), mode = RenderBatch.RenderMode.LineLoop)
|
||||
|
||||
def endRendering(self):
|
||||
pass
|
||||
|
@ -8,13 +8,6 @@ i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": i18n_catalog.i18nc("@label", "Solid View"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"description": i18n_catalog.i18nc("@info:whatsthis", "Provides a normal solid mesh view."),
|
||||
"api": 3
|
||||
},
|
||||
"view": {
|
||||
"name": i18n_catalog.i18nc("@item:inmenu", "Solid"),
|
||||
"weight": 0
|
||||
|
8
plugins/SolidView/plugin.json
Normal file
8
plugins/SolidView/plugin.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Solid View",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides a normal solid mesh view.",
|
||||
"api": 4,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
@ -12,7 +12,6 @@ Cura.MachineAction
|
||||
anchors.fill: parent;
|
||||
property var selectedPrinter: null
|
||||
property bool completeProperties: true
|
||||
property var connectingToPrinter: null
|
||||
|
||||
Connections
|
||||
{
|
||||
@ -33,9 +32,8 @@ Cura.MachineAction
|
||||
if(base.selectedPrinter && base.completeProperties)
|
||||
{
|
||||
var printerKey = base.selectedPrinter.getKey()
|
||||
if(connectingToPrinter != printerKey) {
|
||||
// prevent an infinite loop
|
||||
connectingToPrinter = printerKey;
|
||||
if(manager.getStoredKey() != printerKey)
|
||||
{
|
||||
manager.setKey(printerKey);
|
||||
completed();
|
||||
}
|
||||
|
34
plugins/UM3NetworkPrinting/MonitorItem.qml
Normal file
34
plugins/UM3NetworkPrinting/MonitorItem.qml
Normal file
@ -0,0 +1,34 @@
|
||||
import QtQuick 2.2
|
||||
|
||||
|
||||
import UM 1.3 as UM
|
||||
import Cura 1.0 as Cura
|
||||
|
||||
Component
|
||||
{
|
||||
Image
|
||||
{
|
||||
id: cameraImage
|
||||
width: sourceSize.width
|
||||
height: sourceSize.height * width / sourceSize.width
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
onVisibleChanged:
|
||||
{
|
||||
if(visible)
|
||||
{
|
||||
OutputDevice.startCamera()
|
||||
} else
|
||||
{
|
||||
OutputDevice.stopCamera()
|
||||
}
|
||||
}
|
||||
source:
|
||||
{
|
||||
if(OutputDevice.cameraImage)
|
||||
{
|
||||
return OutputDevice.cameraImage;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
@ -178,6 +178,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
|
||||
self._last_command = ""
|
||||
|
||||
self._compressing_print = False
|
||||
self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml")
|
||||
|
||||
printer_type = self._properties.get(b"machine", b"").decode("utf-8")
|
||||
if printer_type.startswith("9511"):
|
||||
@ -187,9 +188,18 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
|
||||
else:
|
||||
self._updatePrinterType("unknown")
|
||||
|
||||
Application.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
|
||||
|
||||
def _onNetworkAccesibleChanged(self, accessible):
|
||||
Logger.log("d", "Network accessible state changed to: %s", accessible)
|
||||
|
||||
## Triggered when the output device manager changes devices.
|
||||
#
|
||||
# This is how we can detect that our device is no longer active now.
|
||||
def _onOutputDevicesChanged(self):
|
||||
if self.getId() not in Application.getInstance().getOutputDeviceManager().getOutputDeviceIds():
|
||||
self.stopCamera()
|
||||
|
||||
def _onAuthenticationTimer(self):
|
||||
self._authentication_counter += 1
|
||||
self._authentication_requested_message.setProgress(self._authentication_counter / self._max_authentication_counter * 100)
|
||||
@ -306,8 +316,11 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
|
||||
def _stopCamera(self):
|
||||
self._camera_timer.stop()
|
||||
if self._image_reply:
|
||||
self._image_reply.abort()
|
||||
self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress)
|
||||
try:
|
||||
self._image_reply.abort()
|
||||
self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress)
|
||||
except RuntimeError:
|
||||
pass # It can happen that the wrapped c++ object is already deleted.
|
||||
self._image_reply = None
|
||||
self._image_request = None
|
||||
|
||||
@ -612,7 +625,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
|
||||
# is ignored.
|
||||
# \param kwargs Keyword arguments.
|
||||
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs):
|
||||
if self._printer_state != "idle":
|
||||
if self._printer_state not in ["idle", ""]:
|
||||
self._error_message = Message(
|
||||
i18n_catalog.i18nc("@info:status", "Unable to start a new print job, printer is busy. Current printer status is %s.") % self._printer_state)
|
||||
self._error_message.show()
|
||||
@ -638,7 +651,7 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice):
|
||||
if self._json_printer_state["heads"][0]["extruders"][index]["hotend"]["id"] == "":
|
||||
Logger.log("e", "No cartridge loaded in slot %s, unable to start print", index + 1)
|
||||
self._error_message = Message(
|
||||
i18n_catalog.i18nc("@info:status", "Unable to start a new print job. No PrinterCore loaded in slot {0}".format(index + 1)))
|
||||
i18n_catalog.i18nc("@info:status", "Unable to start a new print job. No Printcore loaded in slot {0}".format(index + 1)))
|
||||
self._error_message.show()
|
||||
return
|
||||
if self._json_printer_state["heads"][0]["extruders"][index]["active_material"]["guid"] == "":
|
||||
|
@ -6,15 +6,7 @@ from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"plugin": {
|
||||
"name": "UM3 Network Connection",
|
||||
"author": "Ultimaker",
|
||||
"description": catalog.i18nc("@info:whatsthis", "Manages network connections to Ultimaker 3 printers"),
|
||||
"version": "1.0",
|
||||
"api": 3
|
||||
}
|
||||
}
|
||||
return {}
|
||||
|
||||
def register(app):
|
||||
return { "output_device": NetworkPrinterOutputDevicePlugin.NetworkPrinterOutputDevicePlugin(), "machine_action": DiscoverUM3Action.DiscoverUM3Action()}
|
8
plugins/UM3NetworkPrinting/plugin.json
Normal file
8
plugins/UM3NetworkPrinting/plugin.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "UM3 Network Connection",
|
||||
"author": "Ultimaker B.V.",
|
||||
"description": "Manages network connections to Ultimaker 3 printers",
|
||||
"version": "1.0.0",
|
||||
"api": 4,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
@ -259,7 +259,7 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
|
||||
i = 0
|
||||
while True:
|
||||
values = winreg.EnumValue(key, i)
|
||||
if not only_list_usb or "USBSER" in values[0]:
|
||||
if not only_list_usb or "USBSER" or "VCP" in values[0]:
|
||||
base_list += [values[1]]
|
||||
i += 1
|
||||
except Exception as e:
|
||||
|
@ -8,14 +8,6 @@ i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"type": "extension",
|
||||
"plugin": {
|
||||
"name": i18n_catalog.i18nc("@label", "USB printing"),
|
||||
"author": "Ultimaker",
|
||||
"version": "1.0",
|
||||
"api": 3,
|
||||
"description": i18n_catalog.i18nc("@info:whatsthis","Accepts G-Code and sends them to a printer. Plugin can also update firmware.")
|
||||
}
|
||||
}
|
||||
|
||||
def register(app):
|
||||
|
8
plugins/USBPrinting/plugin.json
Normal file
8
plugins/USBPrinting/plugin.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "USB printing",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"api": 4,
|
||||
"description": "Accepts G-Code and sends them to a printer. Plugin can also update firmware.",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
65
plugins/UltimakerMachineActions/UM2UpgradeSelection.py
Normal file
65
plugins/UltimakerMachineActions/UM2UpgradeSelection.py
Normal file
@ -0,0 +1,65 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Uranium is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from cura.MachineAction import MachineAction
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Application import Application
|
||||
from UM.Util import parseBool
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
import UM.Settings.InstanceContainer
|
||||
|
||||
|
||||
## The Ultimaker 2 can have a few revisions & upgrades.
|
||||
class UM2UpgradeSelection(MachineAction):
|
||||
def __init__(self):
|
||||
super().__init__("UM2UpgradeSelection", catalog.i18nc("@action", "Select upgrades"))
|
||||
self._qml_url = "UM2UpgradeSelectionMachineAction.qml"
|
||||
|
||||
self._container_registry = ContainerRegistry.getInstance()
|
||||
|
||||
def _reset(self):
|
||||
self.hasVariantsChanged.emit()
|
||||
|
||||
hasVariantsChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(bool, notify = hasVariantsChanged)
|
||||
def hasVariants(self):
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack:
|
||||
return parseBool(global_container_stack.getMetaDataEntry("has_variants", "false"))
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def setHasVariants(self, has_variants = True):
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack:
|
||||
variant_container = global_container_stack.variant
|
||||
variant_index = global_container_stack.getContainerIndex(variant_container)
|
||||
|
||||
if has_variants:
|
||||
if "has_variants" in global_container_stack.getMetaData():
|
||||
global_container_stack.setMetaDataEntry("has_variants", True)
|
||||
else:
|
||||
global_container_stack.addMetaDataEntry("has_variants", True)
|
||||
|
||||
# Set the variant container to a sane default
|
||||
empty_container = ContainerRegistry.getInstance().getEmptyInstanceContainer()
|
||||
if type(variant_container) == type(empty_container):
|
||||
search_criteria = { "type": "variant", "definition": "ultimaker2", "id": "*0.4*" }
|
||||
containers = self._container_registry.findInstanceContainers(**search_criteria)
|
||||
if containers:
|
||||
global_container_stack.variant = containers[0]
|
||||
else:
|
||||
# The metadata entry is stored in an ini, and ini files are parsed as strings only.
|
||||
# Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False.
|
||||
if "has_variants" in global_container_stack.getMetaData():
|
||||
global_container_stack.removeMetaDataEntry("has_variants")
|
||||
|
||||
# Set the variant container to an empty variant
|
||||
global_container_stack.variant = ContainerRegistry.getInstance().getEmptyInstanceContainer()
|
||||
|
||||
Application.getInstance().globalContainerStackChanged.emit()
|
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