Merge branch 'Ultimaker:master' into master

This commit is contained in:
NilsRo 2021-07-06 07:50:00 +02:00 committed by GitHub
commit 235b9a9b4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3952 changed files with 135703 additions and 88964 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

@ -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

@ -7,13 +7,14 @@ import re
import shutil import shutil
from copy import deepcopy from copy import deepcopy
from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile
from typing import Dict, Optional, TYPE_CHECKING from typing import Dict, Optional, TYPE_CHECKING, List
from UM import i18nCatalog from UM import i18nCatalog
from UM.Logger import Logger 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 = [] # type: List[str]
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.
@ -163,6 +166,9 @@ class Backup:
Logger.log("d", "Moving preferences file from %s to %s", backup_preferences_file, preferences_file) Logger.log("d", "Moving preferences file from %s to %s", backup_preferences_file, preferences_file)
shutil.move(backup_preferences_file, preferences_file) shutil.move(backup_preferences_file, preferences_file)
# Read the preferences from the newly restored configuration (or else the cached Preferences will override the restored ones)
self._application.readPreferencesFromConfiguration()
# Restore the obfuscated settings # Restore the obfuscated settings
self._illuminate(**secrets) self._illuminate(**secrets)
@ -187,11 +193,13 @@ class Backup:
Logger.log("d", "Removing current data in location: %s", target_path) Logger.log("d", "Removing current data in location: %s", target_path)
Resources.factoryReset() Resources.factoryReset()
Logger.log("d", "Extracting backup to location: %s", target_path) Logger.log("d", "Extracting backup to location: %s", target_path)
try: name_list = archive.namelist()
archive.extractall(target_path) for archive_filename in name_list:
except (PermissionError, EnvironmentError): try:
Logger.logException("e", "Unable to extract the backup due to permission or file system errors.") archive.extract(archive_filename, target_path)
return False except (PermissionError, EnvironmentError):
Logger.logException("e", f"Unable to extract the file {archive_filename} from the backup due to permission or file system errors.")
CuraApplication.getInstance().processEvents()
return True return True
def _obfuscate(self) -> Dict[str, str]: def _obfuscate(self) -> Dict[str, str]:

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,7 @@ 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()
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

@ -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 numpy import numpy
@ -916,6 +916,8 @@ class BuildVolume(SceneNode):
return {} return {}
for area in self._global_container_stack.getProperty("machine_disallowed_areas", "value"): for area in self._global_container_stack.getProperty("machine_disallowed_areas", "value"):
if len(area) == 0:
continue # Numpy doesn't deal well with 0-length arrays, since it can't determine the dimensionality of them.
polygon = Polygon(numpy.array(area, numpy.float32)) polygon = Polygon(numpy.array(area, numpy.float32))
polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(border_size)) polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(border_size))
machine_disallowed_polygons.append(polygon) machine_disallowed_polygons.append(polygon)

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

@ -129,7 +129,7 @@ class CuraApplication(QtApplication):
# SettingVersion represents the set of settings available in the machine/extruder definitions. # SettingVersion represents the set of settings available in the machine/extruder definitions.
# You need to make sure that this version number needs to be increased if there is any non-backwards-compatible # You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
# changes of the settings. # changes of the settings.
SettingVersion = 16 SettingVersion = 17
Created = False Created = False
@ -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
@ -705,6 +708,8 @@ class CuraApplication(QtApplication):
@pyqtSlot(str) @pyqtSlot(str)
def discardOrKeepProfileChangesClosed(self, option: str) -> None: def discardOrKeepProfileChangesClosed(self, option: str) -> None:
global_stack = self.getGlobalContainerStack() global_stack = self.getGlobalContainerStack()
if global_stack is None:
return
if option == "discard": if option == "discard":
for extruder in global_stack.extruderList: for extruder in global_stack.extruderList:
extruder.userChanges.clear() extruder.userChanges.clear()
@ -1526,12 +1531,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 +1547,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 +1868,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

@ -53,6 +53,9 @@ class ExtrudersModel(ListModel):
EnabledRole = Qt.UserRole + 11 EnabledRole = Qt.UserRole + 11
"""Is the extruder enabled?""" """Is the extruder enabled?"""
MaterialTypeRole = Qt.UserRole + 12
"""The type of the material (e.g. PLA, ABS, PETG, etc.)."""
defaultColors = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"] defaultColors = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"]
"""List of colours to display if there is no material or the material has no known colour. """ """List of colours to display if there is no material or the material has no known colour. """
@ -75,6 +78,7 @@ class ExtrudersModel(ListModel):
self.addRoleName(self.StackRole, "stack") self.addRoleName(self.StackRole, "stack")
self.addRoleName(self.MaterialBrandRole, "material_brand") self.addRoleName(self.MaterialBrandRole, "material_brand")
self.addRoleName(self.ColorNameRole, "color_name") self.addRoleName(self.ColorNameRole, "color_name")
self.addRoleName(self.MaterialTypeRole, "material_type")
self._update_extruder_timer = QTimer() self._update_extruder_timer = QTimer()
self._update_extruder_timer.setInterval(100) self._update_extruder_timer.setInterval(100)
self._update_extruder_timer.setSingleShot(True) self._update_extruder_timer.setSingleShot(True)
@ -193,7 +197,8 @@ class ExtrudersModel(ListModel):
"variant": extruder.variant.getName() if extruder.variant else "", # e.g. print core "variant": extruder.variant.getName() if extruder.variant else "", # e.g. print core
"stack": extruder, "stack": extruder,
"material_brand": material_brand, "material_brand": material_brand,
"color_name": color_name "color_name": color_name,
"material_type": extruder.material.getMetaDataEntry("material") if extruder.material else "",
} }
items.append(item) items.append(item)
@ -218,6 +223,7 @@ class ExtrudersModel(ListModel):
"stack": None, "stack": None,
"material_brand": "", "material_brand": "",
"color_name": "", "color_name": "",
"material_type": "",
} }
items.append(item) items.append(item)
if self._items != items: if self._items != items:

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,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.
import copy # To duplicate materials. import copy # To duplicate materials.
@ -79,6 +79,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 +195,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)

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:
@ -114,10 +114,13 @@ class QualitySettingsModel(ListModel):
global_container = None if len(global_containers) == 0 else global_containers[0] global_container = None if len(global_containers) == 0 else global_containers[0]
extruders_containers = {pos: container_registry.findContainers(id = quality_changes_group.metadata_per_extruder[pos]["id"]) for pos in quality_changes_group.metadata_per_extruder} extruders_containers = {pos: container_registry.findContainers(id = quality_changes_group.metadata_per_extruder[pos]["id"]) for pos in quality_changes_group.metadata_per_extruder}
extruders_container = {pos: None if not containers else containers[0] for pos, containers in extruders_containers.items()} extruders_container = {pos: None if not containers else containers[0] for pos, containers in extruders_containers.items()}
quality_changes_metadata = None
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,8 +113,10 @@ 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
self._storeAuthData(self._auth_data) # 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)
return self._auth_helpers.parseJWT(self._auth_data.access_token) return self._auth_helpers.parseJWT(self._auth_data.access_token)
def getAccessToken(self) -> Optional[str]: def getAccessToken(self) -> Optional[str]:

View File

@ -4,7 +4,7 @@ from typing import Type, TYPE_CHECKING, Optional, List
import keyring import keyring
from keyring.backend import KeyringBackend from keyring.backend import KeyringBackend
from keyring.errors import NoKeyringError, PasswordSetError from keyring.errors import NoKeyringError, PasswordSetError, KeyringLocked
from UM.Logger import Logger from UM.Logger import Logger
@ -39,6 +39,10 @@ class KeyringAttribute:
self._store_secure = False self._store_secure = False
Logger.logException("w", "No keyring backend present") Logger.logException("w", "No keyring backend present")
return getattr(instance, self._name) return getattr(instance, self._name)
except KeyringLocked:
self._store_secure = False
Logger.log("i", "Access to the keyring was denied.")
return getattr(instance, self._name)
else: else:
return getattr(instance, self._name) return getattr(instance, self._name)
@ -48,7 +52,7 @@ class KeyringAttribute:
if value is not None: if value is not None:
try: try:
keyring.set_password("cura", self._keyring_name, value) keyring.set_password("cura", self._keyring_name, value)
except PasswordSetError: except (PasswordSetError, KeyringLocked):
self._store_secure = False self._store_secure = False
if self._name not in DONT_EVER_STORE_LOCALLY: if self._name not in DONT_EVER_STORE_LOCALLY:
setattr(instance, self._name, value) setattr(instance, self._name, value)

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

@ -29,7 +29,7 @@ class WhatsNewPagesModel(WelcomePagesModel):
for filename in files: for filename in files:
basename = os.path.basename(filename) basename = os.path.basename(filename)
base, ext = os.path.splitext(basename) base, ext = os.path.splitext(basename)
if ext not in include or not base.isdigit(): if ext.lower() not in include or not base.isdigit():
continue continue
page_no = int(base) page_no = int(base)
highest = max(highest, page_no) highest = max(highest, page_no)

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"):
quality_type = instance_container_info_dict[quality_container_id].parser["metadata"]["quality_type"] if quality_container_id in instance_container_info_dict:
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,
"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,
"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
} }

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,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -5,7 +5,6 @@ import threading
from datetime import datetime from datetime import datetime
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
import sentry_sdk
from PyQt5.QtNetwork import QNetworkReply from PyQt5.QtNetwork import QNetworkReply
from UM.Job import Job from UM.Job import Job
@ -99,13 +98,7 @@ class CreateBackupJob(Job):
if HttpRequestManager.safeHttpStatus(reply) == 400: if HttpRequestManager.safeHttpStatus(reply) == 400:
errors = json.loads(replyText)["errors"] errors = json.loads(replyText)["errors"]
if "moreThanMaximum" in [error["code"] for error in errors if error["meta"] and error["meta"]["field_name"] == "backup_size"]: if "moreThanMaximum" in [error["code"] for error in errors if error["meta"] and error["meta"]["field_name"] == "backup_size"]:
if self._backup_zip is None: # will never happen; keep mypy happy
zip_error = "backup is None."
else:
zip_error = "{} exceeds max size.".format(str(len(self._backup_zip)))
sentry_sdk.capture_message("backup failed: {}".format(zip_error), level ="warning")
self.backup_upload_error_message = catalog.i18nc("@error:file_size", "The backup exceeds the maximum file size.") self.backup_upload_error_message = catalog.i18nc("@error:file_size", "The backup exceeds the maximum file size.")
from sentry_sdk import capture_message
self._job_done.set() self._job_done.set()
return return

View File

@ -93,7 +93,7 @@ class DriveApiService:
def _onRestoreFinished(self, job: "RestoreBackupJob") -> None: def _onRestoreFinished(self, job: "RestoreBackupJob") -> None:
if job.restore_backup_error_message != "": if job.restore_backup_error_message != "":
# If the job contains an error message we pass it along so the UI can display it. # If the job contains an error message we pass it along so the UI can display it.
self.restoringStateChanged.emit(is_restoring=False) self.restoringStateChanged.emit(is_restoring = False)
else: else:
self.restoringStateChanged.emit(is_restoring = False, error_message = job.restore_backup_error_message) self.restoringStateChanged.emit(is_restoring = False, error_message = job.restore_backup_error_message)

View File

@ -34,6 +34,9 @@ class DrivePluginExtension(QObject, Extension):
# Signal emitted when preferences changed (like auto-backup). # Signal emitted when preferences changed (like auto-backup).
preferencesChanged = pyqtSignal() preferencesChanged = pyqtSignal()
# Signal emitted when the id of the backup-to-be-restored is changed
backupIdBeingRestoredChanged = pyqtSignal(arguments = ["backup_id_being_restored"])
DATE_FORMAT = "%d/%m/%Y %H:%M:%S" DATE_FORMAT = "%d/%m/%Y %H:%M:%S"
def __init__(self) -> None: def __init__(self) -> None:
@ -45,6 +48,7 @@ class DrivePluginExtension(QObject, Extension):
self._backups = [] # type: List[Dict[str, Any]] self._backups = [] # type: List[Dict[str, Any]]
self._is_restoring_backup = False self._is_restoring_backup = False
self._is_creating_backup = False self._is_creating_backup = False
self._backup_id_being_restored = ""
# Initialize services. # Initialize services.
preferences = CuraApplication.getInstance().getPreferences() preferences = CuraApplication.getInstance().getPreferences()
@ -52,6 +56,7 @@ class DrivePluginExtension(QObject, Extension):
# Attach signals. # Attach signals.
CuraApplication.getInstance().getCuraAPI().account.loginStateChanged.connect(self._onLoginStateChanged) CuraApplication.getInstance().getCuraAPI().account.loginStateChanged.connect(self._onLoginStateChanged)
CuraApplication.getInstance().applicationShuttingDown.connect(self._onApplicationShuttingDown)
self._drive_api_service.restoringStateChanged.connect(self._onRestoringStateChanged) self._drive_api_service.restoringStateChanged.connect(self._onRestoringStateChanged)
self._drive_api_service.creatingStateChanged.connect(self._onCreatingStateChanged) self._drive_api_service.creatingStateChanged.connect(self._onCreatingStateChanged)
@ -75,6 +80,10 @@ class DrivePluginExtension(QObject, Extension):
if self._drive_window: if self._drive_window:
self._drive_window.show() self._drive_window.show()
def _onApplicationShuttingDown(self):
if self._drive_window:
self._drive_window.hide()
def _autoBackup(self) -> None: def _autoBackup(self) -> None:
preferences = CuraApplication.getInstance().getPreferences() preferences = CuraApplication.getInstance().getPreferences()
if preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY) and self._isLastBackupTooLongAgo(): if preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY) and self._isLastBackupTooLongAgo():
@ -100,10 +109,11 @@ class DrivePluginExtension(QObject, Extension):
if logged_in: if logged_in:
self.refreshBackups() self.refreshBackups()
def _onRestoringStateChanged(self, is_restoring: bool = False, error_message: str = None) -> None: def _onRestoringStateChanged(self, is_restoring: bool = False, error_message: Optional[str] = None) -> None:
self._is_restoring_backup = is_restoring self._is_restoring_backup = is_restoring
self.restoringStateChanged.emit() self.restoringStateChanged.emit()
if error_message: if error_message:
self.backupIdBeingRestored = ""
Message(error_message, title = catalog.i18nc("@info:title", "Backup")).show() Message(error_message, title = catalog.i18nc("@info:title", "Backup")).show()
def _onCreatingStateChanged(self, is_creating: bool = False, error_message: str = None) -> None: def _onCreatingStateChanged(self, is_creating: bool = False, error_message: str = None) -> None:
@ -152,6 +162,7 @@ class DrivePluginExtension(QObject, Extension):
for backup in self._backups: for backup in self._backups:
if backup.get("backup_id") == backup_id: if backup.get("backup_id") == backup_id:
self._drive_api_service.restoreBackup(backup) self._drive_api_service.restoreBackup(backup)
self.setBackupIdBeingRestored(backup_id)
return return
Logger.log("w", "Unable to find backup with the ID %s", backup_id) Logger.log("w", "Unable to find backup with the ID %s", backup_id)
@ -166,3 +177,12 @@ class DrivePluginExtension(QObject, Extension):
def _backupDeletedCallback(self, success: bool): def _backupDeletedCallback(self, success: bool):
if success: if success:
self.refreshBackups() self.refreshBackups()
def setBackupIdBeingRestored(self, backup_id_being_restored: str) -> None:
if backup_id_being_restored != self._backup_id_being_restored:
self._backup_id_being_restored = backup_id_being_restored
self.backupIdBeingRestoredChanged.emit()
@pyqtProperty(str, fset = setBackupIdBeingRestored, notify = backupIdBeingRestoredChanged)
def backupIdBeingRestored(self) -> str:
return self._backup_id_being_restored

View File

@ -71,6 +71,7 @@ Item
text: catalog.i18nc("@button", "Restore") text: catalog.i18nc("@button", "Restore")
enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup
onClicked: confirmRestoreDialog.visible = true onClicked: confirmRestoreDialog.visible = true
busy: CuraDrive.backupIdBeingRestored == modelData.backup_id && CuraDrive.isRestoringBackup
} }
UM.SimpleButton UM.SimpleButton

View File

@ -17,7 +17,7 @@ ColumnLayout
// Cura version // Cura version
BackupListItemDetailsRow BackupListItemDetailsRow
{ {
iconSource: UM.Theme.getIcon("UltimakeCura") iconSource: UM.Theme.getIcon("UltimakerCura")
label: catalog.i18nc("@backuplist:label", "Cura Version") label: catalog.i18nc("@backuplist:label", "Cura Version")
value: backupDetailsData.metadata.cura_release value: backupDetailsData.metadata.cura_release
} }

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,
"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,
"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,
"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,
"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

@ -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.

View File

@ -1,8 +1,3 @@
# Prevents error: "PyCapsule_GetPointer called with incorrect name" with conflicting SIP configurations between Arcus and PyQt: Import custom Sip bindings first!
import Savitar # Dont remove this line
import Arcus # No really. Don't. It needs to be there!
import pynest2d # Really!
# Ensure that the importing for all tests work # Ensure that the importing for all tests work
import sys import sys

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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"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,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -73,38 +73,40 @@ 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.
definition = self._stack.getSettingDefinition(item) continue
if definition: definition = self._stack.getSettingDefinition(item)
new_instance = SettingInstance(definition, settings) 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)
stack_nr = -1
stack = None
# Check from what stack we should copy the raw property of the setting from.
if self._stack.getProperty("machine_extruder_count", "value") > 1:
if definition.limit_to_extruder != "-1":
# A limit to extruder function was set and it's a multi extrusion machine. Check what stack we do need to use.
stack_nr = str(int(round(float(self._stack.getProperty(item, "limit_to_extruder")))))
# Check if the found stack_number is in the extruder list of extruders.
if stack_nr not in ExtruderManager.getInstance().extruderIds and self._stack.getProperty("extruder_nr", "value") is not None:
stack_nr = -1 stack_nr = -1
stack = None
# Check from what stack we should copy the raw property of the setting from.
if self._stack.getProperty("machine_extruder_count", "value") > 1:
if definition.limit_to_extruder != "-1":
# A limit to extruder function was set and it's a multi extrusion machine. Check what stack we do need to use.
stack_nr = str(int(round(float(self._stack.getProperty(item, "limit_to_extruder")))))
# Check if the found stack_number is in the extruder list of extruders. # Use the found stack number to get the right stack to copy the value from.
if stack_nr not in ExtruderManager.getInstance().extruderIds and self._stack.getProperty("extruder_nr", "value") is not None: if stack_nr in ExtruderManager.getInstance().extruderIds:
stack_nr = -1 stack = ContainerRegistry.getInstance().findContainerStacks(id = ExtruderManager.getInstance().extruderIds[stack_nr])[0]
else:
stack = self._stack
# Use the found stack number to get the right stack to copy the value from. # Use the raw property to set the value (so the inheritance doesn't break)
if stack_nr in ExtruderManager.getInstance().extruderIds: if stack is not None:
stack = ContainerRegistry.getInstance().findContainerStacks(id = ExtruderManager.getInstance().extruderIds[stack_nr])[0] new_instance.setProperty("value", stack.getRawProperty(item, "value"))
else: else:
stack = self._stack new_instance.setProperty("value", None)
new_instance.resetState() # Ensure that the state is not seen as a user state.
# Use the raw property to set the value (so the inheritance doesn't break) settings.addInstance(new_instance)
if stack is not None: visibility_changed = True
new_instance.setProperty("value", stack.getRawProperty(item, "value"))
else:
new_instance.setProperty("value", None)
new_instance.resetState() # Ensure that the state is not seen as a user state.
settings.addInstance(new_instance)
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
@ -116,7 +116,7 @@ Item
{ {
id: antiOverhangMeshButton id: antiOverhangMeshButton
text: catalog.i18nc("@label", "Don't support overlaps") text: catalog.i18nc("@label", "Don't support overlaps")
iconSource: UM.Theme.getIcon("MeshTypeExclude"); iconSource: UM.Theme.getIcon("BlockSupportOverlaps");
property bool needBorder: true property bool needBorder: true
checkable: true checkable: true
onClicked: setMeshType(antiOverhangMeshType) onClicked: setMeshType(antiOverhangMeshType)
@ -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,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -1,10 +1,6 @@
<?xml version="1.0" encoding="iso-8859-1"?> <?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Artwork" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> <polygon points="4.4,12 8.2,15.8 6.8,17.2 1.6,12 6.8,6.8 8.2,8.2" />
<g> <polygon points="22.4,12 17.2,17.2 15.8,15.8 19.6,12 15.8,8.2 17.2,6.8" />
<polygon style="fill:#1D1C1A;" points="4.4,12 8.2,15.8 6.8,17.2 1.6,12 6.8,6.8 8.2,8.2 "/> <rect x="3.9" y="11" transform="matrix(0.1236 -0.9923 0.9923 0.1236 -1.429 22.4317)" width="16.1" height="2" />
<polygon style="fill:#1D1C1A;" points="22.4,12 17.2,17.2 15.8,15.8 19.6,12 15.8,8.2 17.2,6.8 "/>
<rect x="3.9" y="11" transform="matrix(0.1236 -0.9923 0.9923 0.1236 -1.429 22.4317)" style="fill:#1D1C1A;" width="16.1" height="2"/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 600 B

After

Width:  |  Height:  |  Size: 380 B

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,
"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

@ -72,6 +72,15 @@ class FilamentChange(Script):
"type": "float", "type": "float",
"default_value": 0, "default_value": 0,
"enabled": "not firmware_config" "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
} }
} }
}""" }"""
@ -87,6 +96,7 @@ 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") firmware_config = self.getSettingValueByKey("firmware_config")
color_change = "M600" color_change = "M600"
@ -100,10 +110,13 @@ class FilamentChange(Script):
if x_pos is not None: if x_pos is not None:
color_change = color_change + (" X%.2f" % x_pos) color_change = color_change + (" X%.2f" % x_pos)
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(",")
@ -116,4 +129,4 @@ class FilamentChange(Script):
if 0 < layer_num < len(data): if 0 < layer_num < len(data):
data[layer_num] = color_change + data[layer_num] data[layer_num] = color_change + data[layer_num]
return data return data

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 ..Script import Script from ..Script import Script
@ -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,10 +507,23 @@ 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
# reset extrude value to pre pause value relative_extrusion = Application.getInstance().getGlobalContainerStack().getProperty("relative_extrusion", "value")
prepend_gcode += self.putValue(G = 92, E = current_e) + "\n" 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
prepend_gcode += self.putValue(G = 92, E = current_e) + "\n"
elif redo_layer:
# All other options reset the E value to what it was before the pause because E things were added.
# If it's not yet reset, it still needs to be reset if there were any redo layers.
prepend_gcode += self.putValue(G = 92, E = current_e) + "\n"
layer = prepend_gcode + layer layer = prepend_gcode + layer

View File

@ -8,7 +8,6 @@ import QtQuick.Controls 2.3
import UM 1.3 as UM import UM 1.3 as UM
import Cura 1.1 as Cura import Cura 1.1 as Cura
import QtGraphicalEffects 1.0 // For the dropshadow
Item Item
{ {
@ -42,42 +41,34 @@ Item
anchors.left: openFileButton.right anchors.left: openFileButton.right
anchors.right: parent.right anchors.right: parent.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width anchors.leftMargin: UM.Theme.getSize("default_margin").width
property int machineSelectorWidth: Math.round((width - printSetupSelectorItem.width) / 3)
height: parent.height height: parent.height
spacing: 0 // This is a trick to make sure that the borders of the two adjacent buttons' borders overlap. Otherwise
// there will be double border (one from each button)
spacing: -UM.Theme.getSize("default_lining").width
Cura.MachineSelector Cura.MachineSelector
{ {
id: machineSelection id: machineSelection
headerCornerSide: Cura.RoundedRectangle.Direction.Left headerCornerSide: Cura.RoundedRectangle.Direction.Left
Layout.minimumWidth: UM.Theme.getSize("machine_selector_widget").width headerBackgroundBorder.width: UM.Theme.getSize("default_lining").width
Layout.maximumWidth: UM.Theme.getSize("machine_selector_widget").width headerBackgroundBorder.color: UM.Theme.getColor("lining")
enableHeaderShadow: false
Layout.preferredWidth: parent.machineSelectorWidth
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
} }
// Separator line
Rectangle
{
height: parent.height
width: UM.Theme.getSize("default_lining").width
color: UM.Theme.getColor("lining")
}
Cura.ConfigurationMenu Cura.ConfigurationMenu
{ {
id: printerSetup id: printerSetup
enableHeaderShadow: false
headerBackgroundBorder.width: UM.Theme.getSize("default_lining").width
headerBackgroundBorder.color: UM.Theme.getColor("lining")
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredWidth: itemRow.width - machineSelection.width - printSetupSelectorItem.width - 2 * UM.Theme.getSize("default_lining").width Layout.preferredWidth: parent.machineSelectorWidth * 2
}
// Separator line
Rectangle
{
height: parent.height
width: UM.Theme.getSize("default_lining").width
color: UM.Theme.getColor("lining")
} }
Item Item
@ -120,24 +111,12 @@ Item
id: background id: background
height: UM.Theme.getSize("stage_menu").height height: UM.Theme.getSize("stage_menu").height
width: UM.Theme.getSize("stage_menu").height width: UM.Theme.getSize("stage_menu").height
border.color: UM.Theme.getColor("lining")
border.width: UM.Theme.getSize("default_lining").width
radius: UM.Theme.getSize("default_radius").width radius: UM.Theme.getSize("default_radius").width
color: openFileButton.hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("action_button") color: openFileButton.hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("action_button")
} }
DropShadow
{
id: shadow
// Don't blur the shadow
radius: 0
anchors.fill: background
source: background
verticalOffset: 2
visible: true
color: UM.Theme.getColor("action_button_shadow")
// Should always be drawn behind the background.
z: background.z - 1
}
} }
} }
} }

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,
"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,
"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,
"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,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -65,7 +65,7 @@ class SimulationPass(RenderPass):
self._layer_shader.setUniformValue("u_active_extruder", float(max(0, self._extruder_manager.activeExtruderIndex))) self._layer_shader.setUniformValue("u_active_extruder", float(max(0, self._extruder_manager.activeExtruderIndex)))
if not self._compatibility_mode: if not self._compatibility_mode:
self._layer_shader.setUniformValue("u_starts_color", Color(*Application.getInstance().getTheme().getColor("layerview_starts").getRgb())) self._layer_shader.setUniformValue("u_starts_color", Color(*Application.getInstance().getTheme().getColor("layerview_starts").getRgb()))
if self._layer_view: if self._layer_view:
self._layer_shader.setUniformValue("u_max_feedrate", self._layer_view.getMaxFeedrate()) self._layer_shader.setUniformValue("u_max_feedrate", self._layer_view.getMaxFeedrate())
self._layer_shader.setUniformValue("u_min_feedrate", self._layer_view.getMinFeedrate()) self._layer_shader.setUniformValue("u_min_feedrate", self._layer_view.getMinFeedrate())
@ -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)
@ -174,9 +178,9 @@ class SimulationPass(RenderPass):
self._switching_layers = True self._switching_layers = True
# The first line does not have a previous line: add a MoveCombingType in front for start detection # The first line does not have a previous line: add a MoveCombingType in front for start detection
# this way the first start of the layer can also be drawn # this way the first start of the layer can also be drawn
prev_line_types = numpy.concatenate([numpy.asarray([LayerPolygon.MoveCombingType], dtype = numpy.float32), layer_data._attributes["line_types"]["value"]]) prev_line_types = numpy.concatenate([numpy.asarray([LayerPolygon.MoveCombingType], dtype = numpy.float32), layer_data._attributes["line_types"]["value"]])
# Remove the last element # Remove the last element
prev_line_types = prev_line_types[0:layer_data._attributes["line_types"]["value"].size] prev_line_types = prev_line_types[0:layer_data._attributes["line_types"]["value"].size]
layer_data._attributes["prev_line_types"] = {'opengl_type': 'float', 'value': prev_line_types, 'opengl_name': 'a_prev_line_type'} layer_data._attributes["prev_line_types"] = {'opengl_type': 'float', 'value': prev_line_types, 'opengl_name': 'a_prev_line_type'}

View File

@ -94,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
@ -411,6 +413,14 @@ 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 Calculates number of layers, triggers signals if the number of layers changed and makes sure the top layers are
@ -468,6 +478,8 @@ class SimulationView(CuraView):
old_max_linewidth = self._max_line_width old_max_linewidth = self._max_line_width
old_min_thickness = self._min_thickness old_min_thickness = self._min_thickness
old_max_thickness = self._max_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._min_feedrate = sys.float_info.max
self._max_feedrate = sys.float_info.min self._max_feedrate = sys.float_info.min
@ -475,6 +487,8 @@ class SimulationView(CuraView):
self._max_line_width = sys.float_info.min self._max_line_width = sys.float_info.min
self._min_thickness = sys.float_info.max self._min_thickness = sys.float_info.max
self._max_thickness = sys.float_info.min 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. # The colour scheme is only influenced by the visible lines, so filter the lines by if they should be visible.
visible_line_types = [] visible_line_types = []
@ -490,6 +504,7 @@ class SimulationView(CuraView):
visible_line_types.append(LayerPolygon.SupportType) visible_line_types.append(LayerPolygon.SupportType)
visible_line_types.append(LayerPolygon.SupportInfillType) visible_line_types.append(LayerPolygon.SupportInfillType)
visible_line_types.append(LayerPolygon.SupportInterfaceType) visible_line_types.append(LayerPolygon.SupportInterfaceType)
visible_line_types_with_extrusion = visible_line_types.copy() # Copy before travel moves are added
if self.getShowTravelMoves(): if self.getShowTravelMoves():
visible_line_types.append(LayerPolygon.MoveCombingType) visible_line_types.append(LayerPolygon.MoveCombingType)
visible_line_types.append(LayerPolygon.MoveRetractionType) visible_line_types.append(LayerPolygon.MoveRetractionType)
@ -503,12 +518,20 @@ class SimulationView(CuraView):
for polyline in layer_data.getLayer(layer_index).polygons: for polyline in layer_data.getLayer(layer_index).polygons:
is_visible = numpy.isin(polyline.types, visible_line_types) is_visible = numpy.isin(polyline.types, visible_line_types)
visible_indices = numpy.where(is_visible)[0] 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. if visible_indices.size == 0: # No items to take maximum or minimum of.
continue continue
visible_feedrates = numpy.take(polyline.lineFeedrates, visible_indices) 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 = 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 = 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) 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._min_feedrate = min(float(visible_feedrates.min()), self._min_feedrate)
self._max_line_width = max(float(visible_linewidths.max()), self._max_line_width) 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._min_line_width = min(float(visible_linewidths.min()), self._min_line_width)
@ -517,11 +540,12 @@ class SimulationView(CuraView):
self._min_thickness = min(float(visible_thicknesses[numpy.nonzero(visible_thicknesses)].min()), self._min_thickness) self._min_thickness = min(float(visible_thicknesses[numpy.nonzero(visible_thicknesses)].min()), self._min_thickness)
except ValueError: except ValueError:
# Sometimes, when importing a GCode the line thicknesses are zero and so the minimum (avoiding the zero) can't be calculated. # 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") 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 \ 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_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_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() self.colorSchemeLimitsChanged.emit()
def calculateMaxPathsOnLayer(self, layer_num: int) -> None: def calculateMaxPathsOnLayer(self, layer_num: int) -> None:

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);
} }
} }
@ -396,11 +404,17 @@ Cura.ExpandableComponent
{ {
return parseFloat(UM.SimulationView.minThickness).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.minLineWidth).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 ""
} }
@ -460,6 +479,11 @@ Cura.ExpandableComponent
{ {
return parseFloat(UM.SimulationView.maxLineWidth).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

@ -126,6 +126,14 @@ class SimulationViewProxy(QObject):
def minLineWidth(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):

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
@ -105,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;
@ -130,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;
@ -318,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;
@ -337,7 +366,7 @@ geometry41core =
myEmitVertex(v_vertex[0] + vec3(-w, -h, -w), u_starts_color, normalize(vec3(-1.0, -1.0, -1.0)), viewProjectionMatrix * (gl_in[0].gl_Position + vec4(-w, -h, -w, 0.0))); // Back-bottom-right myEmitVertex(v_vertex[0] + vec3(-w, -h, -w), u_starts_color, normalize(vec3(-1.0, -1.0, -1.0)), viewProjectionMatrix * (gl_in[0].gl_Position + vec4(-w, -h, -w, 0.0))); // Back-bottom-right
myEmitVertex(v_vertex[0] + vec3( w, h, -w), u_starts_color, normalize(vec3( 1.0, 1.0, -1.0)), viewProjectionMatrix * (gl_in[0].gl_Position + vec4( w, h, -w, 0.0))); // Back-top-left myEmitVertex(v_vertex[0] + vec3( w, h, -w), u_starts_color, normalize(vec3( 1.0, 1.0, -1.0)), viewProjectionMatrix * (gl_in[0].gl_Position + vec4( w, h, -w, 0.0))); // Back-top-left
myEmitVertex(v_vertex[0] + vec3(-w, h, -w), u_starts_color, normalize(vec3(-1.0, 1.0, -1.0)), viewProjectionMatrix * (gl_in[0].gl_Position + vec4(-w, h, -w, 0.0))); // Back-top-right myEmitVertex(v_vertex[0] + vec3(-w, h, -w), u_starts_color, normalize(vec3(-1.0, 1.0, -1.0)), viewProjectionMatrix * (gl_in[0].gl_Position + vec4(-w, h, -w, 0.0))); // Back-top-right
EndPrimitive(); EndPrimitive();
} }
} }

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,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="iso-8859-1"?> <?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Artwork" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> <path d="M8,21H6V3h2V21z M18,3h-2v18h2V3z" />
<path style="fill:#000E1A;" d="M8,21H6V3h2V21z M18,3h-2v18h2V3z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 329 B

After

Width:  |  Height:  |  Size: 167 B

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="iso-8859-1"?> <?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Artwork" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> <path d="M5,20V4c0-0.8,0.9-1.3,1.5-0.9l13,8c0.6,0.4,0.6,1.3,0,1.7l-13,8C5.9,21.3,5,20.8,5,20z M7,5.8v12.4
<path style="fill:#231F20;" d="M5,20V4c0-0.8,0.9-1.3,1.5-0.9l13,8c0.6,0.4,0.6,1.3,0,1.7l-13,8C5.9,21.3,5,20.8,5,20z M7,5.8v12.4
L17.1,12L7,5.8z"/> L17.1,12L7,5.8z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 410 B

After

Width:  |  Height:  |  Size: 247 B

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 json import json
@ -87,8 +87,12 @@ class SliceInfo(QObject, Extension):
return None return None
file_path = os.path.join(plugin_path, "example_data.html") file_path = os.path.join(plugin_path, "example_data.html")
if file_path: if file_path:
with open(file_path, "r", encoding = "utf-8") as f: try:
self._example_data_content = f.read() with open(file_path, "r", encoding = "utf-8") as f:
self._example_data_content = f.read()
except EnvironmentError as e:
Logger.error(f"Unable to read example slice info data to show to the user: {e}")
self._example_data_content = "<i>" + catalog.i18nc("@text", "Unable to read example data file.") + "</i>"
return self._example_data_content return self._example_data_content
@pyqtSlot(bool) @pyqtSlot(bool)
@ -229,6 +233,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,
"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,
"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,
"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,
"description": "Find, manage and install new Cura packages." "description": "Find, manage and install new Cura packages."
} }

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="iso-8859-1"?> <?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Artwork" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> <path d="M19,3H5C3.3,3,2,4.3,2,6v3c0,1.5,0.8,2.7,2,3.4V22h16v-9.6c1.2-0.7,2-2,2-3.4V6C22,4.3,20.7,3,19,3z
<path style="fill:#000E1A;" d="M19,3H5C3.3,3,2,4.3,2,6v3c0,1.5,0.8,2.7,2,3.4V22h16v-9.6c1.2-0.7,2-2,2-3.4V6C22,4.3,20.7,3,19,3z M10,5h4v4c0,1.1-0.9,2-2,2s-2-0.9-2-2V5z M4,9V5h4v4c0,1.1-0.9,2-2,2S4,10.1,4,9z M18,20h-4v-5h-4v5H6v-7c1.2,0,2.3-0.5,3-1.4
M10,5h4v4c0,1.1-0.9,2-2,2s-2-0.9-2-2V5z M4,9V5h4v4c0,1.1-0.9,2-2,2S4,10.1,4,9z M18,20h-4v-5h-4v5H6v-7c1.2,0,2.3-0.5,3-1.4 c0.7,0.8,1.8,1.4,3,1.4s2.3-0.5,3-1.4c0.7,0.8,1.8,1.4,3,1.4V20z M20,9c0,1.1-0.9,2-2,2s-2-0.9-2-2V5h4V9z" />
c0.7,0.8,1.8,1.4,3,1.4s2.3-0.5,3-1.4c0.7,0.8,1.8,1.4,3,1.4V20z M20,9c0,1.1-0.9,2-2,2s-2-0.9-2-2V5h4V9z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 621 B

After

Width:  |  Height:  |  Size: 458 B

View File

@ -4,7 +4,7 @@
import QtQuick 2.10 import QtQuick 2.10
import QtQuick.Controls 1.4 import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4 import QtQuick.Controls.Styles 1.4
import QtGraphicalEffects 1.0
import UM 1.1 as UM import UM 1.1 as UM
Rectangle Rectangle

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
} }

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,35 +48,53 @@ class UFPWriter(MeshWriter):
archive = VirtualFile() archive = VirtualFile()
archive.openStream(stream, "application/x-ufp", OpenMode.WriteOnly) archive.openStream(stream, "application/x-ufp", OpenMode.WriteOnly)
self._writeObjectList(archive) try:
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
gcode = archive.getStream("/3D/model.gcode") try:
gcode.write(gcode_textio.getvalue().encode("UTF-8")) gcode = archive.getStream("/3D/model.gcode")
archive.addRelation(virtual_path = "/3D/model.gcode", relation_type = "http://schemas.ultimaker.org/package/2018/relationships/gcode") 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")
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:
archive.addContentType(extension = "png", mime_type = "image/png") try:
thumbnail = archive.getStream("/Metadata/thumbnail.png") archive.addContentType(extension = "png", mime_type = "image/png")
thumbnail = archive.getStream("/Metadata/thumbnail.png")
thumbnail_buffer = QBuffer() thumbnail_buffer = QBuffer()
thumbnail_buffer.open(QBuffer.ReadWrite) thumbnail_buffer.open(QBuffer.ReadWrite)
snapshot.save(thumbnail_buffer, "PNG") snapshot.save(thumbnail_buffer, "PNG")
thumbnail.write(thumbnail_buffer.data()) thumbnail.write(thumbnail_buffer.data())
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
material_file = archive.getStream(material_file_name) try:
material_file.write(serialized_material.encode("UTF-8")) material_file = archive.getStream(material_file_name)
archive.addRelation(virtual_path = material_file_name, material_file.write(serialized_material.encode("UTF-8"))
relation_type = "http://schemas.ultimaker.org/package/2018/relationships/material", archive.addRelation(virtual_path = material_file_name,
origin = "/3D/model.gcode") relation_type = "http://schemas.ultimaker.org/package/2018/relationships/material",
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,
"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,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -6,7 +6,6 @@ import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4 import QtQuick.Controls.Styles 1.4
import UM 1.3 as UM import UM 1.3 as UM
import Cura 1.0 as Cura import Cura 1.0 as Cura
import QtGraphicalEffects 1.0
// This is the root component for the monitor stage. // This is the root component for the monitor stage.
Component Component

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="iso-8859-1"?> <?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Artwork" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> <path d="M22,6.4l-4,2V8c0-1.7-1.3-3-3-3H5C3.3,5,2,6.3,2,8v8c0,1.7,1.3,3,3,3h10c1.7,0,3-1.3,3-3v-0.4l4,2
<path style="fill:#000E1A;" d="M22,6.4l-4,2V8c0-1.7-1.3-3-3-3H5C3.3,5,2,6.3,2,8v8c0,1.7,1.3,3,3,3h10c1.7,0,3-1.3,3-3v-0.4l4,2 V6.4z M16,17H4V7h12V17z M20,14.4l-2-1c0-0.9,0-1.8,0-2.8l2-1V14.4z" />
V6.4z M16,17H4V7h12V17z M20,14.4l-2-1c0-0.9,0-1.8,0-2.8l2-1V14.4z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 458 B

After

Width:  |  Height:  |  Size: 296 B

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="iso-8859-1"?> <?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Artwork" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> <path d="M12,2C6.5,2,2,6.5,2,12s4.5,10,10,10s10-4.5,10-10S17.5,2,12,2z M12,20c-4.4,0-8-3.6-8-8s3.6-8,8-8
<path style="fill:#000E1A;" d="M12,2C6.5,2,2,6.5,2,12s4.5,10,10,10s10-4.5,10-10S17.5,2,12,2z M12,20c-4.4,0-8-3.6-8-8s3.6-8,8-8
s8,3.6,8,8S16.4,20,12,20z M16.7,8.7L13.4,12l3.3,3.3l-1.4,1.4L12,13.4l-3.3,3.3l-1.4-1.4l3.3-3.3L7.3,8.7l1.4-1.4l3.3,3.3l3.3-3.3 s8,3.6,8,8S16.4,20,12,20z M16.7,8.7L13.4,12l3.3,3.3l-1.4,1.4L12,13.4l-3.3,3.3l-1.4-1.4l3.3-3.3L7.3,8.7l1.4-1.4l3.3,3.3l3.3-3.3
L16.7,8.7z"/> L16.7,8.7z" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 532 B

After

Width:  |  Height:  |  Size: 370 B

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="iso-8859-1"?> <?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Artwork" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> <polygon points="9.1,19.4 1.9,12.2 3.3,10.8 9.1,16.6 19.4,6.3 20.8,7.7" />
<polygon style="fill:#000E1A;" points="9.1,19.4 1.9,12.2 3.3,10.8 9.1,16.6 19.4,6.3 20.8,7.7 "/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 359 B

After

Width:  |  Height:  |  Size: 196 B

View File

@ -1,9 +1,6 @@
<?xml version="1.0" encoding="iso-8859-1"?> <?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Artwork" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> <path d="M12,2C6.5,2,2,6.5,2,12c0,5.5,4.5,10,10,10s10-4.5,10-10C22,6.5,17.5,2,12,2z M12,20
<g> c-4.4,0-8-3.6-8-8c0-4.4,3.6-8,8-8s8,3.6,8,8C20,16.4,16.4,20,12,20z" />
<path style="fill:#000E1A;" d="M12,2C6.5,2,2,6.5,2,12c0,5.5,4.5,10,10,10s10-4.5,10-10C22,6.5,17.5,2,12,2z M12,20 <polygon points="16.3,8.3 11,13.6 7.7,10.3 6.3,11.7 11,16.4 17.7,9.7" />
c-4.4,0-8-3.6-8-8c0-4.4,3.6-8,8-8s8,3.6,8,8C20,16.4,16.4,20,12,20z"/>
<polygon style="fill:#000E1A;" points="16.3,8.3 11,13.6 7.7,10.3 6.3,11.7 11,16.4 17.7,9.7 "/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 554 B

After

Width:  |  Height:  |  Size: 357 B

View File

@ -1,10 +1,7 @@
<?xml version="1.0" encoding="iso-8859-1"?> <?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Artwork" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> <path d="M12,2C6.5,2,2,6.5,2,12c0,5.5,4.5,10,10,10s10-4.5,10-10C22,6.5,17.5,2,12,2z M12,20
<g> c-4.4,0-8-3.6-8-8c0-4.4,3.6-8,8-8s8,3.6,8,8C20,16.4,16.4,20,12,20z" />
<path style="fill:#000E1A;" d="M12,2C6.5,2,2,6.5,2,12c0,5.5,4.5,10,10,10s10-4.5,10-10C22,6.5,17.5,2,12,2z M12,20 <rect x="13" y="8" width="2" height="8"/>
c-4.4,0-8-3.6-8-8c0-4.4,3.6-8,8-8s8,3.6,8,8C20,16.4,16.4,20,12,20z"/> <rect x="9" y="8" width="2" height="8"/>
<rect x="13" y="8" style="fill:#000E1A;" width="2" height="8"/>
<rect x="9" y="8" style="fill:#000E1A;" width="2" height="8"/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 586 B

After

Width:  |  Height:  |  Size: 367 B

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="iso-8859-1"?> <?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Artwork" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> <path d="M12,2C6.5,2,2,6.5,2,12s4.5,10,10,10s10-4.5,10-10S17.5,2,12,2z M4,12c0-1.8,0.6-3.5,1.7-4.9
<path style="fill:#000E1A;" d="M12,2C6.5,2,2,6.5,2,12s4.5,10,10,10s10-4.5,10-10S17.5,2,12,2z M4,12c0-1.8,0.6-3.5,1.7-4.9
l11.2,11.2C15.5,19.4,13.8,20,12,20C7.6,20,4,16.4,4,12z M18.3,16.9L7.1,5.7C8.5,4.6,10.2,4,12,4c4.4,0,8,3.6,8,8 l11.2,11.2C15.5,19.4,13.8,20,12,20C7.6,20,4,16.4,4,12z M18.3,16.9L7.1,5.7C8.5,4.6,10.2,4,12,4c4.4,0,8,3.6,8,8
C20,13.8,19.4,15.5,18.3,16.9z"/> C20,13.8,19.4,15.5,18.3,16.9z" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 528 B

After

Width:  |  Height:  |  Size: 366 B

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="iso-8859-1"?> <?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Artwork" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> <path d="M22,18L13.7,3.1c-0.4-0.6-1-1-1.7-1s-1.4,0.4-1.7,1L2,18c-0.3,0.6-0.3,1.4,0,2c0.4,0.6,1,1,1.7,1
<path style="fill:#000E1A;" d="M22,18L13.7,3.1c-0.4-0.6-1-1-1.7-1s-1.4,0.4-1.7,1L2,18c-0.3,0.6-0.3,1.4,0,2c0.4,0.6,1,1,1.7,1 h16.6c0.7,0,1.4-0.4,1.7-1C22.4,19.4,22.4,18.7,22,18z M3.7,19L12,4.1L20.3,19H3.7z M11,8h2v6h-2V8z M13,18h-2v-2h2V18z" />
h16.6c0.7,0,1.4-0.4,1.7-1C22.4,19.4,22.4,18.7,22,18z M3.7,19L12,4.1L20.3,19H3.7z M11,8h2v6h-2V8z M13,18h-2v-2h2V18z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 507 B

After

Width:  |  Height:  |  Size: 345 B

View File

@ -1,7 +1,5 @@
<?xml version="1.0" encoding="iso-8859-1"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<svg version="1.1" id="Artwork" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <path d="M18,21H6c-1.7,0-3-1.3-3-3V6c0-1.7,1.3-3,3-3h12c1.7,0,3,1.3,3,3v12C21,19.7,19.7,21,18,21z M5,19h14
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> V5H5V19z M14.7,7.7l-1.4-1.4l-7,7l1.4,1.4L14.7,7.7z M9.7,7.7L8.3,6.3l-2,2l1.4,1.4L9.7,7.7z" />
<path style="fill:#000E1A;" d="M18,21H6c-1.7,0-3-1.3-3-3V6c0-1.7,1.3-3,3-3h12c1.7,0,3,1.3,3,3v12C21,19.7,19.7,21,18,21z M5,19h14
V5H5V19z M14.7,7.7l-1.4-1.4l-7,7l1.4,1.4L14.7,7.7z M9.7,7.7L8.3,6.3l-2,2l1.4,1.4L9.7,7.7z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 580 B

After

Width:  |  Height:  |  Size: 323 B

View File

@ -1,8 +1,6 @@
<?xml version="1.0" encoding="iso-8859-1"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.2.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<svg version="1.1" id="Artwork" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <path d="M19,6h-1.6l-2-2H8.6l-2,2H5C3.3,6,2,7.3,2,9v8c0,1.7,1.3,3,3,3h14c1.7,0,3-1.3,3-3V9
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
<path style="fill:#000E1A;" d="M19,6h-1.6l-2-2H8.6l-2,2H5C3.3,6,2,7.3,2,9v8c0,1.7,1.3,3,3,3h14c1.7,0,3-1.3,3-3V9
C22,7.3,20.7,6,19,6z M20,18H4V8h3.4l2-2h5.2l2,2H20V18z M12,8c-2.2,0-4,1.8-4,4s1.8,4,4,4s4-1.8,4-4S14.2,8,12,8z M12,14 C22,7.3,20.7,6,19,6z M20,18H4V8h3.4l2-2h5.2l2,2H20V18z M12,8c-2.2,0-4,1.8-4,4s1.8,4,4,4s4-1.8,4-4S14.2,8,12,8z M12,14
c-1.1,0-2-0.9-2-2s0.9-2,2-2s2,0.9,2,2S13.1,14,12,14z"/> c-1.1,0-2-0.9-2-2s0.9-2,2-2s2,0.9,2,2S13.1,14,12,14z" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 646 B

After

Width:  |  Height:  |  Size: 389 B

View File

@ -1,15 +1,5 @@
<?xml version="1.0" encoding="iso-8859-1"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <path d="M15,3C8.37,3,3,8.37,3,15v11h2v-4.36C7.15,24.87,10.83,27,15,27c6.63,0,12-5.37,12-12S21.63,3,15,3z M15,25
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve"> C9.49,25,5,20.51,5,15S9.49,5,15,5s10,4.49,10,10S20.51,25,15,25z"/>
<g id="Layer_1">
<g>
<g>
<path d="M15,3C8.37,3,3,8.37,3,15v11h2v-4.36C7.15,24.87,10.83,27,15,27c6.63,0,12-5.37,12-12S21.63,3,15,3z M15,25
C9.49,25,5,20.51,5,15S9.49,5,15,5s10,4.49,10,10S20.51,25,15,25z"/>
</g>
</g>
</g>
<g id="Comments">
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 600 B

After

Width:  |  Height:  |  Size: 302 B

View File

@ -399,7 +399,7 @@ class CloudOutputDeviceManager:
output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
stored_cluster_id = active_machine.getMetaDataEntry(self.META_CLUSTER_ID) stored_cluster_id = active_machine.getMetaDataEntry(self.META_CLUSTER_ID)
local_network_key = active_machine.getMetaDataEntry(self.META_NETWORK_KEY) local_network_key = active_machine.getMetaDataEntry(self.META_NETWORK_KEY)
for device in self._remote_clusters.values(): for device in list(self._remote_clusters.values()): # Make a copy of the remote devices list, to prevent modifying the list while iterating, if a device gets added asynchronously.
if device.key == stored_cluster_id: if device.key == stored_cluster_id:
# Connect to it if the stored ID matches. # Connect to it if the stored ID matches.
self._connectToOutputDevice(device, active_machine) self._connectToOutputDevice(device, active_machine)

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,
"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,
"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,
"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,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

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