Merge branch 'master' into libArachne_rebased

Conflicts:
	cura/CuraApplication.py: Setting version
	fdmprinter and fdmextruder: Setting version
	intents, qualities and variants: Setting version
	Changelog: 4.9.1 was added, should be below Arachne changes
This commit is contained in:
Ghostkeeper 2021-06-16 18:04:54 +02:00
commit 611208368c
No known key found for this signature in database
GPG Key ID: D2A8871EE34EC59A
1015 changed files with 103196 additions and 86144 deletions

View File

@ -16,7 +16,7 @@ For crashes and similar issues, please attach the following information:
If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder
For additional support, you could also ask in the #cura channel on FreeNode IRC. For help with development, there is also the #cura-dev channel. For additional support, you could also ask in the [#cura channel](https://web.libera.chat/#cura) on [libera.chat](https://libera.chat/). For help with development, there is also the [#cura-dev channel](https://web.libera.chat/#cura-dev).
Dependencies Dependencies
------------ ------------
@ -26,10 +26,16 @@ Dependencies
* [PySerial](https://github.com/pyserial/pyserial) Only required for USB printing support. * [PySerial](https://github.com/pyserial/pyserial) Only required for USB printing support.
* [python-zeroconf](https://github.com/jstasiak/python-zeroconf) Only required to detect mDNS-enabled printers. * [python-zeroconf](https://github.com/jstasiak/python-zeroconf) Only required to detect mDNS-enabled printers.
For a list of required Python packages, with their recommended version, see `requirements.txt`.
This list is not exhaustive at the moment, please check the links in the next section for more details.
Build scripts Build scripts
------------- -------------
Please check out [cura-build](https://github.com/Ultimaker/cura-build) for detailed building instructions. Please check out [cura-build](https://github.com/Ultimaker/cura-build) for detailed building instructions.
If you want to build the entire environment from scratch before building Cura as well, [cura-build-environment](https://github.com/Ultimaker/cura-build) might be a starting point before cura-build. (Again, see cura-build for more details.)
Running from Source Running from Source
------------- -------------
Please check our [Wiki page](https://github.com/Ultimaker/Cura/wiki/Running-Cura-from-Source) for details about running Cura from source. Please check our [Wiki page](https://github.com/Ultimaker/Cura/wiki/Running-Cura-from-Source) for details about running Cura from source.

View File

@ -40,7 +40,7 @@ class Account(QObject):
""" """
# The interval in which sync services are automatically triggered # The interval in which sync services are automatically triggered
SYNC_INTERVAL = 30.0 # seconds SYNC_INTERVAL = 60.0 # seconds
Q_ENUMS(SyncState) Q_ENUMS(SyncState)
loginStateChanged = pyqtSignal(bool) loginStateChanged = pyqtSignal(bool)

View File

@ -13,7 +13,7 @@ DEFAULT_CURA_DEBUG_MODE = False
# Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for # Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for
# example Cura 3.2 with SDK version 6.1 will not work. So the SDK version is hard-coded here and left out of the # example Cura 3.2 with SDK version 6.1 will not work. So the SDK version is hard-coded here and left out of the
# CuraVersion.py.in template. # CuraVersion.py.in template.
CuraSDKVersion = "7.5.0" CuraSDKVersion = "7.6.0"
try: try:
from cura.CuraVersion import CuraAppName # type: ignore from cura.CuraVersion import CuraAppName # type: ignore

View File

@ -1,4 +1,4 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2021 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 PyQt5.QtCore import QTimer from PyQt5.QtCore import QTimer
@ -6,6 +6,8 @@ from typing import Any, TYPE_CHECKING
from UM.Logger import Logger from UM.Logger import Logger
import time
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
@ -56,8 +58,8 @@ class AutoSave:
def _onTimeout(self) -> None: def _onTimeout(self) -> None:
self._saving = True # To prevent the save process from triggering another autosave. self._saving = True # To prevent the save process from triggering another autosave.
Logger.log("d", "Autosaving preferences, instances and profiles")
save_start_time = time.time()
self._application.saveSettings() self._application.saveSettings()
Logger.log("d", "Autosaving preferences, instances and profiles took %s seconds", time.time() - save_start_time)
self._saving = False self._saving = False

View File

@ -14,6 +14,7 @@ from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
from UM.Platform import Platform from UM.Platform import Platform
from UM.Resources import Resources from UM.Resources import Resources
from UM.Version import Version
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
@ -28,6 +29,8 @@ class 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"]
"""These files should be ignored when making a backup.""" """These files should be ignored when making a backup."""
IGNORED_FOLDERS = [r"plugins"]
SECRETS_SETTINGS = ["general/ultimaker_auth_data"] SECRETS_SETTINGS = ["general/ultimaker_auth_data"]
"""Secret preferences that need to obfuscated when making a backup of Cura""" """Secret preferences that need to obfuscated when making a backup of Cura"""
@ -74,8 +77,9 @@ class Backup:
machine_count = max(len([s for s in files if "machine_instances/" in s]) - 1, 0) # If people delete their profiles but not their preferences, it can still make a backup, and report -1 profiles. Server crashes on this. machine_count = max(len([s for s in files if "machine_instances/" in s]) - 1, 0) # If people delete their profiles but not their preferences, it can still make a backup, and report -1 profiles. Server crashes on this.
material_count = max(len([s for s in files if "materials/" in s]) - 1, 0) material_count = max(len([s for s in files if "materials/" in s]) - 1, 0)
profile_count = max(len([s for s in files if "quality_changes/" in s]) - 1, 0) profile_count = max(len([s for s in files if "quality_changes/" in s]) - 1, 0)
plugin_count = len([s for s in files if "plugin.json" in s]) # We don't store plugins anymore, since if you can make backups, you have an account (and the plugins are
# on the marketplace anyway)
plugin_count = 0
# Store the archive and metadata so the BackupManager can fetch them when needed. # Store the archive and metadata so the BackupManager can fetch them when needed.
self.zip_file = buffer.getvalue() self.zip_file = buffer.getvalue()
self.meta_data = { self.meta_data = {
@ -94,8 +98,7 @@ class Backup:
:param root_path: The root directory to archive recursively. :param root_path: The root directory to archive recursively.
:return: The archive as bytes. :return: The archive as bytes.
""" """
ignore_string = re.compile("|".join(self.IGNORED_FILES + self.IGNORED_FOLDERS))
ignore_string = re.compile("|".join(self.IGNORED_FILES))
try: try:
archive = ZipFile(buffer, "w", ZIP_DEFLATED) archive = ZipFile(buffer, "w", ZIP_DEFLATED)
for root, folders, files in os.walk(root_path): for root, folders, files in os.walk(root_path):
@ -132,8 +135,8 @@ class Backup:
"Tried to restore a Cura backup without having proper data or meta data.")) "Tried to restore a Cura backup without having proper data or meta data."))
return False return False
current_version = self._application.getVersion() current_version = Version(self._application.getVersion())
version_to_restore = self.meta_data.get("cura_release", "master") version_to_restore = Version(self.meta_data.get("cura_release", "master"))
if current_version < version_to_restore: if current_version < version_to_restore:
# Cannot restore version newer than current because settings might have changed. # Cannot restore version newer than current because settings might have changed.

View File

@ -4,6 +4,7 @@
from typing import Dict, Optional, Tuple, TYPE_CHECKING from typing import Dict, Optional, Tuple, TYPE_CHECKING
from UM.Logger import Logger from UM.Logger import Logger
from UM.Version import Version
from cura.Backups.Backup import Backup from cura.Backups.Backup import Backup
if TYPE_CHECKING: if TYPE_CHECKING:
@ -52,6 +53,18 @@ class BackupsManager:
backup = Backup(self._application, zip_file = zip_file, meta_data = meta_data) backup = Backup(self._application, zip_file = zip_file, meta_data = meta_data)
restored = backup.restore() restored = backup.restore()
package_manager = self._application.getPackageManager()
# If the backup was made with Cura 4.10 (or higher), we no longer store plugins.
# Since the restored backup doesn't have those plugins anymore, we should remove it from the list
# of installed plugins.
if Version(meta_data.get("cura_release")) >= Version("4.10.0"):
for package_id in package_manager.getAllInstalledPackageIDs():
package_data = package_manager.getInstalledPackageInfo(package_id)
if package_data.get("package_type") == "plugin" and not package_data.get("is_bundled"):
package_manager.removePackage(package_id)
if restored: if restored:
# At this point, Cura will need to restart for the changes to take effect. # At this point, Cura will need to restart for the changes to take effect.
# 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.

View File

@ -67,11 +67,15 @@ class CuraActions(QObject):
current_node = parent_node current_node = parent_node
parent_node = current_node.getParent() parent_node = current_node.getParent()
# This was formerly done with SetTransformOperation but because of # Find out where the bottom of the object is
# unpredictable matrix deconstruction it was possible that mirrors bbox = current_node.getBoundingBox()
# could manifest as rotations. Centering is therefore done by if bbox:
# moving the node to negative whatever its position is: center_y = current_node.getWorldPosition().y - bbox.bottom
center_operation = TranslateOperation(current_node, -current_node._position) else:
center_y = 0
# Move the object so that it's bottom is on to of the buildplate
center_operation = TranslateOperation(current_node, Vector(0, center_y, 0), set_position = True)
operation.addOperation(center_operation) operation.addOperation(center_operation)
operation.push() operation.push()

View File

@ -257,6 +257,9 @@ class CuraApplication(QtApplication):
from cura.CuraPackageManager import CuraPackageManager from cura.CuraPackageManager import CuraPackageManager
self._package_manager_class = CuraPackageManager self._package_manager_class = CuraPackageManager
from UM.CentralFileStorage import CentralFileStorage
CentralFileStorage.setIsEnterprise(ApplicationMetadata.IsEnterpriseVersion)
@pyqtProperty(str, constant=True) @pyqtProperty(str, constant=True)
def ultimakerCloudApiRootUrl(self) -> str: def ultimakerCloudApiRootUrl(self) -> str:
return UltimakerCloudConstants.CuraCloudAPIRoot return UltimakerCloudConstants.CuraCloudAPIRoot
@ -1526,12 +1529,8 @@ class CuraApplication(QtApplication):
# Compute the center of the objects # Compute the center of the objects
object_centers = [] object_centers = []
# Forget about the translation that the original objects have
zero_translation = Matrix(data=numpy.zeros(3))
for mesh, node in zip(meshes, group_node.getChildren()): for mesh, node in zip(meshes, group_node.getChildren()):
transformation = node.getLocalTransformation() transformed_mesh = mesh.getTransformed(Matrix()) # Forget about the transformations that the original object had.
transformation.setTranslation(zero_translation)
transformed_mesh = mesh.getTransformed(transformation)
center = transformed_mesh.getCenterPosition() center = transformed_mesh.getCenterPosition()
if center is not None: if center is not None:
object_centers.append(center) object_centers.append(center)
@ -1546,7 +1545,7 @@ class CuraApplication(QtApplication):
# Move each node to the same position. # Move each node to the same position.
for mesh, node in zip(meshes, group_node.getChildren()): for mesh, node in zip(meshes, group_node.getChildren()):
node.setTransformation(Matrix()) node.setTransformation(Matrix()) # Removes any changes in position and rotation.
# Align the object around its zero position # Align the object around its zero position
# and also apply the offset to center it inside the group. # and also apply the offset to center it inside the group.
node.setPosition(-mesh.getZeroPosition() - offset) node.setPosition(-mesh.getZeroPosition() - offset)
@ -1867,6 +1866,7 @@ class CuraApplication(QtApplication):
else: else:
node = CuraSceneNode() node = CuraSceneNode()
node.setMeshData(original_node.getMeshData()) node.setMeshData(original_node.getMeshData())
node.source_mime_type = original_node.source_mime_type
# Setting meshdata does not apply scaling. # Setting meshdata does not apply scaling.
if original_node.getScale() != Vector(1.0, 1.0, 1.0): if original_node.getScale() != Vector(1.0, 1.0, 1.0):

View File

@ -1,4 +1,4 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -34,4 +34,4 @@ def fetchLayerHeight(quality_group: "QualityGroup") -> float:
if isinstance(layer_height, SettingFunction): if isinstance(layer_height, SettingFunction):
layer_height = layer_height(global_stack) layer_height = layer_height(global_stack)
return float(layer_height) return round(float(layer_height), 3)

View File

@ -1,10 +1,11 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import copy # To duplicate materials. import copy # To duplicate materials.
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot # To allow the preference page proxy to be used from the actual preferences page. from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl
from typing import Any, Dict, Optional, TYPE_CHECKING from typing import Any, Dict, Optional, TYPE_CHECKING
import uuid # To generate new GUIDs for new materials. import uuid # To generate new GUIDs for new materials.
import zipfile # To export all materials in a .zip archive.
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Logger import Logger from UM.Logger import Logger
@ -24,6 +25,11 @@ class MaterialManagementModel(QObject):
This class handles the actions in that page, such as creating new materials, renaming them, etc. This class handles the actions in that page, such as creating new materials, renaming them, etc.
""" """
def __init__(self, parent: QObject) -> None:
super().__init__(parent)
cura_application = cura.CuraApplication.CuraApplication.getInstance()
self._preferred_export_all_path = None # type: Optional[QUrl] # Path to export all materials to. None if not yet initialised.
cura_application.getOutputDeviceManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
favoritesChanged = pyqtSignal(str) favoritesChanged = pyqtSignal(str)
"""Triggered when a favorite is added or removed. """Triggered when a favorite is added or removed.
@ -79,6 +85,7 @@ class MaterialManagementModel(QObject):
:param material_node: The material to remove. :param material_node: The material to remove.
""" """
Logger.info(f"Removing material {material_node.container_id}")
container_registry = CuraContainerRegistry.getInstance() container_registry = CuraContainerRegistry.getInstance()
materials_this_base_file = container_registry.findContainersMetadata(base_file = material_node.base_file) materials_this_base_file = container_registry.findContainersMetadata(base_file = material_node.base_file)
@ -194,6 +201,7 @@ class MaterialManagementModel(QObject):
:return: The root material ID of the duplicate material. :return: The root material ID of the duplicate material.
""" """
Logger.info(f"Duplicating material {material_node.base_file} to {new_base_id}")
return self.duplicateMaterialByBaseFile(material_node.base_file, new_base_id, new_metadata) return self.duplicateMaterialByBaseFile(material_node.base_file, new_base_id, new_metadata)
@pyqtSlot(result = str) @pyqtSlot(result = str)
@ -262,3 +270,52 @@ class MaterialManagementModel(QObject):
self.favoritesChanged.emit(material_base_file) self.favoritesChanged.emit(material_base_file)
except ValueError: # Material was not in the favorites list. except ValueError: # Material was not in the favorites list.
Logger.log("w", "Material {material_base_file} was already not a favorite material.".format(material_base_file = material_base_file)) Logger.log("w", "Material {material_base_file} was already not a favorite material.".format(material_base_file = material_base_file))
def _onOutputDevicesChanged(self) -> None:
"""
When the list of output devices changes, we may want to update the
preferred export path.
"""
cura_application = cura.CuraApplication.CuraApplication.getInstance()
device_manager = cura_application.getOutputDeviceManager()
devices = device_manager.getOutputDevices()
for device in devices:
if device.__class__.__name__ == "RemovableDriveOutputDevice":
self._preferred_export_all_path = QUrl.fromLocalFile(device.getId())
break
else: # No removable drives? Use local path.
self._preferred_export_all_path = cura_application.getDefaultPath("dialog_material_path")
self.outputDevicesChanged.emit()
outputDevicesChanged = pyqtSignal() # Triggered when adding or removing removable drives.
@pyqtProperty(QUrl, notify = outputDevicesChanged)
def preferredExportAllPath(self) -> QUrl:
"""
Get the preferred path to export materials to.
If there is a removable drive, that should be the preferred path. Otherwise it should be the most recent local
file path.
:return: The preferred path to export all materials to.
"""
if self._preferred_export_all_path is None: # Not initialised yet. Can happen when output devices changed before class got created.
self._onOutputDevicesChanged()
return self._preferred_export_all_path
@pyqtSlot(QUrl)
def exportAll(self, file_path: QUrl) -> None:
"""
Export all materials to a certain file path.
:param file_path: The path to export the materials to.
"""
registry = CuraContainerRegistry.getInstance()
archive = zipfile.ZipFile(file_path.toLocalFile(), "w", compression = zipfile.ZIP_DEFLATED)
for metadata in registry.findInstanceContainersMetadata(type = "material"):
if metadata["base_file"] != metadata["id"]: # Only process base files.
continue
if metadata["id"] == "empty_material": # Don't export the empty material.
continue
material = registry.findContainers(id = metadata["id"])[0]
suffix = registry.getMimeTypeForContainer(type(material)).preferredSuffix
filename = metadata["id"] + "." + suffix
archive.writestr(filename, material.serialize())

View File

@ -99,7 +99,7 @@ class QualitySettingsModel(ListModel):
if self._selected_position == self.GLOBAL_STACK_POSITION: if self._selected_position == self.GLOBAL_STACK_POSITION:
quality_node = quality_group.node_for_global quality_node = quality_group.node_for_global
else: else:
quality_node = quality_group.nodes_for_extruders.get(str(self._selected_position)) quality_node = quality_group.nodes_for_extruders.get(self._selected_position)
settings_keys = quality_group.getAllKeys() settings_keys = quality_group.getAllKeys()
quality_containers = [] quality_containers = []
if quality_node is not None and quality_node.container is not None: if quality_node is not None and quality_node.container is not None:
@ -117,7 +117,9 @@ class QualitySettingsModel(ListModel):
if self._selected_position == self.GLOBAL_STACK_POSITION and global_container: if self._selected_position == self.GLOBAL_STACK_POSITION and global_container:
quality_changes_metadata = global_container.getMetaData() quality_changes_metadata = global_container.getMetaData()
else: else:
quality_changes_metadata = extruders_container.get(str(self._selected_position)) extruder = extruders_container.get(self._selected_position)
if extruder:
quality_changes_metadata = extruder.getMetaData()
if quality_changes_metadata is not None: # It can be None if number of extruders are changed during runtime. if quality_changes_metadata is not None: # It can be None if number of extruders are changed during runtime.
container = container_registry.findContainers(id = quality_changes_metadata["id"]) container = container_registry.findContainers(id = quality_changes_metadata["id"])
if container: if container:

View File

@ -1,12 +1,12 @@
# Copyright (c) 2020 Ultimaker B.V. # Copyright (c) 2021 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 datetime import datetime from datetime import datetime
import json import json
import random import random
from hashlib import sha512 from hashlib import sha512
from base64 import b64encode from base64 import b64encode
from typing import Optional, Any, Dict, Tuple from typing import Optional
import requests import requests
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
@ -115,7 +115,7 @@ class AuthorizationHelpers:
token_request = requests.get(check_token_url, headers = { token_request = requests.get(check_token_url, headers = {
"Authorization": "Bearer {}".format(access_token) "Authorization": "Bearer {}".format(access_token)
}) })
except requests.exceptions.ConnectionError: except (requests.exceptions.ConnectionError, requests.exceptions.Timeout):
# Connection was suddenly dropped. Nothing we can do about that. # Connection was suddenly dropped. Nothing we can do about that.
Logger.logException("w", "Something failed while attempting to parse the JWT token") Logger.logException("w", "Something failed while attempting to parse the JWT token")
return None return None

View File

@ -113,7 +113,9 @@ class AuthorizationService:
# The token could not be refreshed using the refresh token. We should login again. # The token could not be refreshed using the refresh token. We should login again.
return None return None
# Ensure it gets stored as otherwise we only have it in memory. The stored refresh token has been deleted # Ensure it gets stored as otherwise we only have it in memory. The stored refresh token has been deleted
# from the server already. # from the server already. Do not store the auth_data if we could not get new auth_data (eg due to a
# network error), since this would cause an infinite loop trying to get new auth-data
if self._auth_data.success:
self._storeAuthData(self._auth_data) self._storeAuthData(self._auth_data)
return self._auth_helpers.parseJWT(self._auth_data.access_token) return self._auth_helpers.parseJWT(self._auth_data.access_token)

View File

@ -119,21 +119,23 @@ class CuraSceneNode(SceneNode):
self._aabb = None self._aabb = None
if self._mesh_data: if self._mesh_data:
self._aabb = self._mesh_data.getExtents(self.getWorldTransformation(copy = False)) self._aabb = self._mesh_data.getExtents(self.getWorldTransformation(copy = False))
else: # If there is no mesh_data, use a bounding box that encompasses the local (0,0,0)
position = self.getWorldPosition()
self._aabb = AxisAlignedBox(minimum = position, maximum = position)
for child in self.getAllChildren(): for child in self.getAllChildren():
if child.callDecoration("isNonPrintingMesh"): if child.callDecoration("isNonPrintingMesh"):
# Non-printing-meshes inside a group should not affect push apart or drop to build plate # Non-printing-meshes inside a group should not affect push apart or drop to build plate
continue continue
if not child.getMeshData(): child_bb = child.getBoundingBox()
# Nodes without mesh data should not affect bounding boxes of their parents. if child_bb is None or child_bb.minimum == child_bb.maximum:
# Child had a degenerate bounding box, such as an empty group. Don't count it along.
continue continue
if self._aabb is None: if self._aabb is None:
self._aabb = child.getBoundingBox() self._aabb = child_bb
else: else:
self._aabb = self._aabb + child.getBoundingBox() self._aabb = self._aabb + child_bb
if self._aabb is None: # No children that should be included? Just use your own position then, but it's an invalid AABB.
position = self.getWorldPosition()
self._aabb = AxisAlignedBox(minimum = position, maximum = position)
def __deepcopy__(self, memo: Dict[int, object]) -> "CuraSceneNode": def __deepcopy__(self, memo: Dict[int, object]) -> "CuraSceneNode":
"""Taken from SceneNode, but replaced SceneNode with CuraSceneNode""" """Taken from SceneNode, but replaced SceneNode with CuraSceneNode"""
@ -142,6 +144,7 @@ class CuraSceneNode(SceneNode):
copy.setTransformation(self.getLocalTransformation(copy= False)) copy.setTransformation(self.getLocalTransformation(copy= False))
copy.setMeshData(self._mesh_data) copy.setMeshData(self._mesh_data)
copy.setVisible(cast(bool, deepcopy(self._visible, memo))) copy.setVisible(cast(bool, deepcopy(self._visible, memo)))
copy.source_mime_type = cast(str, deepcopy(self.source_mime_type, memo))
copy._selectable = cast(bool, deepcopy(self._selectable, memo)) copy._selectable = cast(bool, deepcopy(self._selectable, memo))
copy._name = cast(str, deepcopy(self._name, memo)) copy._name = cast(str, deepcopy(self._name, memo))
for decorator in self._decorators: for decorator in self._decorators:

View File

@ -1,4 +1,4 @@
# Copyright (c) 2020 Ultimaker B.V. # Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import os import os
@ -241,6 +241,7 @@ class ContainerManager(QObject):
file_url = file_url_or_string.toLocalFile() file_url = file_url_or_string.toLocalFile()
else: else:
file_url = file_url_or_string file_url = file_url_or_string
Logger.info(f"Importing material from {file_url}")
if not file_url or not os.path.exists(file_url): if not file_url or not os.path.exists(file_url):
return {"status": "error", "message": "Invalid path"} return {"status": "error", "message": "Invalid path"}

View File

@ -86,6 +86,14 @@ class GlobalStack(CuraContainerStack):
def supportsNetworkConnection(self): def supportsNetworkConnection(self):
return self.getMetaDataEntry("supports_network_connection", False) return self.getMetaDataEntry("supports_network_connection", False)
@pyqtProperty(bool, constant = True)
def supportsMaterialExport(self):
"""
Whether the printer supports Cura's export format of material profiles.
:return: ``True`` if it supports it, or ``False`` if not.
"""
return self.getMetaDataEntry("supports_material_export", False)
@classmethod @classmethod
def getLoadingPriority(cls) -> int: def getLoadingPriority(cls) -> int:
return 2 return 2

View File

@ -132,9 +132,26 @@ class ObjectsModel(ListModel):
is_group = bool(node.callDecoration("isGroup")) is_group = bool(node.callDecoration("isGroup"))
name_handled_as_group = False
force_rename = False force_rename = False
if not is_group: if is_group:
# Handle names for individual nodes # Handle names for grouped nodes
original_name = self._group_name_prefix
current_name = node.getName()
if current_name.startswith(self._group_name_prefix):
# This group has a standard group name, but we may need to renumber it
name_index = int(current_name.split("#")[-1])
name_handled_as_group = True
elif not current_name:
# Force rename this group because this node has not been named as a group yet, probably because
# it's a newly created group.
name_index = 0
force_rename = True
name_handled_as_group = True
if not is_group or not name_handled_as_group:
# Handle names for individual nodes or groups that already have a non-group name
name = node.getName() name = node.getName()
name_match = self._naming_regex.fullmatch(name) name_match = self._naming_regex.fullmatch(name)
@ -144,18 +161,6 @@ class ObjectsModel(ListModel):
else: else:
original_name = name_match.groups()[0] original_name = name_match.groups()[0]
name_index = int(name_match.groups()[1]) name_index = int(name_match.groups()[1])
else:
# Handle names for grouped nodes
original_name = self._group_name_prefix
current_name = node.getName()
if current_name.startswith(self._group_name_prefix):
name_index = int(current_name.split("#")[-1])
else:
# Force rename this group because this node has not been named as a group yet, probably because
# it's a newly created group.
name_index = 0
force_rename = True
if original_name not in name_to_node_info_dict: if original_name not in name_to_node_info_dict:
# Keep track of 2 things: # Keep track of 2 things:

View File

@ -16,14 +16,6 @@ import argparse
import faulthandler import faulthandler
import os import os
# Workaround for a race condition on certain systems where there
# is a race condition between Arcus and PyQt. Importing Arcus
# first seems to prevent Sip from going into a state where it
# tries to create PyQt objects on a non-main thread.
import Arcus # @UnusedImport
import Savitar # @UnusedImport
import pynest2d # @UnusedImport
from PyQt5.QtNetwork import QSslConfiguration, QSslSocket from PyQt5.QtNetwork import QSslConfiguration, QSslSocket
from UM.Platform import Platform from UM.Platform import Platform

View File

@ -1,4 +1,4 @@
# Copyright (c) 2020 Ultimaker B.V. # Copyright (c) 2021 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 configparser import ConfigParser from configparser import ConfigParser
@ -412,7 +412,12 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
quality_container_id = parser["containers"][str(_ContainerIndexes.Quality)] quality_container_id = parser["containers"][str(_ContainerIndexes.Quality)]
quality_type = "empty_quality" quality_type = "empty_quality"
if quality_container_id not in ("empty", "empty_quality"): if quality_container_id not in ("empty", "empty_quality"):
if quality_container_id in instance_container_info_dict:
quality_type = instance_container_info_dict[quality_container_id].parser["metadata"]["quality_type"] quality_type = instance_container_info_dict[quality_container_id].parser["metadata"]["quality_type"]
else: # If a version upgrade changed the quality profile in the stack, we'll need to look for it in the built-in profiles instead of the workspace.
quality_matches = ContainerRegistry.getInstance().findContainersMetadata(id = quality_container_id)
if quality_matches: # If there's no profile with this ID, leave it empty_quality.
quality_type = quality_matches[0]["quality_type"]
# Get machine info # Get machine info
serialized = archive.open(global_stack_file).read().decode("utf-8") serialized = archive.open(global_stack_file).read().decode("utf-8")

View File

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

View File

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

View File

@ -3,5 +3,5 @@
"author": "fieldOfView", "author": "fieldOfView",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides support for reading AMF files.", "description": "Provides support for reading AMF files.",
"api": "7.5.0" "api": "7.6.0"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"description": "Backup and restore your configuration.", "description": "Backup and restore your configuration.",
"version": "1.2.0", "version": "1.2.0",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

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

View File

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

View File

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

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"description": "Connects to the Digital Library, allowing Cura to open files from and save files to the Digital Library.", "description": "Connects to the Digital Library, allowing Cura to open files from and save files to the Digital Library.",
"version": "1.0.0", "version": "1.0.0",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -65,6 +65,11 @@ Item
model: manager.digitalFactoryFileModel model: manager.digitalFactoryFileModel
visible: model.count != 0 && manager.retrievingFileStatus != DF.RetrievalStatus.InProgress visible: model.count != 0 && manager.retrievingFileStatus != DF.RetrievalStatus.InProgress
selectionMode: OldControls.SelectionMode.SingleSelection selectionMode: OldControls.SelectionMode.SingleSelection
onDoubleClicked:
{
manager.setSelectedFileIndices([row]);
openFilesButton.clicked();
}
OldControls.TableViewColumn OldControls.TableViewColumn
{ {

View File

@ -2,6 +2,7 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import json import json
import threading import threading
from json import JSONDecodeError
from typing import List, Dict, Any, Callable, Union, Optional from typing import List, Dict, Any, Callable, Union, Optional
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
@ -43,7 +44,7 @@ class DFFileExportAndUploadManager:
self._library_project_id = library_project_id # type: str self._library_project_id = library_project_id # type: str
self._library_project_name = library_project_name # type: str self._library_project_name = library_project_name # type: str
self._file_name = file_name # type: str self._file_name = file_name # type: str
self._upload_jobs = [] # type: List[ExportFileJob]
self._formats = formats # type: List[str] self._formats = formats # type: List[str]
self._api = DigitalFactoryApiClient(application = CuraApplication.getInstance(), on_error = lambda error: Logger.log("e", str(error))) self._api = DigitalFactoryApiClient(application = CuraApplication.getInstance(), on_error = lambda error: Logger.log("e", str(error)))
@ -80,6 +81,8 @@ class DFFileExportAndUploadManager:
) )
self._generic_success_message.actionTriggered.connect(self._onMessageActionTriggered) self._generic_success_message.actionTriggered.connect(self._onMessageActionTriggered)
def _onCuraProjectFileExported(self, job: ExportFileJob) -> None: def _onCuraProjectFileExported(self, job: ExportFileJob) -> None:
"""Handler for when the DF Library workspace file (3MF) has been created locally. """Handler for when the DF Library workspace file (3MF) has been created locally.
@ -271,7 +274,11 @@ class DFFileExportAndUploadManager:
def extractErrorTitle(reply_body: Optional[str]) -> str: def extractErrorTitle(reply_body: Optional[str]) -> str:
error_title = "" error_title = ""
if reply_body: if reply_body:
try:
reply_dict = json.loads(reply_body) reply_dict = json.loads(reply_body)
except JSONDecodeError:
Logger.logException("w", "Unable to extract title from reply body")
return error_title
if "errors" in reply_dict and len(reply_dict["errors"]) >= 1 and "title" in reply_dict["errors"][0]: if "errors" in reply_dict and len(reply_dict["errors"]) >= 1 and "title" in reply_dict["errors"][0]:
error_title = reply_dict["errors"][0]["title"] error_title = reply_dict["errors"][0]["title"]
return error_title return error_title
@ -313,8 +320,13 @@ class DFFileExportAndUploadManager:
QDesktopServices.openUrl(QUrl(project_url)) QDesktopServices.openUrl(QUrl(project_url))
message.hide() message.hide()
def start(self) -> None:
for job in self._upload_jobs:
job.start()
def initializeFileUploadJobMetadata(self) -> Dict[str, Any]: def initializeFileUploadJobMetadata(self) -> Dict[str, Any]:
metadata = {} metadata = {}
self._upload_jobs = []
if "3mf" in self._formats and "3mf" in self._file_handlers and self._file_handlers["3mf"]: if "3mf" in self._formats and "3mf" in self._file_handlers and self._file_handlers["3mf"]:
filename_3mf = self._file_name + ".3mf" filename_3mf = self._file_name + ".3mf"
metadata[filename_3mf] = { metadata[filename_3mf] = {
@ -335,7 +347,7 @@ class DFFileExportAndUploadManager:
} }
job_3mf = ExportFileJob(self._file_handlers["3mf"], self._nodes, self._file_name, "3mf") job_3mf = ExportFileJob(self._file_handlers["3mf"], self._nodes, self._file_name, "3mf")
job_3mf.finished.connect(self._onCuraProjectFileExported) job_3mf.finished.connect(self._onCuraProjectFileExported)
job_3mf.start() self._upload_jobs.append(job_3mf)
if "ufp" in self._formats and "ufp" in self._file_handlers and self._file_handlers["ufp"]: if "ufp" in self._formats and "ufp" in self._file_handlers and self._file_handlers["ufp"]:
filename_ufp = self._file_name + ".ufp" filename_ufp = self._file_name + ".ufp"
@ -357,5 +369,5 @@ class DFFileExportAndUploadManager:
} }
job_ufp = ExportFileJob(self._file_handlers["ufp"], self._nodes, self._file_name, "ufp") job_ufp = ExportFileJob(self._file_handlers["ufp"], self._nodes, self._file_name, "ufp")
job_ufp.finished.connect(self._onPrintFileExported) job_ufp.finished.connect(self._onPrintFileExported)
job_ufp.start() self._upload_jobs.append(job_ufp)
return metadata return metadata

View File

@ -385,6 +385,11 @@ class DigitalFactoryController(QObject):
def _applicationInitializationFinished(self) -> None: def _applicationInitializationFinished(self) -> None:
self._supported_file_types = self._application.getInstance().getMeshFileHandler().getSupportedFileTypesRead() self._supported_file_types = self._application.getInstance().getMeshFileHandler().getSupportedFileTypesRead()
# Although Cura supports these, it's super confusing in this context to show them.
for extension in ["jpg", "jpeg", "png", "bmp", "gif"]:
if extension in self._supported_file_types:
del self._supported_file_types[extension]
@pyqtSlot() @pyqtSlot()
def openSelectedFiles(self) -> None: def openSelectedFiles(self) -> None:
""" Downloads, then opens all files selected in the Qt frontend open dialog. """ Downloads, then opens all files selected in the Qt frontend open dialog.
@ -541,6 +546,7 @@ class DigitalFactoryController(QObject):
on_upload_success = self.uploadFileSuccess.emit, on_upload_success = self.uploadFileSuccess.emit,
on_upload_finished = self.uploadFileFinished.emit, on_upload_finished = self.uploadFileFinished.emit,
on_upload_progress = self.uploadFileProgress.emit) on_upload_progress = self.uploadFileProgress.emit)
self.file_upload_manager.start()
# Save the project id to make sure it will be preselected the next time the user opens the save dialog # Save the project id to make sure it will be preselected the next time the user opens the save dialog
self._current_workspace_information.setEntryToStore("digital_factory", "library_project_id", library_project_id) self._current_workspace_information.setEntryToStore("digital_factory", "library_project_id", library_project_id)

View File

@ -40,6 +40,7 @@ class DigitalFactoryFileModel(ListModel):
def setFiles(self, df_files_in_project: List[DigitalFactoryFileResponse]) -> None: def setFiles(self, df_files_in_project: List[DigitalFactoryFileResponse]) -> None:
if self._files == df_files_in_project: if self._files == df_files_in_project:
return return
self.clear()
self._files = df_files_in_project self._files = df_files_in_project
self._update() self._update()

View File

@ -21,7 +21,7 @@ class DigitalFactoryProjectModel(ListModel):
dfProjectModelChanged = pyqtSignal() dfProjectModelChanged = pyqtSignal()
def __init__(self, parent = None): def __init__(self, parent = None) -> None:
super().__init__(parent) super().__init__(parent)
self.addRoleName(self.DisplayNameRole, "displayName") self.addRoleName(self.DisplayNameRole, "displayName")
self.addRoleName(self.LibraryProjectIdRole, "libraryProjectId") self.addRoleName(self.LibraryProjectIdRole, "libraryProjectId")

View File

@ -0,0 +1,48 @@
from unittest.mock import MagicMock, patch
import pytest
from src.DFFileExportAndUploadManager import DFFileExportAndUploadManager
@pytest.fixture
def upload_manager():
file_handler = MagicMock(name = "file_handler")
file_handler.getSupportedFileTypesWrite = MagicMock(return_value = [{
"id": "test",
"extension": ".3mf",
"description": "nope",
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
"mode": "binary",
"hide_in_file_dialog": True,
}])
node = MagicMock(name = "SceneNode")
application = MagicMock(name = "CuraApplication")
with patch("cura.CuraApplication.CuraApplication.getInstance", MagicMock(return_value = application)):
return DFFileExportAndUploadManager(file_handlers = {"3mf": file_handler},
nodes = [node],
library_project_id = "test_library_project_id",
library_project_name = "test_library_project_name",
file_name = "file_name",
formats = ["3mf"],
on_upload_error = MagicMock(),
on_upload_success = MagicMock(),
on_upload_finished = MagicMock(),
on_upload_progress = MagicMock())
@pytest.mark.parametrize("input,expected_result",
[("", ""),
("invalid json! {}", ""),
("{\"errors\": [{}]}", ""),
("{\"errors\": [{\"title\": \"some title\"}]}", "some title")])
def test_extractErrorTitle(upload_manager, input, expected_result):
assert upload_manager.extractErrorTitle(input) == expected_result
def test_exportJobError(upload_manager):
mocked_application = MagicMock()
with patch("UM.Application.Application.getInstance", MagicMock(return_value = mocked_application)):
upload_manager._onJobExportError("file_name.3mf")
# Ensure that message was displayed
mocked_application.showMessageSignal.emit.assert_called_once()

View File

@ -0,0 +1,73 @@
from pathlib import Path
from src.DigitalFactoryFileModel import DigitalFactoryFileModel
from src.DigitalFactoryFileResponse import DigitalFactoryFileResponse
file_1 = DigitalFactoryFileResponse(client_id = "client_id_1",
content_type = "zomg",
file_name = "file_1.3mf",
file_id = "file_id_1",
library_project_id = "project_id_1",
status = "test",
user_id = "user_id_1",
username = "username_1",
uploaded_at = "2021-04-07T10:33:25.000Z")
file_2 = DigitalFactoryFileResponse(client_id ="client_id_2",
content_type = "zomg",
file_name = "file_2.3mf",
file_id = "file_id_2",
library_project_id = "project_id_2",
status = "test",
user_id = "user_id_2",
username = "username_2",
uploaded_at = "2021-02-06T09:33:22.000Z")
file_wtf = DigitalFactoryFileResponse(client_id ="client_id_1",
content_type = "zomg",
file_name = "file_3.wtf",
file_id = "file_id_3",
library_project_id = "project_id_1",
status = "test",
user_id = "user_id_1",
username = "username_1",
uploaded_at = "2021-04-06T12:33:25.000Z")
def test_setFiles():
model = DigitalFactoryFileModel()
assert model.count == 0
model.setFiles([file_1, file_2])
assert model.count == 2
assert model.getItem(0)["fileName"] == "file_1.3mf"
assert model.getItem(1)["fileName"] == "file_2.3mf"
def test_clearProjects():
model = DigitalFactoryFileModel()
model.setFiles([file_1, file_2])
model.clearFiles()
assert model.count == 0
def test_setProjectMultipleTimes():
model = DigitalFactoryFileModel()
model.setFiles([file_1, file_2])
model.setFiles([file_2])
assert model.count == 1
assert model.getItem(0)["fileName"] == "file_2.3mf"
def test_setFilter():
model = DigitalFactoryFileModel()
model.setFiles([file_1, file_2, file_wtf])
model.setFilters({"file_name": lambda x: Path(x).suffix[1:].lower() in ["3mf"]})
assert model.count == 2
model.clearFilters()
assert model.count == 3

View File

@ -0,0 +1,55 @@
from src.DigitalFactoryProjectModel import DigitalFactoryProjectModel
from src.DigitalFactoryProjectResponse import DigitalFactoryProjectResponse
project_1 = DigitalFactoryProjectResponse(library_project_id = "omg",
display_name = "zomg",
username = "nope",
organization_shared = True)
project_2 = DigitalFactoryProjectResponse(library_project_id = "omg2",
display_name = "zomg2",
username = "nope",
organization_shared = False)
def test_setProjects():
model = DigitalFactoryProjectModel()
assert model.count == 0
model.setProjects([project_1, project_2])
assert model.count == 2
assert model.getItem(0)["displayName"] == "zomg"
assert model.getItem(1)["displayName"] == "zomg2"
def test_clearProjects():
model = DigitalFactoryProjectModel()
model.setProjects([project_1, project_2])
model.clearProjects()
assert model.count == 0
def test_setProjectMultipleTimes():
model = DigitalFactoryProjectModel()
model.setProjects([project_1, project_2])
model.setProjects([project_2])
assert model.count == 1
assert model.getItem(0)["displayName"] == "zomg2"
def test_extendProjects():
model = DigitalFactoryProjectModel()
assert model.count == 0
model.setProjects([project_1])
assert model.count == 1
model.extendProjects([project_2])
assert model.count == 2
assert model.getItem(0)["displayName"] == "zomg"
assert model.getItem(1)["displayName"] == "zomg2"

View File

@ -0,0 +1,86 @@
from unittest.mock import MagicMock
import pytest
from cura.CuraApplication import CuraApplication
from src.DigitalFactoryApiClient import DigitalFactoryApiClient
from src.PaginationManager import PaginationManager
@pytest.fixture
def application():
app = MagicMock(spec=CuraApplication, name = "Mocked Cura Application")
return app
@pytest.fixture
def pagination_manager():
manager = MagicMock(name = "Mocked Pagination Manager")
return manager
@pytest.fixture
def api_client(application, pagination_manager):
api_client = DigitalFactoryApiClient(application, MagicMock())
api_client._projects_pagination_mgr = pagination_manager
return api_client
def test_getProjectsFirstPage(api_client):
# setup
http_manager = MagicMock()
api_client._http = http_manager
pagination_manager = api_client._projects_pagination_mgr
pagination_manager.limit = 20
finished_callback = MagicMock()
failed_callback = MagicMock()
# Call
api_client.getProjectsFirstPage(on_finished = finished_callback, failed = failed_callback)
# Asserts
pagination_manager.reset.assert_called_once() # Should be called since we asked for new set of projects
http_manager.get.assert_called_once()
args = http_manager.get.call_args_list[0]
# Ensure that it's called with the right limit
assert args[0][0] == "https://api.ultimaker.com/cura/v1/projects?limit=20"
# Change the limit & try again
http_manager.get.reset_mock()
pagination_manager.limit = 80
api_client.getProjectsFirstPage(on_finished = finished_callback, failed = failed_callback)
args = http_manager.get.call_args_list[0]
# Ensure that it's called with the right limit
assert args[0][0] == "https://api.ultimaker.com/cura/v1/projects?limit=80"
def test_getMoreProjects_noNewProjects(api_client):
api_client.hasMoreProjectsToLoad = MagicMock(return_value = False)
http_manager = MagicMock()
api_client._http = http_manager
finished_callback = MagicMock()
failed_callback = MagicMock()
api_client.getMoreProjects(finished_callback, failed_callback)
http_manager.get.assert_not_called()
def test_getMoreProjects_hasNewProjects(api_client):
api_client.hasMoreProjectsToLoad = MagicMock(return_value = True)
http_manager = MagicMock()
api_client._http = http_manager
finished_callback = MagicMock()
failed_callback = MagicMock()
api_client.getMoreProjects(finished_callback, failed_callback)
http_manager.get.assert_called_once()
def test_clear(api_client):
api_client.clear()
api_client._projects_pagination_mgr.reset.assert_called_once()

View File

@ -0,0 +1,5 @@
# Ensure that the importing for all tests work
import sys
import os
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))

View File

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

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides a machine actions for updating firmware.", "description": "Provides a machine actions for updating firmware.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

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

View File

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

View File

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

View File

@ -3,6 +3,6 @@
"author": "Victor Larchenko, Ultimaker B.V.", "author": "Victor Larchenko, Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Allows loading and displaying G-code files.", "description": "Allows loading and displaying G-code files.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

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

View File

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

View File

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

View File

@ -3,6 +3,6 @@
"author": "fieldOfView, Ultimaker B.V.", "author": "fieldOfView, Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).", "description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

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

View File

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

View File

@ -73,9 +73,13 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
# Add all instances that are not added, but are in visibility list # Add all instances that are not added, but are in visibility list
for item in visible: for item in visible:
if settings.getInstance(item) is None: # Setting was not added already. if settings.getInstance(item) is not None: # Setting was added already.
continue
definition = self._stack.getSettingDefinition(item) definition = self._stack.getSettingDefinition(item)
if definition: if not definition:
Logger.log("w", f"Unable to add instance ({item}) to per-object visibility because we couldn't find the matching definition.")
continue
new_instance = SettingInstance(definition, settings) new_instance = SettingInstance(definition, settings)
stack_nr = -1 stack_nr = -1
stack = None stack = None
@ -103,8 +107,6 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
new_instance.resetState() # Ensure that the state is not seen as a user state. new_instance.resetState() # Ensure that the state is not seen as a user state.
settings.addInstance(new_instance) settings.addInstance(new_instance)
visibility_changed = True visibility_changed = True
else:
Logger.log("w", "Unable to add instance (%s) to per-object visibility because we couldn't find the matching definition", item)
if visibility_changed: if visibility_changed:
self.visibilityChanged.emit() self.visibilityChanged.emit()

View File

@ -1,4 +1,4 @@
// Copyright (c) 2017 Ultimaker B.V. // Copyright (c) 2021 Ultimaker B.V.
// Uranium is released under the terms of the LGPLv3 or higher. // Uranium is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2 import QtQuick 2.2
@ -136,10 +136,12 @@ Item
} }
ComboBox Cura.ComboBox
{ {
id: infillOnlyComboBox id: infillOnlyComboBox
width: parent.width / 2 - UM.Theme.getSize("default_margin").width width: parent.width / 2 - UM.Theme.getSize("default_margin").width
height: UM.Theme.getSize("setting_control").height
textRole: "text"
model: ListModel model: ListModel
{ {

View File

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

View File

@ -1,14 +1,6 @@
# Copyright (c) 2020 Jaime van Kessel, Ultimaker B.V. # Copyright (c) 2020 Jaime van Kessel, Ultimaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. # The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
# Workaround for a race condition on certain systems where there
# is a race condition between Arcus and PyQt. Importing Arcus
# first seems to prevent Sip from going into a state where it
# tries to create PyQt objects on a non-main thread.
import Arcus # @UnusedImport
import Savitar # @UnusedImport
import pynest2d # @UnusedImport
from . import PostProcessingPlugin from . import PostProcessingPlugin

View File

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

View File

@ -1,6 +1,9 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2021 Ultimaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. # The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
# Modification 06.09.2020
# add checkbox, now you can choose and use configuration from the firmware itself.
from typing import List from typing import List
from ..Script import Script from ..Script import Script
@ -27,14 +30,21 @@ class FilamentChange(Script):
"type": "str", "type": "str",
"default_value": "1" "default_value": "1"
}, },
"firmware_config":
{
"label": "Use Firmware Configuration",
"description": "Use the settings in your firmware, or customise the parameters of the filament change here.",
"type": "bool",
"default_value": false
},
"initial_retract": "initial_retract":
{ {
"label": "Initial Retraction", "label": "Initial Retraction",
"description": "Initial filament retraction distance. The filament will be retracted with this amount before moving the nozzle away from the ongoing print.", "description": "Initial filament retraction distance. The filament will be retracted with this amount before moving the nozzle away from the ongoing print.",
"unit": "mm", "unit": "mm",
"type": "float", "type": "float",
"default_value": 30.0 "default_value": 30.0,
"enabled": "not firmware_config"
}, },
"later_retract": "later_retract":
{ {
@ -42,7 +52,8 @@ class FilamentChange(Script):
"description": "Later filament retraction distance for removal. The filament will be retracted all the way out of the printer so that you can change the filament.", "description": "Later filament retraction distance for removal. The filament will be retracted all the way out of the printer so that you can change the filament.",
"unit": "mm", "unit": "mm",
"type": "float", "type": "float",
"default_value": 300.0 "default_value": 300.0,
"enabled": "not firmware_config"
}, },
"x_position": "x_position":
{ {
@ -50,7 +61,8 @@ class FilamentChange(Script):
"description": "Extruder X position. The print head will move here for filament change.", "description": "Extruder X position. The print head will move here for filament change.",
"unit": "mm", "unit": "mm",
"type": "float", "type": "float",
"default_value": 0 "default_value": 0,
"enabled": "not firmware_config"
}, },
"y_position": "y_position":
{ {
@ -58,7 +70,17 @@ class FilamentChange(Script):
"description": "Extruder Y position. The print head will move here for filament change.", "description": "Extruder Y position. The print head will move here for filament change.",
"unit": "mm", "unit": "mm",
"type": "float", "type": "float",
"default_value": 0 "default_value": 0,
"enabled": "not firmware_config"
},
"z_position":
{
"label": "Z Position (relative)",
"description": "Extruder relative Z position. Move the print head up for filament change.",
"unit": "mm",
"type": "float",
"default_value": 0,
"minimum_value": 0
} }
} }
}""" }"""
@ -74,9 +96,12 @@ class FilamentChange(Script):
later_retract = self.getSettingValueByKey("later_retract") later_retract = self.getSettingValueByKey("later_retract")
x_pos = self.getSettingValueByKey("x_position") x_pos = self.getSettingValueByKey("x_position")
y_pos = self.getSettingValueByKey("y_position") y_pos = self.getSettingValueByKey("y_position")
z_pos = self.getSettingValueByKey("z_position")
firmware_config = self.getSettingValueByKey("firmware_config")
color_change = "M600" color_change = "M600"
if not firmware_config:
if initial_retract is not None and initial_retract > 0.: if initial_retract is not None and initial_retract > 0.:
color_change = color_change + (" E%.2f" % initial_retract) color_change = color_change + (" E%.2f" % initial_retract)
@ -89,6 +114,9 @@ class FilamentChange(Script):
if y_pos is not None: if y_pos is not None:
color_change = color_change + (" Y%.2f" % y_pos) color_change = color_change + (" Y%.2f" % y_pos)
if z_pos is not None and z_pos > 0.:
color_change = color_change + (" Z%.2f" % z_pos)
color_change = color_change + " ; Generated by FilamentChange plugin\n" color_change = color_change + " ; Generated by FilamentChange plugin\n"
layer_targets = layer_nums.split(",") layer_targets = layer_nums.split(",")

View File

@ -387,7 +387,7 @@ class PauseAtHeight(Script):
#Retraction #Retraction
prepend_gcode += self.putValue(M = 83) + " ; switch to relative E values for any needed retraction\n" prepend_gcode += self.putValue(M = 83) + " ; switch to relative E values for any needed retraction\n"
if retraction_amount != 0: if retraction_amount != 0:
prepend_gcode += self.putValue(G = 1, E = retraction_amount, F = 6000) + "\n" prepend_gcode += self.putValue(G = 1, E = -retraction_amount, F = 6000) + "\n"
#Move the head away #Move the head away
prepend_gcode += self.putValue(G = 1, Z = current_z + 1, F = 300) + " ; move up a millimeter to get out of the way\n" prepend_gcode += self.putValue(G = 1, Z = current_z + 1, F = 300) + " ; move up a millimeter to get out of the way\n"
@ -507,7 +507,15 @@ class PauseAtHeight(Script):
else: else:
Logger.log("w", "No previous feedrate found in gcode, feedrate for next layer(s) might be incorrect") Logger.log("w", "No previous feedrate found in gcode, feedrate for next layer(s) might be incorrect")
prepend_gcode += self.putValue(M = 82) + " ; switch back to absolute E values\n" extrusion_mode_string = "absolute"
extrusion_mode_numeric = 82
relative_extrusion = Application.getInstance().getGlobalContainerStack().getProperty("relative_extrusion", "value")
if relative_extrusion:
extrusion_mode_string = "relative"
extrusion_mode_numeric = 83
prepend_gcode += self.putValue(M = extrusion_mode_numeric) + " ; switch back to " + extrusion_mode_string + " E values\n"
# reset extrude value to pre pause value # reset extrude value to pre pause value
prepend_gcode += self.putValue(G = 92, E = current_e) + "\n" prepend_gcode += self.putValue(G = 92, E = current_e) + "\n"

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides a prepare stage in Cura.", "description": "Provides a prepare stage in Cura.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides a preview stage in Cura.", "description": "Provides a preview stage in Cura.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"description": "Provides removable drive hotplugging and writing support.", "description": "Provides removable drive hotplugging and writing support.",
"version": "1.0.1", "version": "1.0.1",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Logs certain events so that they can be used by the crash reporter", "description": "Logs certain events so that they can be used by the crash reporter",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -73,6 +73,8 @@ class SimulationPass(RenderPass):
self._layer_shader.setUniformValue("u_min_thickness", self._layer_view.getMinThickness()) self._layer_shader.setUniformValue("u_min_thickness", self._layer_view.getMinThickness())
self._layer_shader.setUniformValue("u_max_line_width", self._layer_view.getMaxLineWidth()) self._layer_shader.setUniformValue("u_max_line_width", self._layer_view.getMaxLineWidth())
self._layer_shader.setUniformValue("u_min_line_width", self._layer_view.getMinLineWidth()) self._layer_shader.setUniformValue("u_min_line_width", self._layer_view.getMinLineWidth())
self._layer_shader.setUniformValue("u_max_flow_rate", self._layer_view.getMaxFlowRate())
self._layer_shader.setUniformValue("u_min_flow_rate", self._layer_view.getMinFlowRate())
self._layer_shader.setUniformValue("u_layer_view_type", self._layer_view.getSimulationViewType()) self._layer_shader.setUniformValue("u_layer_view_type", self._layer_view.getSimulationViewType())
self._layer_shader.setUniformValue("u_extruder_opacity", self._layer_view.getExtruderOpacities()) self._layer_shader.setUniformValue("u_extruder_opacity", self._layer_view.getExtruderOpacities())
self._layer_shader.setUniformValue("u_show_travel_moves", self._layer_view.getShowTravelMoves()) self._layer_shader.setUniformValue("u_show_travel_moves", self._layer_view.getShowTravelMoves())
@ -86,6 +88,8 @@ class SimulationPass(RenderPass):
self._layer_shader.setUniformValue("u_min_feedrate", 0) self._layer_shader.setUniformValue("u_min_feedrate", 0)
self._layer_shader.setUniformValue("u_max_thickness", 1) self._layer_shader.setUniformValue("u_max_thickness", 1)
self._layer_shader.setUniformValue("u_min_thickness", 0) self._layer_shader.setUniformValue("u_min_thickness", 0)
self._layer_shader.setUniformValue("u_max_flow_rate", 1)
self._layer_shader.setUniformValue("u_min_flow_rate", 0)
self._layer_shader.setUniformValue("u_max_line_width", 1) self._layer_shader.setUniformValue("u_max_line_width", 1)
self._layer_shader.setUniformValue("u_min_line_width", 0) self._layer_shader.setUniformValue("u_min_line_width", 0)
self._layer_shader.setUniformValue("u_layer_view_type", 1) self._layer_shader.setUniformValue("u_layer_view_type", 1)

View File

@ -1,4 +1,4 @@
# Copyright (c) 2020 Ultimaker B.V. # Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import sys import sys
@ -30,6 +30,7 @@ from UM.View.GL.ShaderProgram import ShaderProgram
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from cura.CuraView import CuraView from cura.CuraView import CuraView
from cura.LayerPolygon import LayerPolygon # To distinguish line types.
from cura.Scene.ConvexHullNode import ConvexHullNode from cura.Scene.ConvexHullNode import ConvexHullNode
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
@ -93,6 +94,8 @@ class SimulationView(CuraView):
self._min_thickness = sys.float_info.max self._min_thickness = sys.float_info.max
self._max_line_width = sys.float_info.min self._max_line_width = sys.float_info.min
self._min_line_width = sys.float_info.max self._min_line_width = sys.float_info.max
self._min_flow_rate = sys.float_info.max
self._max_flow_rate = sys.float_info.min
self._global_container_stack = None # type: Optional[ContainerStack] self._global_container_stack = None # type: Optional[ContainerStack]
self._proxy = None self._proxy = None
@ -115,6 +118,7 @@ class SimulationView(CuraView):
Application.getInstance().getPreferences().addPreference("layerview/show_infill", True) Application.getInstance().getPreferences().addPreference("layerview/show_infill", True)
Application.getInstance().getPreferences().addPreference("layerview/show_starts", True) Application.getInstance().getPreferences().addPreference("layerview/show_starts", True)
self.visibleStructuresChanged.connect(self.calculateColorSchemeLimits)
self._updateWithPreferences() self._updateWithPreferences()
self._solid_layers = int(Application.getInstance().getPreferences().getValue("view/top_layer_count")) self._solid_layers = int(Application.getInstance().getPreferences().getValue("view/top_layer_count"))
@ -198,6 +202,7 @@ class SimulationView(CuraView):
if node.getMeshData() is None: if node.getMeshData() is None:
return return
self.setActivity(False) self.setActivity(False)
self.calculateColorSchemeLimits()
self.calculateMaxLayers() self.calculateMaxLayers()
self.calculateMaxPathsOnLayer(self._current_layer_num) self.calculateMaxPathsOnLayer(self._current_layer_num)
@ -218,12 +223,6 @@ class SimulationView(CuraView):
def resetLayerData(self) -> None: def resetLayerData(self) -> None:
self._current_layer_mesh = None self._current_layer_mesh = None
self._current_layer_jumps = None self._current_layer_jumps = None
self._max_feedrate = sys.float_info.min
self._min_feedrate = sys.float_info.max
self._max_thickness = sys.float_info.min
self._min_thickness = sys.float_info.max
self._max_line_width = sys.float_info.min
self._min_line_width = sys.float_info.max
def beginRendering(self) -> None: def beginRendering(self) -> None:
scene = self.getController().getScene() scene = self.getController().getScene()
@ -248,58 +247,59 @@ class SimulationView(CuraView):
renderer.queueNode(node, transparent = True, shader = self._ghost_shader) renderer.queueNode(node, transparent = True, shader = self._ghost_shader)
def setLayer(self, value: int) -> None: def setLayer(self, value: int) -> None:
"""
Set the upper end of the range of visible layers.
If setting it below the lower end of the range, the lower end is lowered so that 1 layer stays visible.
:param value: The new layer number to show, 0-indexed.
"""
if self._current_layer_num != value: if self._current_layer_num != value:
self._current_layer_num = value self._current_layer_num = min(max(value, 0), self._max_layers)
if self._current_layer_num < 0: self._minimum_layer_num = min(self._current_layer_num, self._minimum_layer_num)
self._current_layer_num = 0
if self._current_layer_num > self._max_layers:
self._current_layer_num = self._max_layers
if self._current_layer_num < self._minimum_layer_num:
self._minimum_layer_num = self._current_layer_num
self._startUpdateTopLayers() self._startUpdateTopLayers()
self.currentLayerNumChanged.emit() self.currentLayerNumChanged.emit()
def setMinimumLayer(self, value: int) -> None: def setMinimumLayer(self, value: int) -> None:
"""
Set the lower end of the range of visible layers.
If setting it above the upper end of the range, the upper end is increased so that 1 layer stays visible.
:param value: The new lower end of the range of visible layers, 0-indexed.
"""
if self._minimum_layer_num != value: if self._minimum_layer_num != value:
self._minimum_layer_num = value self._minimum_layer_num = min(max(value, 0), self._max_layers)
if self._minimum_layer_num < 0: self._current_layer_num = max(self._current_layer_num, self._minimum_layer_num)
self._minimum_layer_num = 0
if self._minimum_layer_num > self._max_layers:
self._minimum_layer_num = self._max_layers
if self._minimum_layer_num > self._current_layer_num:
self._current_layer_num = self._minimum_layer_num
self._startUpdateTopLayers() self._startUpdateTopLayers()
self.currentLayerNumChanged.emit() self.currentLayerNumChanged.emit()
def setPath(self, value: int) -> None: def setPath(self, value: int) -> None:
"""
Set the upper end of the range of visible paths on the current layer.
If setting it below the lower end of the range, the lower end is lowered so that 1 path stays visible.
:param value: The new path index to show, 0-indexed.
"""
if self._current_path_num != value: if self._current_path_num != value:
self._current_path_num = value self._current_path_num = min(max(value, 0), self._max_paths)
if self._current_path_num < 0: self._minimum_path_num = min(self._minimum_path_num, self._current_path_num)
self._current_path_num = 0
if self._current_path_num > self._max_paths:
self._current_path_num = self._max_paths
if self._current_path_num < self._minimum_path_num:
self._minimum_path_num = self._current_path_num
self._startUpdateTopLayers() self._startUpdateTopLayers()
self.currentPathNumChanged.emit() self.currentPathNumChanged.emit()
def setMinimumPath(self, value: int) -> None: def setMinimumPath(self, value: int) -> None:
"""
Set the lower end of the range of visible paths on the current layer.
If setting it above the upper end of the range, the upper end is increased so that 1 path stays visible.
:param value: The new lower end of the range of visible paths, 0-indexed.
"""
if self._minimum_path_num != value: if self._minimum_path_num != value:
self._minimum_path_num = value self._minimum_path_num = min(max(value, 0), self._max_paths)
if self._minimum_path_num < 0: self._current_path_num = max(self._current_path_num, self._minimum_path_num)
self._minimum_path_num = 0
if self._minimum_path_num > self._max_layers:
self._minimum_path_num = self._max_layers
if self._minimum_path_num > self._current_path_num:
self._current_path_num = self._minimum_path_num
self._startUpdateTopLayers() self._startUpdateTopLayers()
self.currentPathNumChanged.emit() self.currentPathNumChanged.emit()
def setSimulationViewType(self, layer_view_type: int) -> None: def setSimulationViewType(self, layer_view_type: int) -> None:
@ -333,37 +333,52 @@ class SimulationView(CuraView):
# If more than 16 extruders are called for, this should be converted to a sampler1d. # If more than 16 extruders are called for, this should be converted to a sampler1d.
return Matrix(self._extruder_opacity) return Matrix(self._extruder_opacity)
def setShowTravelMoves(self, show): def setShowTravelMoves(self, show: bool) -> None:
if show == self._show_travel_moves:
return
self._show_travel_moves = show self._show_travel_moves = show
self.currentLayerNumChanged.emit() self.currentLayerNumChanged.emit()
self.visibleStructuresChanged.emit()
def getShowTravelMoves(self): def getShowTravelMoves(self) -> bool:
return self._show_travel_moves return self._show_travel_moves
def setShowHelpers(self, show: bool) -> None: def setShowHelpers(self, show: bool) -> None:
if show == self._show_helpers:
return
self._show_helpers = show self._show_helpers = show
self.currentLayerNumChanged.emit() self.currentLayerNumChanged.emit()
self.visibleStructuresChanged.emit()
def getShowHelpers(self) -> bool: def getShowHelpers(self) -> bool:
return self._show_helpers return self._show_helpers
def setShowSkin(self, show: bool) -> None: def setShowSkin(self, show: bool) -> None:
if show == self._show_skin:
return
self._show_skin = show self._show_skin = show
self.currentLayerNumChanged.emit() self.currentLayerNumChanged.emit()
self.visibleStructuresChanged.emit()
def getShowSkin(self) -> bool: def getShowSkin(self) -> bool:
return self._show_skin return self._show_skin
def setShowInfill(self, show: bool) -> None: def setShowInfill(self, show: bool) -> None:
if show == self._show_infill:
return
self._show_infill = show self._show_infill = show
self.currentLayerNumChanged.emit() self.currentLayerNumChanged.emit()
self.visibleStructuresChanged.emit()
def getShowInfill(self) -> bool: def getShowInfill(self) -> bool:
return self._show_infill return self._show_infill
def setShowStarts(self, show: bool) -> None: def setShowStarts(self, show: bool) -> None:
if show == self._show_starts:
return
self._show_starts = show self._show_starts = show
self.currentLayerNumChanged.emit() self.currentLayerNumChanged.emit()
self.visibleStructuresChanged.emit()
def getShowStarts(self) -> bool: def getShowStarts(self) -> bool:
return self._show_starts return self._show_starts
@ -398,12 +413,23 @@ class SimulationView(CuraView):
return 0.0 # If it's still max-float, there are no measurements. Use 0 then. return 0.0 # If it's still max-float, there are no measurements. Use 0 then.
return self._min_line_width return self._min_line_width
def getMaxFlowRate(self) -> float:
return self._max_flow_rate
def getMinFlowRate(self) -> float:
if abs(self._min_flow_rate - sys.float_info.max) < 10: # Some lenience due to floating point rounding.
return 0.0 # If it's still max-float, there are no measurements. Use 0 then.
return self._min_flow_rate
def calculateMaxLayers(self) -> None: def calculateMaxLayers(self) -> None:
"""
Calculates number of layers, triggers signals if the number of layers changed and makes sure the top layers are
recalculated for legacy layer view.
"""
scene = self.getController().getScene() scene = self.getController().getScene()
self._old_max_layers = self._max_layers self._old_max_layers = self._max_layers
new_max_layers = -1 new_max_layers = -1
"""Recalculate num max layers"""
for node in DepthFirstIterator(scene.getRoot()): # type: ignore for node in DepthFirstIterator(scene.getRoot()): # type: ignore
layer_data = node.callDecoration("getLayerData") layer_data = node.callDecoration("getLayerData")
if not layer_data: if not layer_data:
@ -418,19 +444,6 @@ class SimulationView(CuraView):
if len(layer_data.getLayer(layer_id).polygons) < 1: if len(layer_data.getLayer(layer_id).polygons) < 1:
continue continue
# Store the max and min feedrates and thicknesses for display purposes
for p in layer_data.getLayer(layer_id).polygons:
self._max_feedrate = max(float(p.lineFeedrates.max()), self._max_feedrate)
self._min_feedrate = min(float(p.lineFeedrates.min()), self._min_feedrate)
self._max_line_width = max(float(p.lineWidths.max()), self._max_line_width)
self._min_line_width = min(float(p.lineWidths.min()), self._min_line_width)
self._max_thickness = max(float(p.lineThicknesses.max()), self._max_thickness)
try:
self._min_thickness = min(float(p.lineThicknesses[numpy.nonzero(p.lineThicknesses)].min()), self._min_thickness)
except ValueError:
# Sometimes, when importing a GCode the line thicknesses are zero and so the minimum (avoiding
# the zero) can't be calculated
Logger.log("i", "Min thickness can't be calculated because all the values are zero")
if max_layer_number < layer_id: if max_layer_number < layer_id:
max_layer_number = layer_id max_layer_number = layer_id
if min_layer_number > layer_id: if min_layer_number > layer_id:
@ -454,6 +467,87 @@ class SimulationView(CuraView):
self.maxLayersChanged.emit() self.maxLayersChanged.emit()
self._startUpdateTopLayers() self._startUpdateTopLayers()
def calculateColorSchemeLimits(self) -> None:
"""
Calculates the limits of the colour schemes, depending on the layer view data that is visible to the user.
"""
# Before we start, save the old values so that we can tell if any of the spectrums need to change.
old_min_feedrate = self._min_feedrate
old_max_feedrate = self._max_feedrate
old_min_linewidth = self._min_line_width
old_max_linewidth = self._max_line_width
old_min_thickness = self._min_thickness
old_max_thickness = self._max_thickness
old_min_flow_rate = self._min_flow_rate
old_max_flow_rate = self._max_flow_rate
self._min_feedrate = sys.float_info.max
self._max_feedrate = sys.float_info.min
self._min_line_width = sys.float_info.max
self._max_line_width = sys.float_info.min
self._min_thickness = sys.float_info.max
self._max_thickness = sys.float_info.min
self._min_flow_rate = sys.float_info.max
self._max_flow_rate = sys.float_info.min
# The colour scheme is only influenced by the visible lines, so filter the lines by if they should be visible.
visible_line_types = []
if self.getShowSkin(): # Actually "shell".
visible_line_types.append(LayerPolygon.SkinType)
visible_line_types.append(LayerPolygon.Inset0Type)
visible_line_types.append(LayerPolygon.InsetXType)
if self.getShowInfill():
visible_line_types.append(LayerPolygon.InfillType)
if self.getShowHelpers():
visible_line_types.append(LayerPolygon.PrimeTowerType)
visible_line_types.append(LayerPolygon.SkirtType)
visible_line_types.append(LayerPolygon.SupportType)
visible_line_types.append(LayerPolygon.SupportInfillType)
visible_line_types.append(LayerPolygon.SupportInterfaceType)
visible_line_types_with_extrusion = visible_line_types.copy() # Copy before travel moves are added
if self.getShowTravelMoves():
visible_line_types.append(LayerPolygon.MoveCombingType)
visible_line_types.append(LayerPolygon.MoveRetractionType)
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
layer_data = node.callDecoration("getLayerData")
if not layer_data:
continue
for layer_index in layer_data.getLayers():
for polyline in layer_data.getLayer(layer_index).polygons:
is_visible = numpy.isin(polyline.types, visible_line_types)
visible_indices = numpy.where(is_visible)[0]
visible_indicies_with_extrusion = numpy.where(numpy.isin(polyline.types, visible_line_types_with_extrusion))[0]
if visible_indices.size == 0: # No items to take maximum or minimum of.
continue
visible_feedrates = numpy.take(polyline.lineFeedrates, visible_indices)
visible_feedrates_with_extrusion = numpy.take(polyline.lineFeedrates, visible_indicies_with_extrusion)
visible_linewidths = numpy.take(polyline.lineWidths, visible_indices)
visible_linewidths_with_extrusion = numpy.take(polyline.lineWidths, visible_indicies_with_extrusion)
visible_thicknesses = numpy.take(polyline.lineThicknesses, visible_indices)
visible_thicknesses_with_extrusion = numpy.take(polyline.lineThicknesses, visible_indicies_with_extrusion)
self._max_feedrate = max(float(visible_feedrates.max()), self._max_feedrate)
if visible_feedrates_with_extrusion.size != 0:
flow_rates = visible_feedrates_with_extrusion * visible_linewidths_with_extrusion * visible_thicknesses_with_extrusion
self._min_flow_rate = min(float(flow_rates.min()), self._min_flow_rate)
self._max_flow_rate = max(float(flow_rates.max()), self._max_flow_rate)
self._min_feedrate = min(float(visible_feedrates.min()), self._min_feedrate)
self._max_line_width = max(float(visible_linewidths.max()), self._max_line_width)
self._min_line_width = min(float(visible_linewidths.min()), self._min_line_width)
self._max_thickness = max(float(visible_thicknesses.max()), self._max_thickness)
try:
self._min_thickness = min(float(visible_thicknesses[numpy.nonzero(visible_thicknesses)].min()), self._min_thickness)
except ValueError:
# Sometimes, when importing a GCode the line thicknesses are zero and so the minimum (avoiding the zero) can't be calculated.
Logger.log("w", "Min thickness can't be calculated because all the values are zero")
if old_min_feedrate != self._min_feedrate or old_max_feedrate != self._max_feedrate \
or old_min_linewidth != self._min_line_width or old_max_linewidth != self._max_line_width \
or old_min_thickness != self._min_thickness or old_max_thickness != self._max_thickness \
or old_min_flow_rate != self._min_flow_rate or old_max_flow_rate != self._max_flow_rate:
self.colorSchemeLimitsChanged.emit()
def calculateMaxPathsOnLayer(self, layer_num: int) -> None: def calculateMaxPathsOnLayer(self, layer_num: int) -> None:
# Update the currentPath # Update the currentPath
scene = self.getController().getScene() scene = self.getController().getScene()
@ -480,6 +574,8 @@ class SimulationView(CuraView):
preferencesChanged = Signal() preferencesChanged = Signal()
busyChanged = Signal() busyChanged = Signal()
activityChanged = Signal() activityChanged = Signal()
visibleStructuresChanged = Signal()
colorSchemeLimitsChanged = Signal()
def getProxy(self, engine, script_engine): def getProxy(self, engine, script_engine):
"""Hackish way to ensure the proxy is already created """Hackish way to ensure the proxy is already created
@ -511,6 +607,7 @@ class SimulationView(CuraView):
Application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged) Application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged) self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged)
self.calculateColorSchemeLimits()
self.calculateMaxLayers() self.calculateMaxLayers()
self.calculateMaxPathsOnLayer(self._current_layer_num) self.calculateMaxPathsOnLayer(self._current_layer_num)

View File

@ -90,6 +90,7 @@ Cura.ExpandableComponent
property bool show_feedrate_gradient: show_gradient && UM.Preferences.getValue("layerview/layer_view_type") == 2 property bool show_feedrate_gradient: show_gradient && UM.Preferences.getValue("layerview/layer_view_type") == 2
property bool show_thickness_gradient: show_gradient && UM.Preferences.getValue("layerview/layer_view_type") == 3 property bool show_thickness_gradient: show_gradient && UM.Preferences.getValue("layerview/layer_view_type") == 3
property bool show_line_width_gradient: show_gradient && UM.Preferences.getValue("layerview/layer_view_type") == 4 property bool show_line_width_gradient: show_gradient && UM.Preferences.getValue("layerview/layer_view_type") == 4
property bool show_flow_rate_gradient: show_gradient && UM.Preferences.getValue("layerview/layer_view_type") == 5
property bool only_show_top_layers: UM.Preferences.getValue("view/only_show_top_layers") property bool only_show_top_layers: UM.Preferences.getValue("view/only_show_top_layers")
property int top_layer_count: UM.Preferences.getValue("view/top_layer_count") property int top_layer_count: UM.Preferences.getValue("view/top_layer_count")
@ -125,6 +126,10 @@ Cura.ExpandableComponent
text: catalog.i18nc("@label:listbox", "Line Width"), text: catalog.i18nc("@label:listbox", "Line Width"),
type_id: 4 type_id: 4
}) })
layerViewTypes.append({
text: catalog.i18nc("@label:listbox", "Flow"),
type_id: 5
})
} }
ComboBox ComboBox
@ -150,10 +155,13 @@ Cura.ExpandableComponent
{ {
// Update the visibility of the legends. // Update the visibility of the legends.
viewSettings.show_legend = UM.SimulationView.compatibilityMode || (type_id == 1); viewSettings.show_legend = UM.SimulationView.compatibilityMode || (type_id == 1);
viewSettings.show_gradient = !UM.SimulationView.compatibilityMode && (type_id == 2 || type_id == 3 || type_id == 4); viewSettings.show_gradient = !UM.SimulationView.compatibilityMode &&
(type_id == 2 || type_id == 3 || type_id == 4 || type_id == 5) ;
viewSettings.show_feedrate_gradient = viewSettings.show_gradient && (type_id == 2); viewSettings.show_feedrate_gradient = viewSettings.show_gradient && (type_id == 2);
viewSettings.show_thickness_gradient = viewSettings.show_gradient && (type_id == 3); viewSettings.show_thickness_gradient = viewSettings.show_gradient && (type_id == 3);
viewSettings.show_line_width_gradient = viewSettings.show_gradient && (type_id == 4); viewSettings.show_line_width_gradient = viewSettings.show_gradient && (type_id == 4);
viewSettings.show_flow_rate_gradient = viewSettings.show_gradient && (type_id == 5);
} }
} }
@ -389,18 +397,24 @@ Cura.ExpandableComponent
// Feedrate selected // Feedrate selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 2) if (UM.Preferences.getValue("layerview/layer_view_type") == 2)
{ {
return parseFloat(UM.SimulationView.getMinFeedrate()).toFixed(2) return parseFloat(UM.SimulationView.minFeedrate).toFixed(2)
} }
// Layer thickness selected // Layer thickness selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 3) if (UM.Preferences.getValue("layerview/layer_view_type") == 3)
{ {
return parseFloat(UM.SimulationView.getMinThickness()).toFixed(2) return parseFloat(UM.SimulationView.minThickness).toFixed(2)
} }
// Line width selected // Line width selected
if(UM.Preferences.getValue("layerview/layer_view_type") == 4) if(UM.Preferences.getValue("layerview/layer_view_type") == 4)
{ {
return parseFloat(UM.SimulationView.getMinLineWidth()).toFixed(2); return parseFloat(UM.SimulationView.minLineWidth).toFixed(2);
} }
// Flow Rate selected
if(UM.Preferences.getValue("layerview/layer_view_type") == 5)
{
return parseFloat(UM.SimulationView.minFlowRate).toFixed(2);
}
} }
return catalog.i18nc("@label","min") return catalog.i18nc("@label","min")
} }
@ -431,6 +445,11 @@ Cura.ExpandableComponent
{ {
return "mm" return "mm"
} }
// Flow Rate selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 5)
{
return "mm³/s"
}
} }
return "" return ""
} }
@ -448,17 +467,22 @@ Cura.ExpandableComponent
// Feedrate selected // Feedrate selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 2) if (UM.Preferences.getValue("layerview/layer_view_type") == 2)
{ {
return parseFloat(UM.SimulationView.getMaxFeedrate()).toFixed(2) return parseFloat(UM.SimulationView.maxFeedrate).toFixed(2)
} }
// Layer thickness selected // Layer thickness selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 3) if (UM.Preferences.getValue("layerview/layer_view_type") == 3)
{ {
return parseFloat(UM.SimulationView.getMaxThickness()).toFixed(2) return parseFloat(UM.SimulationView.maxThickness).toFixed(2)
} }
//Line width selected //Line width selected
if(UM.Preferences.getValue("layerview/layer_view_type") == 4) if(UM.Preferences.getValue("layerview/layer_view_type") == 4)
{ {
return parseFloat(UM.SimulationView.getMaxLineWidth()).toFixed(2); return parseFloat(UM.SimulationView.maxLineWidth).toFixed(2);
}
// Flow rate selected
if(UM.Preferences.getValue("layerview/layer_view_type") == 5)
{
return parseFloat(UM.SimulationView.maxFlowRate).toFixed(2);
} }
} }
return catalog.i18nc("@label","max") return catalog.i18nc("@label","max")
@ -474,7 +498,10 @@ Cura.ExpandableComponent
Rectangle Rectangle
{ {
id: feedrateGradient id: feedrateGradient
visible: viewSettings.show_feedrate_gradient || viewSettings.show_line_width_gradient visible: (
viewSettings.show_feedrate_gradient ||
viewSettings.show_line_width_gradient
)
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
height: Math.round(UM.Theme.getSize("layerview_row").height * 1.5) height: Math.round(UM.Theme.getSize("layerview_row").height * 1.5)
@ -526,7 +553,9 @@ Cura.ExpandableComponent
Rectangle Rectangle
{ {
id: thicknessGradient id: thicknessGradient
visible: viewSettings.show_thickness_gradient visible: (
viewSettings.show_thickness_gradient
)
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
height: Math.round(UM.Theme.getSize("layerview_row").height * 1.5) height: Math.round(UM.Theme.getSize("layerview_row").height * 1.5)
@ -578,6 +607,85 @@ Cura.ExpandableComponent
} }
} }
} }
// Gradient colors for flow (similar to jet colormap)
Rectangle
{
id: jetGradient
visible: (
viewSettings.show_flow_rate_gradient
)
anchors.left: parent.left
anchors.right: parent.right
height: Math.round(UM.Theme.getSize("layerview_row").height * 1.5)
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
LinearGradient
{
anchors
{
left: parent.left
leftMargin: UM.Theme.getSize("default_lining").width
right: parent.right
rightMargin: UM.Theme.getSize("default_lining").width
top: parent.top
topMargin: UM.Theme.getSize("default_lining").width
bottom: parent.bottom
bottomMargin: UM.Theme.getSize("default_lining").width
}
start: Qt.point(0, 0)
end: Qt.point(parent.width, 0)
gradient: Gradient
{
GradientStop
{
position: 0.0
color: Qt.rgba(0, 0, 0.5, 1)
}
GradientStop
{
position: 0.125
color: Qt.rgba(0, 0.0, 1.0, 1)
}
GradientStop
{
position: 0.25
color: Qt.rgba(0, 0.5, 1.0, 1)
}
GradientStop
{
position: 0.375
color: Qt.rgba(0.0, 1.0, 1.0, 1)
}
GradientStop
{
position: 0.5
color: Qt.rgba(0.5, 1.0, 0.5, 1)
}
GradientStop
{
position: 0.625
color: Qt.rgba(1.0, 1.0, 0.0, 1)
}
GradientStop
{
position: 0.75
color: Qt.rgba(1.0, 0.5, 0, 1)
}
GradientStop
{
position: 0.875
color: Qt.rgba(1.0, 0.0, 0, 1)
}
GradientStop
{
position: 1.0
color: Qt.rgba(0.5, 0, 0, 1)
}
}
}
}
} }
FontMetrics FontMetrics

View File

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -28,6 +28,7 @@ class SimulationViewProxy(QObject):
globalStackChanged = pyqtSignal() globalStackChanged = pyqtSignal()
preferencesChanged = pyqtSignal() preferencesChanged = pyqtSignal()
busyChanged = pyqtSignal() busyChanged = pyqtSignal()
colorSchemeLimitsChanged = pyqtSignal()
@pyqtProperty(bool, notify=activityChanged) @pyqtProperty(bool, notify=activityChanged)
def layerActivity(self): def layerActivity(self):
@ -101,30 +102,38 @@ class SimulationViewProxy(QObject):
def getSimulationRunning(self): def getSimulationRunning(self):
return self._simulation_view.isSimulationRunning() return self._simulation_view.isSimulationRunning()
@pyqtSlot(result=float) @pyqtProperty(float, notify = colorSchemeLimitsChanged)
def getMinFeedrate(self): def minFeedrate(self):
return self._simulation_view.getMinFeedrate() return self._simulation_view.getMinFeedrate()
@pyqtSlot(result=float) @pyqtProperty(float, notify = colorSchemeLimitsChanged)
def getMaxFeedrate(self): def maxFeedrate(self):
return self._simulation_view.getMaxFeedrate() return self._simulation_view.getMaxFeedrate()
@pyqtSlot(result=float) @pyqtProperty(float, notify = colorSchemeLimitsChanged)
def getMinThickness(self): def minThickness(self):
return self._simulation_view.getMinThickness() return self._simulation_view.getMinThickness()
@pyqtSlot(result=float) @pyqtProperty(float, notify = colorSchemeLimitsChanged)
def getMaxThickness(self): def maxThickness(self):
return self._simulation_view.getMaxThickness() return self._simulation_view.getMaxThickness()
@pyqtSlot(result=float) @pyqtProperty(float, notify = colorSchemeLimitsChanged)
def getMaxLineWidth(self): def maxLineWidth(self):
return self._simulation_view.getMaxLineWidth() return self._simulation_view.getMaxLineWidth()
@pyqtSlot(result=float) @pyqtProperty(float, notify = colorSchemeLimitsChanged)
def getMinLineWidth(self): def minLineWidth(self):
return self._simulation_view.getMinLineWidth() return self._simulation_view.getMinLineWidth()
@pyqtProperty(float, notify=colorSchemeLimitsChanged)
def maxFlowRate(self):
return self._simulation_view.getMaxFlowRate()
@pyqtProperty(float, notify=colorSchemeLimitsChanged)
def minFlowRate(self):
return self._simulation_view.getMinFlowRate()
# Opacity 0..1 # Opacity 0..1
@pyqtSlot(int, float) @pyqtSlot(int, float)
def setExtruderOpacity(self, extruder_nr, opacity): def setExtruderOpacity(self, extruder_nr, opacity):
@ -153,6 +162,9 @@ class SimulationViewProxy(QObject):
self.currentLayerChanged.emit() self.currentLayerChanged.emit()
self._layerActivityChanged() self._layerActivityChanged()
def _onColorSchemeLimitsChanged(self):
self.colorSchemeLimitsChanged.emit()
def _onPathChanged(self): def _onPathChanged(self):
self.currentPathChanged.emit() self.currentPathChanged.emit()
self._layerActivityChanged() self._layerActivityChanged()
@ -182,6 +194,7 @@ class SimulationViewProxy(QObject):
active_view = self._controller.getActiveView() active_view = self._controller.getActiveView()
if active_view == self._simulation_view: if active_view == self._simulation_view:
self._simulation_view.currentLayerNumChanged.connect(self._onLayerChanged) self._simulation_view.currentLayerNumChanged.connect(self._onLayerChanged)
self._simulation_view.colorSchemeLimitsChanged.connect(self._onColorSchemeLimitsChanged)
self._simulation_view.currentPathNumChanged.connect(self._onPathChanged) self._simulation_view.currentPathNumChanged.connect(self._onPathChanged)
self._simulation_view.maxLayersChanged.connect(self._onMaxLayersChanged) self._simulation_view.maxLayersChanged.connect(self._onMaxLayersChanged)
self._simulation_view.maxPathsChanged.connect(self._onMaxPathsChanged) self._simulation_view.maxPathsChanged.connect(self._onMaxPathsChanged)
@ -194,6 +207,7 @@ class SimulationViewProxy(QObject):
# Disconnect all of em again. # Disconnect all of em again.
self.is_simulationView_selected = False self.is_simulationView_selected = False
self._simulation_view.currentLayerNumChanged.disconnect(self._onLayerChanged) self._simulation_view.currentLayerNumChanged.disconnect(self._onLayerChanged)
self._simulation_view.colorSchemeLimitsChanged.connect(self._onColorSchemeLimitsChanged)
self._simulation_view.currentPathNumChanged.disconnect(self._onPathChanged) self._simulation_view.currentPathNumChanged.disconnect(self._onPathChanged)
self._simulation_view.maxLayersChanged.disconnect(self._onMaxLayersChanged) self._simulation_view.maxLayersChanged.disconnect(self._onMaxLayersChanged)
self._simulation_view.maxPathsChanged.disconnect(self._onMaxPathsChanged) self._simulation_view.maxPathsChanged.disconnect(self._onMaxPathsChanged)

View File

@ -12,6 +12,8 @@ vertex41core =
uniform lowp float u_min_thickness; uniform lowp float u_min_thickness;
uniform lowp float u_max_line_width; uniform lowp float u_max_line_width;
uniform lowp float u_min_line_width; uniform lowp float u_min_line_width;
uniform lowp float u_max_flow_rate;
uniform lowp float u_min_flow_rate;
uniform lowp int u_layer_view_type; uniform lowp int u_layer_view_type;
uniform lowp mat4 u_extruder_opacity; // currently only for max 16 extruders, others always visible uniform lowp mat4 u_extruder_opacity; // currently only for max 16 extruders, others always visible
@ -44,7 +46,15 @@ vertex41core =
vec4 feedrateGradientColor(float abs_value, float min_value, float max_value) vec4 feedrateGradientColor(float abs_value, float min_value, float max_value)
{ {
float value = (abs_value - min_value)/(max_value - min_value); float value;
if(abs(max_value - min_value) < 0.0001) //Max and min are equal (barring floating point rounding errors).
{
value = 0.5; //Pick a colour in exactly the middle of the range.
}
else
{
value = (abs_value - min_value) / (max_value - min_value);
}
float red = value; float red = value;
float green = 1-abs(1-4*value); float green = 1-abs(1-4*value);
if (value > 0.375) if (value > 0.375)
@ -57,7 +67,15 @@ vertex41core =
vec4 layerThicknessGradientColor(float abs_value, float min_value, float max_value) vec4 layerThicknessGradientColor(float abs_value, float min_value, float max_value)
{ {
float value = (abs_value - min_value)/(max_value - min_value); float value;
if(abs(max_value - min_value) < 0.0001) //Max and min are equal (barring floating point rounding errors).
{
value = 0.5; //Pick a colour in exactly the middle of the range.
}
else
{
value = (abs_value - min_value) / (max_value - min_value);
}
float red = min(max(4*value-2, 0), 1); float red = min(max(4*value-2, 0), 1);
float green = min(1.5*value, 0.75); float green = min(1.5*value, 0.75);
if (value > 0.75) if (value > 0.75)
@ -70,7 +88,15 @@ vertex41core =
vec4 lineWidthGradientColor(float abs_value, float min_value, float max_value) vec4 lineWidthGradientColor(float abs_value, float min_value, float max_value)
{ {
float value = (abs_value - min_value) / (max_value - min_value); float value;
if(abs(max_value - min_value) < 0.0001) //Max and min are equal (barring floating point rounding errors).
{
value = 0.5; //Pick a colour in exactly the middle of the range.
}
else
{
value = (abs_value - min_value) / (max_value - min_value);
}
float red = value; float red = value;
float green = 1 - abs(1 - 4 * value); float green = 1 - abs(1 - 4 * value);
if(value > 0.375) if(value > 0.375)
@ -81,6 +107,30 @@ vertex41core =
return vec4(red, green, blue, 1.0); return vec4(red, green, blue, 1.0);
} }
float clamp(float v)
{
float t = v < 0 ? 0 : v;
return t > 1.0 ? 1.0 : t;
}
// Inspired by https://stackoverflow.com/a/46628410
vec4 flowRateGradientColor(float abs_value, float min_value, float max_value)
{
float t;
if(abs(min_value - max_value) < 0.0001)
{
t = 0;
}
else
{
t = 2.0 * ((abs_value - min_value) / (max_value - min_value)) - 1;
}
float red = clamp(1.5 - abs(2.0 * t - 1.0));
float green = clamp(1.5 - abs(2.0 * t));
float blue = clamp(1.5 - abs(2.0 * t + 1.0));
return vec4(red, green, blue, 1.0);
}
void main() void main()
{ {
vec4 v1_vertex = a_vertex; vec4 v1_vertex = a_vertex;
@ -106,6 +156,10 @@ vertex41core =
case 4: // "Line width" case 4: // "Line width"
v_color = lineWidthGradientColor(a_line_dim.x, u_min_line_width, u_max_line_width); v_color = lineWidthGradientColor(a_line_dim.x, u_min_line_width, u_max_line_width);
break; break;
case 5: // "Flow"
float flow_rate = a_line_dim.x * a_line_dim.y * a_feedrate;
v_color = flowRateGradientColor(flow_rate, u_min_flow_rate, u_max_flow_rate);
break;
} }
v_vertex = world_space_vert.xyz; v_vertex = world_space_vert.xyz;
@ -294,7 +348,6 @@ geometry41core =
EndPrimitive(); EndPrimitive();
} }
if ((u_show_starts == 1) && (v_prev_line_type[0] != 1) && (v_line_type[0] == 1)) { if ((u_show_starts == 1) && (v_prev_line_type[0] != 1) && (v_line_type[0] == 1)) {
float w = size_x; float w = size_x;
float h = size_y; float h = size_y;

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides the Simulation view.", "description": "Provides the Simulation view.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -229,6 +229,11 @@ class SliceInfo(QObject, Extension):
model["model_settings"] = model_settings model["model_settings"] = model_settings
if node.source_mime_type is None:
model["mime_type"] = ""
else:
model["mime_type"] = node.source_mime_type.name
data["models"].append(model) data["models"].append(model)
print_times = print_information.printTimes() print_times = print_information.printTimes()

View File

@ -54,6 +54,7 @@
<li><b>Bounding Box:</b> [minimum x, y, z; maximum x, y, z]</li> <li><b>Bounding Box:</b> [minimum x, y, z; maximum x, y, z]</li>
<li><b>Is Helper Mesh:</b> no</li> <li><b>Is Helper Mesh:</b> no</li>
<li><b>Helper Mesh Type:</b> support mesh</li> <li><b>Helper Mesh Type:</b> support mesh</li>
<li><b>File type:</b> STL</li>
</ul> </ul>
</li> </li>
</ul> </ul>

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Submits anonymous slice info. Can be disabled through preferences.", "description": "Submits anonymous slice info. Can be disabled through preferences.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides a normal solid mesh view.", "description": "Provides a normal solid mesh view.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Creates an eraser mesh to block the printing of support in certain places", "description": "Creates an eraser mesh to block the printing of support in certain places",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -2,6 +2,6 @@
"name": "Toolbox", "name": "Toolbox",
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"api": "7.5.0", "api": "7.6.0",
"description": "Find, manage and install new Cura packages." "description": "Find, manage and install new Cura packages."
} }

View File

@ -3,5 +3,5 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides support for reading model files.", "description": "Provides support for reading model files.",
"api": "7.5.0" "api": "7.6.0"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides support for reading Ultimaker Format Packages.", "description": "Provides support for reading Ultimaker Format Packages.",
"supported_sdk_versions": ["7.5.0"], "supported_sdk_versions": ["7.6.0"],
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -5,6 +5,7 @@ from typing import cast, List, Dict
from Charon.VirtualFile import VirtualFile # To open UFP files. from Charon.VirtualFile import VirtualFile # To open UFP files.
from Charon.OpenMode import OpenMode # To indicate that we want to write to UFP files. from Charon.OpenMode import OpenMode # To indicate that we want to write to UFP files.
from Charon.filetypes.OpenPackagingConvention import OPCError
from io import StringIO # For converting g-code to bytes. from io import StringIO # For converting g-code to bytes.
from PyQt5.QtCore import QBuffer from PyQt5.QtCore import QBuffer
@ -47,24 +48,37 @@ class UFPWriter(MeshWriter):
archive = VirtualFile() archive = VirtualFile()
archive.openStream(stream, "application/x-ufp", OpenMode.WriteOnly) archive.openStream(stream, "application/x-ufp", OpenMode.WriteOnly)
try:
self._writeObjectList(archive) self._writeObjectList(archive)
# Store the g-code from the scene. # Store the g-code from the scene.
archive.addContentType(extension = "gcode", mime_type = "text/x-gcode") archive.addContentType(extension = "gcode", mime_type = "text/x-gcode")
except EnvironmentError as e:
error_msg = catalog.i18nc("@info:error", "Can't write to UFP file:") + " " + str(e)
self.setInformation(error_msg)
Logger.error(error_msg)
return False
gcode_textio = StringIO() # We have to convert the g-code into bytes. gcode_textio = StringIO() # We have to convert the g-code into bytes.
gcode_writer = cast(MeshWriter, PluginRegistry.getInstance().getPluginObject("GCodeWriter")) gcode_writer = cast(MeshWriter, PluginRegistry.getInstance().getPluginObject("GCodeWriter"))
success = gcode_writer.write(gcode_textio, None) success = gcode_writer.write(gcode_textio, None)
if not success: # Writing the g-code failed. Then I can also not write the gzipped g-code. if not success: # Writing the g-code failed. Then I can also not write the gzipped g-code.
self.setInformation(gcode_writer.getInformation()) self.setInformation(gcode_writer.getInformation())
return False return False
try:
gcode = archive.getStream("/3D/model.gcode") gcode = archive.getStream("/3D/model.gcode")
gcode.write(gcode_textio.getvalue().encode("UTF-8")) gcode.write(gcode_textio.getvalue().encode("UTF-8"))
archive.addRelation(virtual_path = "/3D/model.gcode", relation_type = "http://schemas.ultimaker.org/package/2018/relationships/gcode") archive.addRelation(virtual_path = "/3D/model.gcode", relation_type = "http://schemas.ultimaker.org/package/2018/relationships/gcode")
except EnvironmentError as e:
error_msg = catalog.i18nc("@info:error", "Can't write to UFP file:") + " " + str(e)
self.setInformation(error_msg)
Logger.error(error_msg)
return False
# Attempt to store the thumbnail, if any: # Attempt to store the thumbnail, if any:
backend = CuraApplication.getInstance().getBackend() backend = CuraApplication.getInstance().getBackend()
snapshot = None if getattr(backend, "getLatestSnapshot", None) is None else backend.getLatestSnapshot() snapshot = None if getattr(backend, "getLatestSnapshot", None) is None else backend.getLatestSnapshot()
if snapshot: if snapshot:
try:
archive.addContentType(extension = "png", mime_type = "image/png") archive.addContentType(extension = "png", mime_type = "image/png")
thumbnail = archive.getStream("/Metadata/thumbnail.png") thumbnail = archive.getStream("/Metadata/thumbnail.png")
@ -76,6 +90,11 @@ class UFPWriter(MeshWriter):
archive.addRelation(virtual_path = "/Metadata/thumbnail.png", archive.addRelation(virtual_path = "/Metadata/thumbnail.png",
relation_type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail", relation_type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail",
origin = "/3D/model.gcode") origin = "/3D/model.gcode")
except EnvironmentError as e:
error_msg = catalog.i18nc("@info:error", "Can't write to UFP file:") + " " + str(e)
self.setInformation(error_msg)
Logger.error(error_msg)
return False
else: else:
Logger.log("w", "Thumbnail not created, cannot save it") Logger.log("w", "Thumbnail not created, cannot save it")
@ -90,7 +109,7 @@ class UFPWriter(MeshWriter):
try: try:
archive.addContentType(extension = material_extension, mime_type = material_mime_type) archive.addContentType(extension = material_extension, mime_type = material_mime_type)
except: except OPCError:
Logger.log("w", "The material extension: %s was already added", material_extension) Logger.log("w", "The material extension: %s was already added", material_extension)
added_materials = [] added_materials = []
@ -120,17 +139,23 @@ class UFPWriter(MeshWriter):
Logger.log("e", "Unable serialize material container with root id: %s", material_root_id) Logger.log("e", "Unable serialize material container with root id: %s", material_root_id)
return False return False
try:
material_file = archive.getStream(material_file_name) material_file = archive.getStream(material_file_name)
material_file.write(serialized_material.encode("UTF-8")) material_file.write(serialized_material.encode("UTF-8"))
archive.addRelation(virtual_path = material_file_name, archive.addRelation(virtual_path = material_file_name,
relation_type = "http://schemas.ultimaker.org/package/2018/relationships/material", relation_type = "http://schemas.ultimaker.org/package/2018/relationships/material",
origin = "/3D/model.gcode") origin = "/3D/model.gcode")
except EnvironmentError as e:
error_msg = catalog.i18nc("@info:error", "Can't write to UFP file:") + " " + str(e)
self.setInformation(error_msg)
Logger.error(error_msg)
return False
added_materials.append(material_file_name) added_materials.append(material_file_name)
try: try:
archive.close() archive.close()
except OSError as e: except EnvironmentError as e:
error_msg = catalog.i18nc("@info:error", "Can't write to UFP file:") + " " + str(e) error_msg = catalog.i18nc("@info:error", "Can't write to UFP file:") + " " + str(e)
self.setInformation(error_msg) self.setInformation(error_msg)
Logger.error(error_msg) Logger.error(error_msg)

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides support for writing Ultimaker Format Packages.", "description": "Provides support for writing Ultimaker Format Packages.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"description": "Manages network connections to Ultimaker networked printers.", "description": "Manages network connections to Ultimaker networked printers.",
"version": "2.0.0", "version": "2.0.0",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -7,26 +7,34 @@ from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from UM.Job import Job from UM.Job import Job
from UM.Logger import Logger from UM.Logger import Logger
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.Utils.Threading import call_on_qt_thread
from ..Models.Http.ClusterMaterial import ClusterMaterial from ..Models.Http.ClusterMaterial import ClusterMaterial
from ..Models.LocalMaterial import LocalMaterial from ..Models.LocalMaterial import LocalMaterial
from ..Messages.MaterialSyncMessage import MaterialSyncMessage from ..Messages.MaterialSyncMessage import MaterialSyncMessage
import time
import threading
if TYPE_CHECKING: if TYPE_CHECKING:
from .LocalClusterOutputDevice import LocalClusterOutputDevice from .LocalClusterOutputDevice import LocalClusterOutputDevice
class SendMaterialJob(Job): class SendMaterialJob(Job):
"""Asynchronous job to send material profiles to the printer. """Asynchronous job to send material profiles to the printer.
This way it won't freeze up the interface while sending those materials. This way it won't freeze up the interface while sending those materials.
""" """
def __init__(self, device: "LocalClusterOutputDevice") -> None: def __init__(self, device: "LocalClusterOutputDevice") -> None:
super().__init__() super().__init__()
self.device = device # type: LocalClusterOutputDevice self.device = device # type: LocalClusterOutputDevice
self._send_material_thread = threading.Thread(target = self._sendMissingMaterials)
self._send_material_thread.setDaemon(True)
self._remote_materials = {} # type: Dict[str, ClusterMaterial]
def run(self) -> None: def run(self) -> None:
"""Send the request to the printer and register a callback""" """Send the request to the printer and register a callback"""
@ -36,9 +44,15 @@ class SendMaterialJob(Job):
"""Callback for when the remote materials were returned.""" """Callback for when the remote materials were returned."""
remote_materials_by_guid = {material.guid: material for material in materials} remote_materials_by_guid = {material.guid: material for material in materials}
self._sendMissingMaterials(remote_materials_by_guid) self._remote_materials = remote_materials_by_guid
# It's not the nicest way to do it, but if we don't handle this in a thread
# we are blocking the main interface (even though the original call was done in a job)
# This should really be refactored so that calculating the list of materials that need to be sent
# to the printer is done outside of the job (and running the job actually sends the materials)
# TODO: Fix this hack that was introduced for 4.9.1
self._send_material_thread.start()
def _sendMissingMaterials(self, remote_materials_by_guid: Dict[str, ClusterMaterial]) -> None: def _sendMissingMaterials(self) -> None:
"""Determine which materials should be updated and send them to the printer. """Determine which materials should be updated and send them to the printer.
:param remote_materials_by_guid: The remote materials by GUID. :param remote_materials_by_guid: The remote materials by GUID.
@ -47,7 +61,7 @@ class SendMaterialJob(Job):
if len(local_materials_by_guid) == 0: if len(local_materials_by_guid) == 0:
Logger.log("d", "There are no local materials to synchronize with the printer.") Logger.log("d", "There are no local materials to synchronize with the printer.")
return return
material_ids_to_send = self._determineMaterialsToSend(local_materials_by_guid, remote_materials_by_guid) material_ids_to_send = self._determineMaterialsToSend(local_materials_by_guid, self._remote_materials)
if len(material_ids_to_send) == 0: if len(material_ids_to_send) == 0:
Logger.log("d", "There are no remote materials to update.") Logger.log("d", "There are no remote materials to update.")
return return
@ -96,7 +110,11 @@ class SendMaterialJob(Job):
file_name = os.path.basename(file_path) file_name = os.path.basename(file_path)
self._sendMaterialFile(file_path, file_name, root_material_id) self._sendMaterialFile(file_path, file_name, root_material_id)
time.sleep(1) # Throttle the sending a bit.
# This needs to be called on the QT thread since the onFinished needs to happen
# in the same thread as where the network manager is located (aka; main thread)
@call_on_qt_thread
def _sendMaterialFile(self, file_path: str, file_name: str, material_id: str) -> None: def _sendMaterialFile(self, file_path: str, file_name: str, material_id: str) -> None:
"""Send a single material file to the printer. """Send a single material file to the printer.

View File

@ -1,9 +1,2 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2019 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.
# Workaround for a race condition on certain systems where there
# is a race condition between Arcus and PyQt. Importing Arcus
# first seems to prevent Sip from going into a state where it
# tries to create PyQt objects on a non-main thread.
import Arcus #@UnusedImport
import Savitar #@UnusedImport

View File

@ -70,7 +70,10 @@ class AutoDetectBaudJob(Job):
timeout_time = time() + wait_response_timeout timeout_time = time() + wait_response_timeout
while timeout_time > time(): while timeout_time > time():
line = serial.readline() # If baudrate is wrong, then readline() might never
# return, even with timeouts set. Using read_until
# with size limit seems to fix this.
line = serial.read_until(size = 100)
if b"ok" in line and b"T:" in line: if b"ok" in line and b"T:" in line:
self.setResult(baud_rate) self.setResult(baud_rate)
Logger.log("d", "Detected baud rate {baud_rate} on serial {serial} on retry {retry} with after {time_elapsed:0.2f} seconds.".format( Logger.log("d", "Detected baud rate {baud_rate} on serial {serial} on retry {retry} with after {time_elapsed:0.2f} seconds.".format(

View File

@ -2,7 +2,7 @@
"name": "USB printing", "name": "USB printing",
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.2", "version": "1.0.2",
"api": "7.5.0", "api": "7.6.0",
"description": "Accepts G-Code and sends them to a printer. Plugin can also update firmware.", "description": "Accepts G-Code and sends them to a printer. Plugin can also update firmware.",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides machine actions for Ultimaker machines (such as bed leveling wizard, selecting upgrades, etc.).", "description": "Provides machine actions for Ultimaker machines (such as bed leveling wizard, selecting upgrades, etc.).",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Upgrades configurations from Cura 2.1 to Cura 2.2.", "description": "Upgrades configurations from Cura 2.1 to Cura 2.2.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Upgrades configurations from Cura 2.2 to Cura 2.4.", "description": "Upgrades configurations from Cura 2.2 to Cura 2.4.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Upgrades configurations from Cura 2.5 to Cura 2.6.", "description": "Upgrades configurations from Cura 2.5 to Cura 2.6.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Upgrades configurations from Cura 2.6 to Cura 2.7.", "description": "Upgrades configurations from Cura 2.6 to Cura 2.7.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Upgrades configurations from Cura 2.7 to Cura 3.0.", "description": "Upgrades configurations from Cura 2.7 to Cura 3.0.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Upgrades configurations from Cura 3.0 to Cura 3.1.", "description": "Upgrades configurations from Cura 3.0 to Cura 3.1.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Upgrades configurations from Cura 3.2 to Cura 3.3.", "description": "Upgrades configurations from Cura 3.2 to Cura 3.3.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Upgrades configurations from Cura 3.3 to Cura 3.4.", "description": "Upgrades configurations from Cura 3.3 to Cura 3.4.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Upgrades configurations from Cura 3.4 to Cura 3.5.", "description": "Upgrades configurations from Cura 3.4 to Cura 3.5.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Upgrades configurations from Cura 3.5 to Cura 4.0.", "description": "Upgrades configurations from Cura 3.5 to Cura 4.0.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Upgrades configurations from Cura 4.0 to Cura 4.1.", "description": "Upgrades configurations from Cura 4.0 to Cura 4.1.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Upgrades configurations from Cura 4.1 to Cura 4.2.", "description": "Upgrades configurations from Cura 4.1 to Cura 4.2.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Upgrades configurations from Cura 4.2 to Cura 4.3.", "description": "Upgrades configurations from Cura 4.2 to Cura 4.3.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Upgrades configurations from Cura 4.3 to Cura 4.4.", "description": "Upgrades configurations from Cura 4.3 to Cura 4.4.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Upgrades configurations from Cura 4.4 to Cura 4.5.", "description": "Upgrades configurations from Cura 4.4 to Cura 4.5.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Upgrades configurations from Cura 4.5 to Cura 4.6.", "description": "Upgrades configurations from Cura 4.5 to Cura 4.6.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Upgrades configurations from Cura 4.6.0 to Cura 4.6.2.", "description": "Upgrades configurations from Cura 4.6.0 to Cura 4.6.2.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Upgrades configurations from Cura 4.6.2 to Cura 4.7.", "description": "Upgrades configurations from Cura 4.6.2 to Cura 4.7.",
"api": "7.5.0", "api": "7.6.0",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

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