Merge branch 'master' into feature_send_material_profiles

This commit is contained in:
Ghostkeeper 2018-06-13 16:57:16 +02:00
commit bb366afc2b
No known key found for this signature in database
GPG key ID: 5252B696FB5E7C7A
53 changed files with 302 additions and 266 deletions

View file

@ -3,30 +3,26 @@
from cura.Backups.BackupsManager import BackupsManager from cura.Backups.BackupsManager import BackupsManager
## The back-ups API provides a version-proof bridge between Cura's
# BackupManager and plug-ins that hook into it.
#
# Usage:
# ``from cura.API import CuraAPI
# api = CuraAPI()
# api.backups.createBackup()
# api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})``
class Backups: class Backups:
"""
The backups API provides a version-proof bridge between Cura's BackupManager and plugins that hook into it.
Usage:
from cura.API import CuraAPI
api = CuraAPI()
api.backups.createBackup()
api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})
"""
manager = BackupsManager() # Re-used instance of the backups manager. manager = BackupsManager() # Re-used instance of the backups manager.
## Create a new back-up using the BackupsManager.
# \return Tuple containing a ZIP file with the back-up data and a dict
# with metadata about the back-up.
def createBackup(self) -> (bytes, dict): def createBackup(self) -> (bytes, dict):
"""
Create a new backup using the BackupsManager.
:return: Tuple containing a ZIP file with the backup data and a dict with meta data about the backup.
"""
return self.manager.createBackup() return self.manager.createBackup()
## Restore a back-up using the BackupsManager.
# \param zip_file A ZIP file containing the actual back-up data.
# \param meta_data Some metadata needed for restoring a back-up, like the
# Cura version number.
def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None: def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None:
"""
Restore a backup using the BackupManager.
:param zip_file: A ZIP file containing the actual backup data.
:param meta_data: Some meta data needed for restoring a backup, like the Cura version number.
"""
return self.manager.restoreBackup(zip_file, meta_data) return self.manager.restoreBackup(zip_file, meta_data)

View file

@ -3,14 +3,13 @@
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
from cura.API.Backups import Backups from cura.API.Backups import Backups
## The official Cura API that plug-ins can use to interact with Cura.
#
# Python does not technically prevent talking to other classes as well, but
# this API provides a version-safe interface with proper deprecation warnings
# etc. Usage of any other methods than the ones provided in this API can cause
# plug-ins to be unstable.
class CuraAPI: class CuraAPI:
"""
The official Cura API that plugins can use to interact with Cura.
Python does not technically prevent talking to other classes as well,
but this API provides a version-safe interface with proper deprecation warnings etc.
Usage of any other methods than the ones provided in this API can cause plugins to be unstable.
"""
# For now we use the same API version to be consistent. # For now we use the same API version to be consistent.
VERSION = PluginRegistry.APIVersion VERSION = PluginRegistry.APIVersion

View file

@ -1,10 +1,12 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import List
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Logger import Logger from UM.Logger import Logger
from UM.Math.Polygon import Polygon from UM.Math.Polygon import Polygon
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from UM.Scene.SceneNode import SceneNode
from cura.Arranging.ShapeArray import ShapeArray from cura.Arranging.ShapeArray import ShapeArray
from cura.Scene import ZOffsetDecorator from cura.Scene import ZOffsetDecorator
@ -85,8 +87,7 @@ class Arrange:
# \param node # \param node
# \param offset_shape_arr ShapeArray with offset, for placing the shape # \param offset_shape_arr ShapeArray with offset, for placing the shape
# \param hull_shape_arr ShapeArray without offset, used to find location # \param hull_shape_arr ShapeArray without offset, used to find location
def findNodePlacement(self, node, offset_shape_arr, hull_shape_arr, step = 1): def findNodePlacement(self, node: SceneNode, offset_shape_arr: ShapeArray, hull_shape_arr: ShapeArray, step = 1):
new_node = copy.deepcopy(node)
best_spot = self.bestSpot( best_spot = self.bestSpot(
hull_shape_arr, start_prio = self._last_priority, step = step) hull_shape_arr, start_prio = self._last_priority, step = step)
x, y = best_spot.x, best_spot.y x, y = best_spot.x, best_spot.y
@ -95,21 +96,21 @@ class Arrange:
self._last_priority = best_spot.priority self._last_priority = best_spot.priority
# Ensure that the object is above the build platform # Ensure that the object is above the build platform
new_node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator) node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
if new_node.getBoundingBox(): if node.getBoundingBox():
center_y = new_node.getWorldPosition().y - new_node.getBoundingBox().bottom center_y = node.getWorldPosition().y - node.getBoundingBox().bottom
else: else:
center_y = 0 center_y = 0
if x is not None: # We could find a place if x is not None: # We could find a place
new_node.setPosition(Vector(x, center_y, y)) node.setPosition(Vector(x, center_y, y))
found_spot = True found_spot = True
self.place(x, y, offset_shape_arr) # place the object in arranger self.place(x, y, offset_shape_arr) # place the object in arranger
else: else:
Logger.log("d", "Could not find spot!"), Logger.log("d", "Could not find spot!"),
found_spot = False found_spot = False
new_node.setPosition(Vector(200, center_y, 100)) node.setPosition(Vector(200, center_y, 100))
return new_node, found_spot return found_spot
## Fill priority, center is best. Lower value is better ## Fill priority, center is best. Lower value is better
# This is a strategy for the arranger. # This is a strategy for the arranger.

View file

@ -17,12 +17,11 @@ from UM.Resources import Resources
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
## The back-up class holds all data about a back-up.
#
# It is also responsible for reading and writing the zip file to the user data
# folder.
class Backup: class Backup:
"""
The backup class holds all data about a backup.
It is also responsible for reading and writing the zip file to the user data folder.
"""
# These files should be ignored when making a backup. # These files should be ignored when making a backup.
IGNORED_FILES = [r"cura\.log", r"plugins\.json", r"cache", r"__pycache__", r"\.qmlc", r"\.pyc"] IGNORED_FILES = [r"cura\.log", r"plugins\.json", r"cache", r"__pycache__", r"\.qmlc", r"\.pyc"]
@ -33,10 +32,8 @@ class Backup:
self.zip_file = zip_file # type: Optional[bytes] self.zip_file = zip_file # type: Optional[bytes]
self.meta_data = meta_data # type: Optional[dict] self.meta_data = meta_data # type: Optional[dict]
## Create a back-up from the current user config folder.
def makeFromCurrent(self) -> (bool, Optional[str]): def makeFromCurrent(self) -> (bool, Optional[str]):
"""
Create a backup from the current user config folder.
"""
cura_release = CuraApplication.getInstance().getVersion() cura_release = CuraApplication.getInstance().getVersion()
version_data_dir = Resources.getDataStoragePath() version_data_dir = Resources.getDataStoragePath()
@ -75,12 +72,10 @@ class Backup:
"plugin_count": str(plugin_count) "plugin_count": str(plugin_count)
} }
## Make a full archive from the given root path with the given name.
# \param root_path The root directory to archive recursively.
# \return The archive as bytes.
def _makeArchive(self, buffer: "io.BytesIO", root_path: str) -> Optional[ZipFile]: def _makeArchive(self, buffer: "io.BytesIO", root_path: str) -> Optional[ZipFile]:
"""
Make a full archive from the given root path with the given name.
:param root_path: The root directory to archive recursively.
:return: The archive as bytes.
"""
ignore_string = re.compile("|".join(self.IGNORED_FILES)) ignore_string = re.compile("|".join(self.IGNORED_FILES))
try: try:
archive = ZipFile(buffer, "w", ZIP_DEFLATED) archive = ZipFile(buffer, "w", ZIP_DEFLATED)
@ -99,15 +94,13 @@ class Backup:
"Could not create archive from user data directory: {}".format(error))) "Could not create archive from user data directory: {}".format(error)))
return None return None
## Show a UI message.
def _showMessage(self, message: str) -> None: def _showMessage(self, message: str) -> None:
"""Show a UI message"""
Message(message, title=self.catalog.i18nc("@info:title", "Backup"), lifetime=30).show() Message(message, title=self.catalog.i18nc("@info:title", "Backup"), lifetime=30).show()
## Restore this back-up.
# \return Whether we had success or not.
def restore(self) -> bool: def restore(self) -> bool:
"""
Restore this backups
:return: A boolean whether we had success or not.
"""
if not self.zip_file or not self.meta_data or not self.meta_data.get("cura_release", None): if not self.zip_file or not self.meta_data or not self.meta_data.get("cura_release", None):
# We can restore without the minimum required information. # We can restore without the minimum required information.
Logger.log("w", "Tried to restore a Cura backup without having proper data or meta data.") Logger.log("w", "Tried to restore a Cura backup without having proper data or meta data.")
@ -140,14 +133,12 @@ class Backup:
return extracted return extracted
## Extract the whole archive to the given target path.
# \param archive The archive as ZipFile.
# \param target_path The target path.
# \return Whether we had success or not.
@staticmethod @staticmethod
def _extractArchive(archive: "ZipFile", target_path: str) -> bool: def _extractArchive(archive: "ZipFile", target_path: str) -> bool:
"""
Extract the whole archive to the given target path.
:param archive: The archive as ZipFile.
:param target_path: The target path.
:return: A boolean whether we had success or not.
"""
Logger.log("d", "Removing current data in location: %s", target_path) Logger.log("d", "Removing current data in location: %s", target_path)
Resources.factoryReset() Resources.factoryReset()
Logger.log("d", "Extracting backup to location: %s", target_path) Logger.log("d", "Extracting backup to location: %s", target_path)

View file

@ -7,19 +7,18 @@ from cura.Backups.Backup import Backup
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
## The BackupsManager is responsible for managing the creating and restoring of
# back-ups.
#
# Back-ups themselves are represented in a different class.
class BackupsManager: class BackupsManager:
"""
The BackupsManager is responsible for managing the creating and restoring of backups.
Backups themselves are represented in a different class.
"""
def __init__(self): def __init__(self):
self._application = CuraApplication.getInstance() self._application = CuraApplication.getInstance()
## Get a back-up of the current configuration.
# \return A tuple containing a ZipFile (the actual back-up) and a dict
# containing some metadata (like version).
def createBackup(self) -> (Optional[bytes], Optional[dict]): def createBackup(self) -> (Optional[bytes], Optional[dict]):
"""
Get a backup of the current configuration.
:return: A Tuple containing a ZipFile (the actual backup) and a dict containing some meta data (like version).
"""
self._disableAutoSave() self._disableAutoSave()
backup = Backup() backup = Backup()
backup.makeFromCurrent() backup.makeFromCurrent()
@ -27,12 +26,11 @@ class BackupsManager:
# We don't return a Backup here because we want plugins only to interact with our API and not full objects. # We don't return a Backup here because we want plugins only to interact with our API and not full objects.
return backup.zip_file, backup.meta_data return backup.zip_file, backup.meta_data
## Restore a back-up from a given ZipFile.
# \param zip_file A bytes object containing the actual back-up.
# \param meta_data A dict containing some metadata that is needed to
# restore the back-up correctly.
def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None: def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None:
"""
Restore a backup from a given ZipFile.
:param zip_file: A bytes object containing the actual backup.
:param meta_data: A dict containing some meta data that is needed to restore the backup correctly.
"""
if not meta_data.get("cura_release", None): if not meta_data.get("cura_release", None):
# If there is no "cura_release" specified in the meta data, we don't execute a backup restore. # If there is no "cura_release" specified in the meta data, we don't execute a backup restore.
Logger.log("w", "Tried to restore a backup without specifying a Cura version number.") Logger.log("w", "Tried to restore a backup without specifying a Cura version number.")
@ -47,10 +45,11 @@ class BackupsManager:
# We don't want to store the data at this point as that would override the just-restored backup. # We don't want to store the data at this point as that would override the just-restored backup.
self._application.windowClosed(save_data=False) self._application.windowClosed(save_data=False)
## Here we try to disable the auto-save plug-in as it might interfere with
# restoring a back-up.
def _disableAutoSave(self): def _disableAutoSave(self):
"""Here we try to disable the auto-save plugin as it might interfere with restoring a backup."""
self._application.setSaveDataEnabled(False) self._application.setSaveDataEnabled(False)
## Re-enable auto-save after we're done.
def _enableAutoSave(self): def _enableAutoSave(self):
"""Re-enable auto-save after we're done."""
self._application.setSaveDataEnabled(True) self._application.setSaveDataEnabled(True)

View file

@ -225,6 +225,8 @@ class CuraApplication(QtApplication):
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
self._container_registry_class = CuraContainerRegistry self._container_registry_class = CuraContainerRegistry
from cura.CuraPackageManager import CuraPackageManager
self._package_manager_class = CuraPackageManager
# Adds command line options to the command line parser. This should be called after the application is created and # Adds command line options to the command line parser. This should be called after the application is created and
# before the pre-start. # before the pre-start.
@ -511,7 +513,6 @@ class CuraApplication(QtApplication):
preferences.addPreference("cura/asked_dialog_on_project_save", False) preferences.addPreference("cura/asked_dialog_on_project_save", False)
preferences.addPreference("cura/choice_on_profile_override", "always_ask") preferences.addPreference("cura/choice_on_profile_override", "always_ask")
preferences.addPreference("cura/choice_on_open_project", "always_ask") preferences.addPreference("cura/choice_on_open_project", "always_ask")
preferences.addPreference("cura/not_arrange_objects_on_load", False)
preferences.addPreference("cura/use_multi_build_plate", False) preferences.addPreference("cura/use_multi_build_plate", False)
preferences.addPreference("cura/currency", "") preferences.addPreference("cura/currency", "")
@ -1601,9 +1602,7 @@ class CuraApplication(QtApplication):
self._currently_loading_files.remove(filename) self._currently_loading_files.remove(filename)
self.fileLoaded.emit(filename) self.fileLoaded.emit(filename)
arrange_objects_on_load = ( arrange_objects_on_load = not self.getPreferences().getValue("cura/use_multi_build_plate")
not self.getPreferences().getValue("cura/use_multi_build_plate") or
not self.getPreferences().getValue("cura/not_arrange_objects_on_load"))
target_build_plate = self.getMultiBuildPlateModel().activeBuildPlate if arrange_objects_on_load else -1 target_build_plate = self.getMultiBuildPlateModel().activeBuildPlate if arrange_objects_on_load else -1
root = self.getController().getScene().getRoot() root = self.getController().getScene().getRoot()
@ -1676,7 +1675,7 @@ class CuraApplication(QtApplication):
return return
# Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher # Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher
node, _ = arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step = 10) arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step = 10)
# This node is deep copied from some other node which already has a BuildPlateDecorator, but the deepcopy # This node is deep copied from some other node which already has a BuildPlateDecorator, but the deepcopy
# of BuildPlateDecorator produces one that's associated with build plate -1. So, here we need to check if # of BuildPlateDecorator produces one that's associated with build plate -1. So, here we need to check if

View file

@ -7,8 +7,11 @@ from UM.Resources import Resources #To find storage paths for some resource type
class CuraPackageManager(PackageManager): class CuraPackageManager(PackageManager):
def __init__(self, parent = None): def __init__(self, application, parent = None):
super().__init__(parent) super().__init__(application, parent)
def initialize(self):
self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer) self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer)
self._installation_dirs_dict["qualities"] = Resources.getStoragePath(CuraApplication.ResourceTypes.QualityInstanceContainer) self._installation_dirs_dict["qualities"] = Resources.getStoragePath(CuraApplication.ResourceTypes.QualityInstanceContainer)
super().initialize()

View file

@ -47,22 +47,38 @@ class BrandMaterialsModel(ListModel):
self.addRoleName(self.MaterialsRole, "materials") self.addRoleName(self.MaterialsRole, "materials")
self._extruder_position = 0 self._extruder_position = 0
self._extruder_stack = None
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
self._machine_manager = CuraApplication.getInstance().getMachineManager() self._machine_manager = CuraApplication.getInstance().getMachineManager()
self._extruder_manager = CuraApplication.getInstance().getExtruderManager() self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
self._material_manager = CuraApplication.getInstance().getMaterialManager() self._material_manager = CuraApplication.getInstance().getMaterialManager()
self._machine_manager.globalContainerChanged.connect(self._updateExtruderStack)
self._machine_manager.activeStackChanged.connect(self._update) #Update when switching machines. self._machine_manager.activeStackChanged.connect(self._update) #Update when switching machines.
self._material_manager.materialsUpdated.connect(self._update) #Update when the list of materials changes. self._material_manager.materialsUpdated.connect(self._update) #Update when the list of materials changes.
self._update() self._update()
def _updateExtruderStack(self):
global_stack = self._machine_manager.activeMachine
if global_stack is None:
return
if self._extruder_stack is not None:
self._extruder_stack.pyqtContainersChanged.disconnect(self._update)
self._extruder_stack = global_stack.extruders.get(str(self._extruder_position))
if self._extruder_stack is not None:
self._extruder_stack.pyqtContainersChanged.connect(self._update)
# Force update the model when the extruder stack changes
self._update()
def setExtruderPosition(self, position: int): def setExtruderPosition(self, position: int):
if self._extruder_position != position: if self._extruder_stack is None or self._extruder_position != position:
self._extruder_position = position self._extruder_position = position
self._updateExtruderStack()
self.extruderPositionChanged.emit() self.extruderPositionChanged.emit()
@pyqtProperty(int, fset = setExtruderPosition, notify = extruderPositionChanged) @pyqtProperty(int, fset=setExtruderPosition, notify=extruderPositionChanged)
def extruderPosition(self) -> int: def extruderPosition(self) -> int:
return self._extruder_position return self._extruder_position

View file

@ -64,10 +64,11 @@ class MultiplyObjectsJob(Job):
arranger.resetLastPriority() arranger.resetLastPriority()
for i in range(self._count): for i in range(self._count):
# We do place the nodes one by one, as we want to yield in between. # We do place the nodes one by one, as we want to yield in between.
new_node = copy.deepcopy(node)
solution_found = False
if not node_too_big: if not node_too_big:
new_node, solution_found = arranger.findNodePlacement(current_node, offset_shape_arr, hull_shape_arr) solution_found = arranger.findNodePlacement(new_node, offset_shape_arr, hull_shape_arr)
else:
new_node = copy.deepcopy(node)
if node_too_big or not solution_found: if node_too_big or not solution_found:
found_solution_for_all = False found_solution_for_all = False
new_location = new_node.getPosition() new_location = new_node.getPosition()

View file

@ -8,6 +8,7 @@ from UM.Scene.SceneNode import SceneNode
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from UM.Scene.Selection import Selection from UM.Scene.Selection import Selection
from UM.Scene.SceneNodeSettings import SceneNodeSettings
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
@ -80,6 +81,10 @@ class PlatformPhysics:
# only push away objects if this node is a printing mesh # only push away objects if this node is a printing mesh
if not node.callDecoration("isNonPrintingMesh") and Application.getInstance().getPreferences().getValue("physics/automatic_push_free"): if not node.callDecoration("isNonPrintingMesh") and Application.getInstance().getPreferences().getValue("physics/automatic_push_free"):
# Do not move locked nodes
if node.getSetting(SceneNodeSettings.LockPosition):
continue
# Check for collisions between convex hulls # Check for collisions between convex hulls
for other_node in BreadthFirstIterator(root): for other_node in BreadthFirstIterator(root):
# Ignore root, ourselves and anything that is not a normal SceneNode. # Ignore root, ourselves and anything that is not a normal SceneNode.

View file

@ -42,6 +42,7 @@ class ContainerManager(QObject):
self._container_registry = self._application.getContainerRegistry() self._container_registry = self._application.getContainerRegistry()
self._machine_manager = self._application.getMachineManager() self._machine_manager = self._application.getMachineManager()
self._material_manager = self._application.getMaterialManager() self._material_manager = self._application.getMaterialManager()
self._quality_manager = self._application.getQualityManager()
self._container_name_filters = {} self._container_name_filters = {}
@pyqtSlot(str, str, result=str) @pyqtSlot(str, str, result=str)
@ -312,11 +313,19 @@ class ContainerManager(QObject):
self._machine_manager.blurSettings.emit() self._machine_manager.blurSettings.emit()
global_stack = self._machine_manager.activeMachine current_quality_changes_name = global_stack.qualityChanges.getName()
current_quality_type = global_stack.quality.getMetaDataEntry("quality_type")
extruder_stacks = list(global_stack.extruders.values()) extruder_stacks = list(global_stack.extruders.values())
for stack in [global_stack] + extruder_stacks: for stack in [global_stack] + extruder_stacks:
# Find the quality_changes container for this stack and merge the contents of the top container into it. # Find the quality_changes container for this stack and merge the contents of the top container into it.
quality_changes = stack.qualityChanges quality_changes = stack.qualityChanges
if quality_changes.getId() == "empty_quality_changes":
quality_changes = self._quality_manager._createQualityChanges(current_quality_type, current_quality_changes_name,
global_stack, stack)
self._container_registry.addContainer(quality_changes)
stack.qualityChanges = quality_changes
if not quality_changes or self._container_registry.isReadOnly(quality_changes.getId()): if not quality_changes or self._container_registry.isReadOnly(quality_changes.getId()):
Logger.log("e", "Could not update quality of a nonexistant or read only quality profile in stack %s", stack.getId()) Logger.log("e", "Could not update quality of a nonexistant or read only quality profile in stack %s", stack.getId())
continue continue

View file

@ -461,10 +461,6 @@ class ExtruderManager(QObject):
if global_stack.definitionChanges.hasProperty(key, "value"): if global_stack.definitionChanges.hasProperty(key, "value"):
global_stack.definitionChanges.removeInstance(key, postpone_emit = True) global_stack.definitionChanges.removeInstance(key, postpone_emit = True)
# Update material diameter for extruders
for position in extruder_positions_to_update:
self.updateMaterialForDiameter(position, global_stack = global_stack)
## Get all extruder values for a certain setting. ## Get all extruder values for a certain setting.
# #
# This is exposed to SettingFunction so it can be used in value functions. # This is exposed to SettingFunction so it can be used in value functions.
@ -556,96 +552,6 @@ class ExtruderManager(QObject):
def getInstanceExtruderValues(self, key): def getInstanceExtruderValues(self, key):
return ExtruderManager.getExtruderValues(key) return ExtruderManager.getExtruderValues(key)
## Updates the material container to a material that matches the material diameter set for the printer
def updateMaterialForDiameter(self, extruder_position: int, global_stack = None):
if not global_stack:
global_stack = Application.getInstance().getGlobalContainerStack()
if not global_stack:
return
if not global_stack.getMetaDataEntry("has_materials", False):
return
extruder_stack = global_stack.extruders[str(extruder_position)]
material_diameter = extruder_stack.material.getProperty("material_diameter", "value")
if not material_diameter:
# in case of "empty" material
material_diameter = 0
material_approximate_diameter = str(round(material_diameter))
material_diameter = extruder_stack.definitionChanges.getProperty("material_diameter", "value")
setting_provider = extruder_stack
if not material_diameter:
if extruder_stack.definition.hasProperty("material_diameter", "value"):
material_diameter = extruder_stack.definition.getProperty("material_diameter", "value")
else:
material_diameter = global_stack.definition.getProperty("material_diameter", "value")
setting_provider = global_stack
if isinstance(material_diameter, SettingFunction):
material_diameter = material_diameter(setting_provider)
machine_approximate_diameter = str(round(material_diameter))
if material_approximate_diameter != machine_approximate_diameter:
Logger.log("i", "The the currently active material(s) do not match the diameter set for the printer. Finding alternatives.")
if global_stack.getMetaDataEntry("has_machine_materials", False):
materials_definition = global_stack.definition.getId()
has_material_variants = global_stack.getMetaDataEntry("has_variants", False)
else:
materials_definition = "fdmprinter"
has_material_variants = False
old_material = extruder_stack.material
search_criteria = {
"type": "material",
"approximate_diameter": machine_approximate_diameter,
"material": old_material.getMetaDataEntry("material", "value"),
"brand": old_material.getMetaDataEntry("brand", "value"),
"supplier": old_material.getMetaDataEntry("supplier", "value"),
"color_name": old_material.getMetaDataEntry("color_name", "value"),
"definition": materials_definition
}
if has_material_variants:
search_criteria["variant"] = extruder_stack.variant.getId()
container_registry = Application.getInstance().getContainerRegistry()
empty_material = container_registry.findInstanceContainers(id = "empty_material")[0]
if old_material == empty_material:
search_criteria.pop("material", None)
search_criteria.pop("supplier", None)
search_criteria.pop("brand", None)
search_criteria.pop("definition", None)
search_criteria["id"] = extruder_stack.getMetaDataEntry("preferred_material")
materials = container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Same material with new diameter is not found, search for generic version of the same material type
search_criteria.pop("supplier", None)
search_criteria.pop("brand", None)
search_criteria["color_name"] = "Generic"
materials = container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Generic material with new diameter is not found, search for preferred material
search_criteria.pop("color_name", None)
search_criteria.pop("material", None)
search_criteria["id"] = extruder_stack.getMetaDataEntry("preferred_material")
materials = container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Preferred material with new diameter is not found, search for any material
search_criteria.pop("id", None)
materials = container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Just use empty material as a final fallback
materials = [empty_material]
Logger.log("i", "Selecting new material: %s", materials[0].getId())
extruder_stack.material = materials[0]
## Get the value for a setting from a specific extruder. ## Get the value for a setting from a specific extruder.
# #
# This is exposed to SettingFunction to use in value functions. # This is exposed to SettingFunction to use in value functions.

View file

@ -306,6 +306,11 @@ class MachineManager(QObject):
for position, extruder in global_stack.extruders.items(): for position, extruder in global_stack.extruders.items():
material_dict[position] = extruder.material.getMetaDataEntry("base_file") material_dict[position] = extruder.material.getMetaDataEntry("base_file")
self._current_root_material_id = material_dict self._current_root_material_id = material_dict
# Update materials to make sure that the diameters match with the machine's
for position in global_stack.extruders:
self.updateMaterialWithVariant(position)
global_quality = global_stack.quality global_quality = global_stack.quality
quality_type = global_quality.getMetaDataEntry("quality_type") quality_type = global_quality.getMetaDataEntry("quality_type")
global_quality_changes = global_stack.qualityChanges global_quality_changes = global_stack.qualityChanges
@ -1200,7 +1205,7 @@ class MachineManager(QObject):
current_quality_type, quality_type) current_quality_type, quality_type)
self._setQualityGroup(candidate_quality_groups[quality_type], empty_quality_changes = True) self._setQualityGroup(candidate_quality_groups[quality_type], empty_quality_changes = True)
def _updateMaterialWithVariant(self, position: Optional[str]): def updateMaterialWithVariant(self, position: Optional[str]):
if self._global_container_stack is None: if self._global_container_stack is None:
return return
if position is None: if position is None:
@ -1286,7 +1291,7 @@ class MachineManager(QObject):
self._setMaterial(position, material_container_node) self._setMaterial(position, material_container_node)
else: else:
self._global_container_stack.extruders[position].material = self._empty_material_container self._global_container_stack.extruders[position].material = self._empty_material_container
self._updateMaterialWithVariant(position) self.updateMaterialWithVariant(position)
if configuration.buildplateConfiguration is not None: if configuration.buildplateConfiguration is not None:
global_variant_container_node = self._variant_manager.getBuildplateVariantNode(self._global_container_stack.definition.getId(), configuration.buildplateConfiguration) global_variant_container_node = self._variant_manager.getBuildplateVariantNode(self._global_container_stack.definition.getId(), configuration.buildplateConfiguration)
@ -1332,7 +1337,7 @@ class MachineManager(QObject):
self.blurSettings.emit() self.blurSettings.emit()
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
self._setGlobalVariant(container_node) self._setGlobalVariant(container_node)
self._updateMaterialWithVariant(None) # Update all materials self.updateMaterialWithVariant(None) # Update all materials
self._updateQualityWithMaterial() self._updateQualityWithMaterial()
@pyqtSlot(str, str) @pyqtSlot(str, str)
@ -1369,7 +1374,7 @@ class MachineManager(QObject):
self.blurSettings.emit() self.blurSettings.emit()
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
self._setVariantNode(position, container_node) self._setVariantNode(position, container_node)
self._updateMaterialWithVariant(position) self.updateMaterialWithVariant(position)
self._updateQualityWithMaterial() self._updateQualityWithMaterial()
# See if we need to show the Discard or Keep changes screen # See if we need to show the Discard or Keep changes screen
@ -1433,5 +1438,5 @@ class MachineManager(QObject):
if self._global_container_stack is None: if self._global_container_stack is None:
return return
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
self._updateMaterialWithVariant(None) self.updateMaterialWithVariant(None)
self._updateQualityWithMaterial() self._updateQualityWithMaterial()

View file

@ -63,7 +63,7 @@ class SettingOverrideDecorator(SceneNodeDecorator):
instance_container = copy.deepcopy(self._stack.getContainer(0), memo) instance_container = copy.deepcopy(self._stack.getContainer(0), memo)
# A unique name must be added, or replaceContainer will not replace it # A unique name must be added, or replaceContainer will not replace it
instance_container.setMetaDataEntry("id", self._generateUniqueName) instance_container.setMetaDataEntry("id", self._generateUniqueName())
## Set the copied instance as the first (and only) instance container of the stack. ## Set the copied instance as the first (and only) instance container of the stack.
deep_copy._stack.replaceContainer(0, instance_container) deep_copy._stack.replaceContainer(0, instance_container)

View file

@ -13,6 +13,7 @@ from UM.Workspace.WorkspaceReader import WorkspaceReader
from UM.Application import Application from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Signal import postponeSignals, CompressTechnique from UM.Signal import postponeSignals, CompressTechnique
from UM.Settings.ContainerFormatError import ContainerFormatError from UM.Settings.ContainerFormatError import ContainerFormatError
@ -470,6 +471,20 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
Logger.log("w", "File %s is not a valid workspace.", file_name) Logger.log("w", "File %s is not a valid workspace.", file_name)
return WorkspaceReader.PreReadResult.failed return WorkspaceReader.PreReadResult.failed
# Check if the machine definition exists. If not, indicate failure because we do not import definition files.
def_results = self._container_registry.findDefinitionContainersMetadata(id = machine_definition_id)
if not def_results:
message = Message(i18n_catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!",
"Project file <filename>{0}</filename> contains an unknown machine type"
" <message>{1}</message>. Cannot import the machine."
" Models will be imported instead.", file_name, machine_definition_id),
title = i18n_catalog.i18nc("@info:title", "Open Project File"))
message.show()
Logger.log("i", "Could unknown machine definition %s in project file %s, cannot import it.",
self._machine_info.definition_id, file_name)
return WorkspaceReader.PreReadResult.failed
# In case we use preRead() to check if a file is a valid project file, we don't want to show a dialog. # In case we use preRead() to check if a file is a valid project file, we don't want to show a dialog.
if not show_dialog: if not show_dialog:
return WorkspaceReader.PreReadResult.accepted return WorkspaceReader.PreReadResult.accepted

View file

@ -55,7 +55,7 @@ class ChangeLog(Extension, QObject,):
def loadChangeLogs(self): def loadChangeLogs(self):
self._change_logs = collections.OrderedDict() self._change_logs = collections.OrderedDict()
with open(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.txt"), "r",-1, "utf-8") as f: with open(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.txt"), "r", encoding = "utf-8") as f:
open_version = None open_version = None
open_header = "" # Initialise to an empty header in case there is no "*" in the first line of the changelog open_header = "" # Initialise to an empty header in case there is no "*" in the first line of the changelog
for line in f: for line in f:

View file

@ -379,6 +379,14 @@ class CuraEngineBackend(QObject, Backend):
else: else:
self.backendStateChange.emit(BackendState.NotStarted) self.backendStateChange.emit(BackendState.NotStarted)
if job.getResult() == StartSliceJob.StartJobResult.ObjectsWithDisabledExtruder:
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because there are objects associated with disabled Extruder %s." % job.getMessage()),
title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
self.backendStateChange.emit(BackendState.Error)
self.backendError.emit(job)
return
if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice: if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice:
if Application.getInstance().platformActivity: if Application.getInstance().platformActivity:
self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."), self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."),

View file

@ -32,6 +32,7 @@ class StartJobResult(IntEnum):
MaterialIncompatible = 5 MaterialIncompatible = 5
BuildPlateError = 6 BuildPlateError = 6
ObjectSettingError = 7 #When an error occurs in per-object settings. ObjectSettingError = 7 #When an error occurs in per-object settings.
ObjectsWithDisabledExtruder = 8
## Formatter class that handles token expansion in start/end gcod ## Formatter class that handles token expansion in start/end gcod
@ -213,16 +214,27 @@ class StartSliceJob(Job):
extruders_enabled = {position: stack.isEnabled for position, stack in Application.getInstance().getGlobalContainerStack().extruders.items()} extruders_enabled = {position: stack.isEnabled for position, stack in Application.getInstance().getGlobalContainerStack().extruders.items()}
filtered_object_groups = [] filtered_object_groups = []
has_model_with_disabled_extruders = False
associated_siabled_extruders = set()
for group in object_groups: for group in object_groups:
stack = Application.getInstance().getGlobalContainerStack() stack = Application.getInstance().getGlobalContainerStack()
skip_group = False skip_group = False
for node in group: for node in group:
if not extruders_enabled[node.callDecoration("getActiveExtruderPosition")]: extruder_position = node.callDecoration("getActiveExtruderPosition")
if not extruders_enabled[extruder_position]:
skip_group = True skip_group = True
has_model_with_disabled_extruders = True
associated_siabled_extruders.add(extruder_position)
break break
if not skip_group: if not skip_group:
filtered_object_groups.append(group) filtered_object_groups.append(group)
if has_model_with_disabled_extruders:
self.setResult(StartJobResult.ObjectsWithDisabledExtruder)
associated_siabled_extruders = [str(c) for c in sorted([int(p) + 1 for p in associated_siabled_extruders])]
self.setMessage(", ".join(associated_siabled_extruders))
return
# There are cases when there is nothing to slice. This can happen due to one at a time slicing not being # There are cases when there is nothing to slice. This can happen due to one at a time slicing not being
# able to find a possible sequence or because there are no objects on the build plate (or they are outside # able to find a possible sequence or because there are no objects on the build plate (or they are outside
# the build volume) # the build volume)

View file

@ -57,7 +57,7 @@ class GCodeProfileReader(ProfileReader):
# TODO: Consider moving settings to the start? # TODO: Consider moving settings to the start?
serialized = "" # Will be filled with the serialized profile. serialized = "" # Will be filled with the serialized profile.
try: try:
with open(file_name, "r") as f: with open(file_name, "r", encoding = "utf-8") as f:
for line in f: for line in f:
if line.startswith(prefix): if line.startswith(prefix):
# Remove the prefix and the newline from the line and add it to the rest. # Remove the prefix and the newline from the line and add it to the rest.

View file

@ -100,7 +100,7 @@ class LegacyProfileReader(ProfileReader):
return None return None
try: try:
with open(os.path.join(PluginRegistry.getInstance().getPluginPath("LegacyProfileReader"), "DictionaryOfDoom.json"), "r", -1, "utf-8") as f: with open(os.path.join(PluginRegistry.getInstance().getPluginPath("LegacyProfileReader"), "DictionaryOfDoom.json"), "r", encoding = "utf-8") as f:
dict_of_doom = json.load(f) # Parse the Dictionary of Doom. dict_of_doom = json.load(f) # Parse the Dictionary of Doom.
except IOError as e: except IOError as e:
Logger.log("e", "Could not open DictionaryOfDoom.json for reading: %s", str(e)) Logger.log("e", "Could not open DictionaryOfDoom.json for reading: %s", str(e))

View file

@ -158,4 +158,4 @@ class MachineSettingsAction(MachineAction):
@pyqtSlot(int) @pyqtSlot(int)
def updateMaterialForDiameter(self, extruder_position: int): def updateMaterialForDiameter(self, extruder_position: int):
# Updates the material container to a material that matches the material diameter set for the printer # Updates the material container to a material that matches the material diameter set for the printer
self._application.getExtruderManager().updateMaterialForDiameter(extruder_position) self._application.getMachineManager().updateMaterialWithVariant(str(extruder_position))

View file

@ -53,7 +53,7 @@ UM.PointingRectangle {
verticalCenter: parent.verticalCenter verticalCenter: parent.verticalCenter
} }
width: 40 * screenScaleFactor width: maximumValue.toString().length * 12 * screenScaleFactor
text: sliderLabelRoot.value + startFrom // the current handle value, add 1 because layers is an array text: sliderLabelRoot.value + startFrom // the current handle value, add 1 because layers is an array
horizontalAlignment: TextInput.AlignRight horizontalAlignment: TextInput.AlignRight
@ -77,11 +77,12 @@ UM.PointingRectangle {
if (valueLabel.text != "") { if (valueLabel.text != "") {
// -startFrom because we need to convert back to an array structure // -startFrom because we need to convert back to an array structure
sliderLabelRoot.setValue(parseInt(valueLabel.text) - startFrom) sliderLabelRoot.setValue(parseInt(valueLabel.text) - startFrom)
} }
} }
validator: IntValidator { validator: IntValidator {
bottom:startFrom bottom: startFrom
top: sliderLabelRoot.maximumValue + startFrom // +startFrom because maybe we want to start in a different value rather than 0 top: sliderLabelRoot.maximumValue + startFrom // +startFrom because maybe we want to start in a different value rather than 0
} }
} }

View file

@ -262,12 +262,14 @@ class Toolbox(QObject, Extension):
# list of old plugins # list of old plugins
old_plugin_ids = self._plugin_registry.getInstalledPlugins() old_plugin_ids = self._plugin_registry.getInstalledPlugins()
installed_package_ids = self._package_manager.getAllInstalledPackageIDs() installed_package_ids = self._package_manager.getAllInstalledPackageIDs()
scheduled_to_remove_package_ids = self._package_manager.getToRemovePackageIDs()
self._old_plugin_ids = [] self._old_plugin_ids = []
self._old_plugin_metadata = [] self._old_plugin_metadata = []
for plugin_id in old_plugin_ids: for plugin_id in old_plugin_ids:
if plugin_id not in installed_package_ids: # Neither the installed packages nor the packages that are scheduled to remove are old plugins
if plugin_id not in installed_package_ids and plugin_id not in scheduled_to_remove_package_ids:
Logger.log('i', 'Found a plugin that was installed with the old plugin browser: %s', plugin_id) Logger.log('i', 'Found a plugin that was installed with the old plugin browser: %s', plugin_id)
old_metadata = self._plugin_registry.getMetaData(plugin_id) old_metadata = self._plugin_registry.getMetaData(plugin_id)

View file

@ -13,7 +13,7 @@ def readHex(filename):
""" """
data = [] data = []
extra_addr = 0 extra_addr = 0
f = io.open(filename, "r") f = io.open(filename, "r", encoding = "utf-8")
for line in f: for line in f:
line = line.strip() line = line.strip()
if len(line) < 1: if len(line) < 1:

View file

@ -94,7 +94,7 @@ class VersionUpgrade22to24(VersionUpgrade):
if variant_path.endswith("_variant.inst.cfg"): if variant_path.endswith("_variant.inst.cfg"):
variant_path = variant_path[:-len("_variant.inst.cfg")] + "_settings.inst.cfg" variant_path = variant_path[:-len("_variant.inst.cfg")] + "_settings.inst.cfg"
with open(os.path.join(machine_instances_dir, os.path.basename(variant_path)), "w") as fp: with open(os.path.join(machine_instances_dir, os.path.basename(variant_path)), "w", encoding = "utf-8") as fp:
variant_config.write(fp) variant_config.write(fp)
return config_name return config_name
@ -105,9 +105,9 @@ class VersionUpgrade22to24(VersionUpgrade):
result = [] result = []
for entry in os.scandir(variants_dir): for entry in os.scandir(variants_dir):
if entry.name.endswith('.inst.cfg') and entry.is_file(): if entry.name.endswith(".inst.cfg") and entry.is_file():
config = configparser.ConfigParser(interpolation = None) config = configparser.ConfigParser(interpolation = None)
with open(entry.path, "r") as fhandle: with open(entry.path, "r", encoding = "utf-8") as fhandle:
config.read_file(fhandle) config.read_file(fhandle)
if config.has_section("general") and config.has_option("general", "name"): if config.has_section("general") and config.has_option("general", "name"):
result.append( { "path": entry.path, "name": config.get("general", "name") } ) result.append( { "path": entry.path, "name": config.get("general", "name") } )

View file

@ -249,11 +249,11 @@ class VersionUpgrade25to26(VersionUpgrade):
definition_changes_dir = Resources.getPath(CuraApplication.ResourceTypes.DefinitionChangesContainer) definition_changes_dir = Resources.getPath(CuraApplication.ResourceTypes.DefinitionChangesContainer)
user_settings_dir = Resources.getPath(CuraApplication.ResourceTypes.UserInstanceContainer) user_settings_dir = Resources.getPath(CuraApplication.ResourceTypes.UserInstanceContainer)
with open(os.path.join(definition_changes_dir, definition_changes_filename), "w") as f: with open(os.path.join(definition_changes_dir, definition_changes_filename), "w", encoding = "utf-8") as f:
f.write(definition_changes_output.getvalue()) f.write(definition_changes_output.getvalue())
with open(os.path.join(user_settings_dir, user_settings_filename), "w") as f: with open(os.path.join(user_settings_dir, user_settings_filename), "w", encoding = "utf-8") as f:
f.write(user_settings_output.getvalue()) f.write(user_settings_output.getvalue())
with open(os.path.join(extruder_stack_dir, extruder_filename), "w") as f: with open(os.path.join(extruder_stack_dir, extruder_filename), "w", encoding = "utf-8") as f:
f.write(extruder_output.getvalue()) f.write(extruder_output.getvalue())
## Creates a definition changes container which doesn't contain anything for the Custom FDM Printers. ## Creates a definition changes container which doesn't contain anything for the Custom FDM Printers.

View file

@ -650,35 +650,36 @@ class XmlMaterialProfile(InstanceContainer):
machine_manufacturer = identifier.get("manufacturer", definition.get("manufacturer", "Unknown")) #If the XML material doesn't specify a manufacturer, use the one in the actual printer definition. machine_manufacturer = identifier.get("manufacturer", definition.get("manufacturer", "Unknown")) #If the XML material doesn't specify a manufacturer, use the one in the actual printer definition.
if machine_compatibility: # Always create the instance of the material even if it is not compatible, otherwise it will never
new_material_id = self.getId() + "_" + machine_id # show as incompatible if the material profile doesn't define hotends in the machine - CURA-5444
new_material_id = self.getId() + "_" + machine_id
# The child or derived material container may already exist. This can happen when a material in a # The child or derived material container may already exist. This can happen when a material in a
# project file and the a material in Cura have the same ID. # project file and the a material in Cura have the same ID.
# In the case if a derived material already exists, override that material container because if # In the case if a derived material already exists, override that material container because if
# the data in the parent material has been changed, the derived ones should be updated too. # the data in the parent material has been changed, the derived ones should be updated too.
if ContainerRegistry.getInstance().isLoaded(new_material_id): if ContainerRegistry.getInstance().isLoaded(new_material_id):
new_material = ContainerRegistry.getInstance().findContainers(id = new_material_id)[0] new_material = ContainerRegistry.getInstance().findContainers(id = new_material_id)[0]
is_new_material = False is_new_material = False
else: else:
new_material = XmlMaterialProfile(new_material_id) new_material = XmlMaterialProfile(new_material_id)
is_new_material = True is_new_material = True
new_material.setMetaData(copy.deepcopy(self.getMetaData())) new_material.setMetaData(copy.deepcopy(self.getMetaData()))
new_material.getMetaData()["id"] = new_material_id new_material.getMetaData()["id"] = new_material_id
new_material.getMetaData()["name"] = self.getName() new_material.getMetaData()["name"] = self.getName()
new_material.setDefinition(machine_id) new_material.setDefinition(machine_id)
# Don't use setMetadata, as that overrides it for all materials with same base file # Don't use setMetadata, as that overrides it for all materials with same base file
new_material.getMetaData()["compatible"] = machine_compatibility new_material.getMetaData()["compatible"] = machine_compatibility
new_material.getMetaData()["machine_manufacturer"] = machine_manufacturer new_material.getMetaData()["machine_manufacturer"] = machine_manufacturer
new_material.getMetaData()["definition"] = machine_id new_material.getMetaData()["definition"] = machine_id
new_material.setCachedValues(cached_machine_setting_properties) new_material.setCachedValues(cached_machine_setting_properties)
new_material._dirty = False new_material._dirty = False
if is_new_material: if is_new_material:
containers_to_add.append(new_material) containers_to_add.append(new_material)
# Find the buildplates compatibility # Find the buildplates compatibility
buildplates = machine.iterfind("./um:buildplate", self.__namespaces) buildplates = machine.iterfind("./um:buildplate", self.__namespaces)
@ -898,22 +899,23 @@ class XmlMaterialProfile(InstanceContainer):
machine_manufacturer = identifier.get("manufacturer", definition_metadata.get("manufacturer", "Unknown")) #If the XML material doesn't specify a manufacturer, use the one in the actual printer definition. machine_manufacturer = identifier.get("manufacturer", definition_metadata.get("manufacturer", "Unknown")) #If the XML material doesn't specify a manufacturer, use the one in the actual printer definition.
if machine_compatibility: # Always create the instance of the material even if it is not compatible, otherwise it will never
new_material_id = container_id + "_" + machine_id # show as incompatible if the material profile doesn't define hotends in the machine - CURA-5444
new_material_id = container_id + "_" + machine_id
# Do not look for existing container/container metadata with the same ID although they may exist. # Do not look for existing container/container metadata with the same ID although they may exist.
# In project loading and perhaps some other places, we only want to get information (metadata) # In project loading and perhaps some other places, we only want to get information (metadata)
# from a file without changing the current state of the system. If we overwrite the existing # from a file without changing the current state of the system. If we overwrite the existing
# metadata here, deserializeMetadata() will not be safe for retrieving information. # metadata here, deserializeMetadata() will not be safe for retrieving information.
new_material_metadata = {} new_material_metadata = {}
new_material_metadata.update(base_metadata) new_material_metadata.update(base_metadata)
new_material_metadata["id"] = new_material_id new_material_metadata["id"] = new_material_id
new_material_metadata["compatible"] = machine_compatibility new_material_metadata["compatible"] = machine_compatibility
new_material_metadata["machine_manufacturer"] = machine_manufacturer new_material_metadata["machine_manufacturer"] = machine_manufacturer
new_material_metadata["definition"] = machine_id new_material_metadata["definition"] = machine_id
result_metadata.append(new_material_metadata) result_metadata.append(new_material_metadata)
buildplates = machine.iterfind("./um:buildplate", cls.__namespaces) buildplates = machine.iterfind("./um:buildplate", cls.__namespaces)
buildplate_map = {} buildplate_map = {}
@ -1055,7 +1057,7 @@ class XmlMaterialProfile(InstanceContainer):
@classmethod @classmethod
def getProductIdMap(cls) -> Dict[str, List[str]]: def getProductIdMap(cls) -> Dict[str, List[str]]:
product_to_id_file = os.path.join(os.path.dirname(sys.modules[cls.__module__].__file__), "product_to_id.json") product_to_id_file = os.path.join(os.path.dirname(sys.modules[cls.__module__].__file__), "product_to_id.json")
with open(product_to_id_file) as f: with open(product_to_id_file, encoding = "utf-8") as f:
product_to_id_map = json.load(f) product_to_id_map = json.load(f)
product_to_id_map = {key: [value] for key, value in product_to_id_map.items()} product_to_id_map = {key: [value] for key, value in product_to_id_map.items()}
return product_to_id_map return product_to_id_map

View file

@ -8,5 +8,6 @@
"Ultimaker 3 Extended": "ultimaker3_extended", "Ultimaker 3 Extended": "ultimaker3_extended",
"Ultimaker Original": "ultimaker_original", "Ultimaker Original": "ultimaker_original",
"Ultimaker Original+": "ultimaker_original_plus", "Ultimaker Original+": "ultimaker_original_plus",
"Ultimaker Original Dual Extrusion": "ultimaker_original_dual",
"IMADE3D JellyBOX": "imade3d_jellybox" "IMADE3D JellyBOX": "imade3d_jellybox"
} }

View file

@ -1998,7 +1998,7 @@ msgstr "La vitesse à laquelle le filament est préparé pendant une rétraction
#: fdmprinter.def.json #: fdmprinter.def.json
msgctxt "retraction_extra_prime_amount label" msgctxt "retraction_extra_prime_amount label"
msgid "Retraction Extra Prime Amount" msgid "Retraction Extra Prime Amount"
msgstr "Degré supplémentaire de rétraction primaire" msgstr "Volume supplémentaire à l'amorçage"
#: fdmprinter.def.json #: fdmprinter.def.json
msgctxt "retraction_extra_prime_amount description" msgctxt "retraction_extra_prime_amount description"

View file

@ -170,7 +170,7 @@ UM.PreferencesPage
append({ text: "日本語", code: "ja_JP" }) append({ text: "日本語", code: "ja_JP" })
append({ text: "한국어", code: "ko_KR" }) append({ text: "한국어", code: "ko_KR" })
append({ text: "Nederlands", code: "nl_NL" }) append({ text: "Nederlands", code: "nl_NL" })
append({ text: "Polski", code: "pl_PL" }) //Polish is disabled for being incomplete: append({ text: "Polski", code: "pl_PL" })
append({ text: "Português do Brasil", code: "pt_BR" }) append({ text: "Português do Brasil", code: "pt_BR" })
append({ text: "Português", code: "pt_PT" }) append({ text: "Português", code: "pt_PT" })
append({ text: "Русский", code: "ru_RU" }) append({ text: "Русский", code: "ru_RU" })
@ -741,21 +741,6 @@ UM.PreferencesPage
} }
} }
UM.TooltipArea
{
width: childrenRect.width
height: childrenRect.height
text: catalog.i18nc("@info:tooltip", "Should newly loaded models be arranged on the build plate? Used in conjunction with multi build plate (EXPERIMENTAL)")
CheckBox
{
id: arrangeOnLoadCheckbox
text: catalog.i18nc("@option:check", "Do not arrange objects on load")
checked: boolCheck(UM.Preferences.getValue("cura/not_arrange_objects_on_load"))
onCheckedChanged: UM.Preferences.setValue("cura/not_arrange_objects_on_load", checked)
}
}
Connections Connections
{ {
target: UM.Preferences target: UM.Preferences

View file

@ -264,7 +264,7 @@ Item {
{ {
// Another special case. The setting that is overriden is only 1 instance container deeper, // Another special case. The setting that is overriden is only 1 instance container deeper,
// so we can remove it. // so we can remove it.
propertyProvider.removeFromContainer(0) propertyProvider.removeFromContainer(last_entry - 1)
} }
else else
{ {
@ -281,7 +281,7 @@ Item {
color: UM.Theme.getColor("setting_control_button") color: UM.Theme.getColor("setting_control_button")
hoverColor: UM.Theme.getColor("setting_control_button_hover") hoverColor: UM.Theme.getColor("setting_control_button_hover")
iconSource: UM.Theme.getIcon("notice"); iconSource: UM.Theme.getIcon("formula");
onEntered: { hoverTimer.stop(); base.showTooltip(catalog.i18nc("@label", "This setting is normally calculated, but it currently has an absolute value set.\n\nClick to restore the calculated value.")) } onEntered: { hoverTimer.stop(); base.showTooltip(catalog.i18nc("@label", "This setting is normally calculated, but it currently has an absolute value set.\n\nClick to restore the calculated value.")) }
onExited: base.showTooltip(base.tooltipText); onExited: base.showTooltip(base.tooltipText);

View file

@ -1,6 +1,6 @@
[general] [general]
version = 4 version = 4
name = Coarse Quality name = Coarse
definition = fdmprinter definition = fdmprinter
[metadata] [metadata]

View file

@ -1,6 +1,6 @@
[general] [general]
version = 4 version = 4
name = Draft Quality name = Draft
definition = fdmprinter definition = fdmprinter
[metadata] [metadata]

View file

@ -1,6 +1,6 @@
[general] [general]
version = 4 version = 4
name = Extra Coarse Quality name = Extra Coarse
definition = fdmprinter definition = fdmprinter
[metadata] [metadata]

View file

@ -1,6 +1,6 @@
[general] [general]
version = 4 version = 4
name = Low Quality name = Normal
definition = fdmprinter definition = fdmprinter
[metadata] [metadata]

View file

@ -1,6 +1,6 @@
[general] [general]
version = 4 version = 4
name = Draft Quality name = Draft
definition = ultimaker2 definition = ultimaker2
[metadata] [metadata]

View file

@ -1,6 +1,6 @@
[general] [general]
version = 4 version = 4
name = Low Quality name = Normal
definition = ultimaker2 definition = ultimaker2
[metadata] [metadata]

View file

@ -25,3 +25,6 @@ speed_travel = 150
speed_wall = =math.ceil(speed_print * 40 / 55) speed_wall = =math.ceil(speed_print * 40 / 55)
top_bottom_thickness = 0.75 top_bottom_thickness = 0.75
wall_thickness = 0.7 wall_thickness = 0.7
speed_wall_0 = =math.ceil(speed_print * 40 / 55)
speed_wall_x = =math.ceil(speed_print * 80 / 55)
speed_infill = =math.ceil(speed_print * 100 / 55)

View file

@ -23,3 +23,5 @@ speed_print = 45
speed_wall = =math.ceil(speed_print * 30 / 45) speed_wall = =math.ceil(speed_print * 30 / 45)
top_bottom_thickness = 0.72 top_bottom_thickness = 0.72
wall_thickness = 1.05 wall_thickness = 1.05
speed_topbottom = =math.ceil(speed_print * 15 / 45)
speed_infill = =math.ceil(speed_print * 80 / 45)

View file

@ -24,3 +24,7 @@ speed_travel = 150
speed_wall = =math.ceil(speed_print * 40 / 45) speed_wall = =math.ceil(speed_print * 40 / 45)
top_bottom_thickness = 0.75 top_bottom_thickness = 0.75
wall_thickness = 0.7 wall_thickness = 0.7
speed_wall_0 = =math.ceil(speed_print * 40 / 45)
speed_topbottom = =math.ceil(speed_print * 30 / 45)
speed_wall_x = =math.ceil(speed_print * 80 / 45)
speed_infill = =math.ceil(speed_print * 100 / 45)

View file

@ -23,3 +23,5 @@ speed_print = 45
speed_wall = =math.ceil(speed_print * 30 / 45) speed_wall = =math.ceil(speed_print * 30 / 45)
top_bottom_thickness = 0.72 top_bottom_thickness = 0.72
wall_thickness = 1.05 wall_thickness = 1.05
speed_topbottom = =math.ceil(speed_print * 15 / 45)
speed_infill = =math.ceil(speed_print * 80 / 45)

View file

@ -42,3 +42,4 @@ support_xy_distance = 0.6
support_z_distance = =layer_height * 2 support_z_distance = =layer_height * 2
top_bottom_thickness = 1.2 top_bottom_thickness = 1.2
wall_thickness = 1 wall_thickness = 1
speed_infill = =math.ceil(speed_print * 80 / 40)

View file

@ -41,3 +41,6 @@ support_xy_distance = 0.6
support_z_distance = =layer_height * 2 support_z_distance = =layer_height * 2
top_bottom_thickness = 0.75 top_bottom_thickness = 0.75
wall_thickness = 1.06 wall_thickness = 1.06
speed_wall_0 = =math.ceil(speed_print * 40 / 45)
speed_wall_x = =math.ceil(speed_print * 80 / 45)
speed_infill = =math.ceil(speed_print * 100 / 45)

View file

@ -46,3 +46,4 @@ support_xy_distance = 0.7
support_z_distance = =layer_height * 2 support_z_distance = =layer_height * 2
top_bottom_thickness = 1.2 top_bottom_thickness = 1.2
wall_thickness = 1.2 wall_thickness = 1.2
speed_infill = =math.ceil(speed_print * 100 / 55)

View file

@ -36,3 +36,5 @@ support_infill_rate = 20
support_pattern = lines support_pattern = lines
support_z_distance = 0.19 support_z_distance = 0.19
wall_thickness = 0.88 wall_thickness = 0.88
speed_topbottom = =math.ceil(speed_print * 15 / 30)
speed_infill = =math.ceil(speed_print * 80 / 30)

View file

@ -37,3 +37,5 @@ support_infill_rate = 20
support_pattern = lines support_pattern = lines
support_z_distance = 0.19 support_z_distance = 0.19
wall_thickness = 1.2 wall_thickness = 1.2
speed_topbottom = =math.ceil(speed_print * 30 / 45)
speed_infill = =math.ceil(speed_print * 100 / 45)

View file

@ -43,3 +43,4 @@ support_pattern = lines
support_z_distance = 0.21 support_z_distance = 0.21
top_bottom_thickness = 0.75 top_bottom_thickness = 0.75
wall_thickness = 1.06 wall_thickness = 1.06
speed_infill = =math.ceil(speed_print * 100 / 45)

View file

@ -69,3 +69,5 @@ travel_avoid_distance = 3
wall_0_inset = 0 wall_0_inset = 0
wall_line_width_x = =round(line_width * 0.38 / 0.38, 2) wall_line_width_x = =round(line_width * 0.38 / 0.38, 2)
wall_thickness = 0.76 wall_thickness = 0.76
speed_wall_x = =math.ceil(speed_print * 80 / 25)
speed_infill = =math.ceil(speed_print * 100 / 25)

View file

@ -70,3 +70,5 @@ travel_avoid_distance = 3
wall_0_inset = 0 wall_0_inset = 0
wall_line_width_x = =round(line_width * 0.57 / 0.57, 2) wall_line_width_x = =round(line_width * 0.57 / 0.57, 2)
wall_thickness = 1.14 wall_thickness = 1.14
speed_wall_x = =math.ceil(speed_print * 80 / 25)
speed_infill = =math.ceil(speed_print * 100 / 25)

View file

@ -41,3 +41,4 @@ support_xy_distance = 0.6
support_z_distance = =layer_height * 2 support_z_distance = =layer_height * 2
top_bottom_thickness = 1.2 top_bottom_thickness = 1.2
wall_thickness = 0.88 wall_thickness = 0.88
speed_infill = =math.ceil(speed_print * 80 / 40)

View file

@ -43,3 +43,4 @@ support_xy_distance = 0.7
support_z_distance = =layer_height * 2 support_z_distance = =layer_height * 2
top_bottom_thickness = 1.2 top_bottom_thickness = 1.2
wall_thickness = 1.14 wall_thickness = 1.14
speed_infill = =math.ceil(speed_print * 100 / 45)

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="15"
height="15"
viewBox="0 0 15 15"
version="1.1"
id="svg3763"
sodipodi:docname="formula.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)">
<metadata
id="metadata3769">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs3767" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1176"
id="namedview3765"
showgrid="false"
inkscape:zoom="40.8"
inkscape:cx="7.5"
inkscape:cy="7.9433209"
inkscape:window-x="1920"
inkscape:window-y="360"
inkscape:window-maximized="1"
inkscape:current-layer="svg3763" />
<path
d="M 7.5371094 0.037109375 A 7.5 7.5 0 0 0 0.037109375 7.5371094 A 7.5 7.5 0 0 0 7.5371094 15.037109 A 7.5 7.5 0 0 0 15.037109 7.5371094 A 7.5 7.5 0 0 0 7.5371094 0.037109375 z M 7.8867188 1.9335938 L 9.0195312 1.9335938 L 8.8105469 2.9375 L 7.6660156 2.9375 C 7.2319879 2.9375 6.9144965 3.0236545 6.7148438 3.1972656 C 6.515191 3.3708767 6.3719618 3.6833767 6.2851562 4.1347656 L 6.1542969 4.7734375 L 8.1347656 4.7734375 L 7.9511719 5.6972656 L 5.9726562 5.6972656 L 4.6972656 12.064453 L 3.4980469 12.064453 L 4.7753906 5.6972656 L 3.6289062 5.6972656 L 3.8105469 4.7734375 L 4.9570312 4.7734375 L 5.0605469 4.265625 C 5.2254774 3.4496528 5.5299479 2.8587239 5.9726562 2.4941406 C 6.4240451 2.1208767 7.062066 1.9335938 7.8867188 1.9335938 z M 7.1699219 8.3632812 L 8.0175781 8.3632812 L 8.8691406 9.921875 L 10.337891 8.3632812 L 11.185547 8.3632812 L 9.1777344 10.492188 L 10.392578 12.738281 L 9.5449219 12.738281 L 8.6152344 11.019531 L 6.9980469 12.738281 L 6.1503906 12.738281 L 8.3105469 10.449219 L 7.1699219 8.3632812 z "
id="path3773" />
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -19,7 +19,7 @@ import pytest
def test_ultimaker3extended_variants(um3_file, um3e_file): def test_ultimaker3extended_variants(um3_file, um3e_file):
directory = os.path.join(os.path.dirname(__file__), "..", "resources", "variants") #TODO: Hardcoded path relative to this test file. directory = os.path.join(os.path.dirname(__file__), "..", "resources", "variants") #TODO: Hardcoded path relative to this test file.
um3 = configparser.ConfigParser() um3 = configparser.ConfigParser()
um3.read_file(open(os.path.join(directory, um3_file))) um3.read_file(open(os.path.join(directory, um3_file), encoding = "utf-8"))
um3e = configparser.ConfigParser() um3e = configparser.ConfigParser()
um3e.read_file(open(os.path.join(directory, um3e_file))) um3e.read_file(open(os.path.join(directory, um3e_file), encoding = "utf-8"))
assert um3["values"] == um3e["values"] assert um3["values"] == um3e["values"]