From d92196da5346cba0e36d46882599f20c57c8e15a Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 12 Feb 2025 17:22:27 +0100 Subject: [PATCH 01/31] Exclude plugins available in Marketplace from backups. part of CURA-12156 --- cura/API/Backups.py | 15 +++---- cura/Backups/Backup.py | 35 ++++++++++++--- cura/Backups/BackupsManager.py | 9 ++-- plugins/CuraDrive/src/CreateBackupJob.py | 54 +++++++++++++++++++----- 4 files changed, 87 insertions(+), 26 deletions(-) diff --git a/cura/API/Backups.py b/cura/API/Backups.py index 1940d38a36..07001d22e2 100644 --- a/cura/API/Backups.py +++ b/cura/API/Backups.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2025 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. from typing import Tuple, Optional, TYPE_CHECKING, Dict, Any @@ -9,14 +9,10 @@ if TYPE_CHECKING: class Backups: - """The back-ups API provides a version-proof bridge between Cura's - - BackupManager and plug-ins that hook into it. + """The back-ups API provides a version-proof bridge between Cura's BackupManager and plug-ins that hook into it. Usage: - .. code-block:: python - from cura.API import CuraAPI api = CuraAPI() api.backups.createBackup() @@ -26,13 +22,13 @@ class Backups: def __init__(self, application: "CuraApplication") -> None: self.manager = BackupsManager(application) - def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, Any]]]: + def createBackup(self, available_remote_plugins: frozenset[str] = frozenset()) -> Tuple[Optional[bytes], Optional[Dict[str, Any]]]: """Create a new back-up using the BackupsManager. :return: Tuple containing a ZIP file with the back-up data and a dict with metadata about the back-up. """ - return self.manager.createBackup() + return self.manager.createBackup(available_remote_plugins) def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, Any]) -> None: """Restore a back-up using the BackupsManager. @@ -42,3 +38,6 @@ class Backups: """ return self.manager.restoreBackup(zip_file, meta_data) + + def shouldReinstallDownloadablePlugins(self) -> bool: + return self.manager.shouldReinstallDownloadablePlugins() diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 19655df531..9f35e54ef1 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -1,5 +1,6 @@ -# Copyright (c) 2021 Ultimaker B.V. +# Copyright (c) 2025 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. +import json import io import os @@ -13,6 +14,7 @@ from UM import i18nCatalog from UM.Logger import Logger from UM.Message import Message from UM.Platform import Platform +from UM.PluginRegistry import PluginRegistry from UM.Resources import Resources from UM.Version import Version @@ -30,10 +32,14 @@ class Backup: """These files should be ignored when making a backup.""" IGNORED_FOLDERS = [] # type: List[str] + """These folders should be ignored when making a backup.""" SECRETS_SETTINGS = ["general/ultimaker_auth_data"] """Secret preferences that need to obfuscated when making a backup of Cura""" + TO_INSTALL_FILE = "packages.json" + """File that contains the 'to_install' dictionary, that manages plugins to be installed on next startup.""" + catalog = i18nCatalog("cura") """Re-use translation catalog""" @@ -42,7 +48,7 @@ class Backup: self.zip_file = zip_file # type: Optional[bytes] self.meta_data = meta_data # type: Optional[Dict[str, str]] - def makeFromCurrent(self) -> None: + def makeFromCurrent(self, available_remote_plugins: frozenset[str] = frozenset()) -> None: """Create a back-up from the current user config folder.""" cura_release = self._application.getVersion() @@ -92,21 +98,40 @@ class Backup: # Restore the obfuscated settings self._illuminate(**secrets) - def _makeArchive(self, buffer: "io.BytesIO", root_path: str) -> Optional[ZipFile]: + def _fillToInstallsJson(self, file_path: str, reinstall_on_restore: dict[str, str], archive: ZipFile) -> None: + pass # TODO! + + def _findRedownloadablePlugins(self, available_remote_plugins: frozenset) -> dict[str, str]: + """ Find all plugins that should be able to be reinstalled from the Marketplace. + + :param plugins_path: Path to all plugins in the user-space. + :return: Set of all package-id's of plugins that can be reinstalled from the Marketplace. + """ + plugin_reg = PluginRegistry.getInstance() + id = "id" + return {v["location"]: v[id] for v in plugin_reg.getAllMetaData() + if v[id] in available_remote_plugins and not plugin_reg.isBundledPlugin(v[id])} + + def _makeArchive(self, buffer: "io.BytesIO", root_path: str, available_remote_plugins: frozenset) -> Optional[ZipFile]: """Make a full archive from the given root path with the given name. :param root_path: The root directory to archive recursively. :return: The archive as bytes. """ ignore_string = re.compile("|".join(self.IGNORED_FILES + self.IGNORED_FOLDERS)) + reinstall_instead_plugins = self._findRedownloadablePlugins(available_remote_plugins) try: archive = ZipFile(buffer, "w", ZIP_DEFLATED) - for root, folders, files in os.walk(root_path): + for root, folders, files in os.walk(root_path, topdown=True): + folders[:] = [f for f in folders if f not in reinstall_instead_plugins] for item_name in folders + files: absolute_path = os.path.join(root, item_name) if ignore_string.search(absolute_path): continue - archive.write(absolute_path, absolute_path[len(root_path) + len(os.sep):]) + if item_name == self.TO_INSTALL_FILE: + self._fillToInstallsJson(absolute_path, reinstall_instead_plugins, archive) + else: + archive.write(absolute_path, absolute_path[len(root_path) + len(os.sep):]) archive.close() return archive except (IOError, OSError, BadZipfile) as error: diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index 6c4670edb6..90dfc5e34e 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -1,4 +1,4 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2025 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. from typing import Dict, Optional, Tuple, TYPE_CHECKING @@ -22,7 +22,10 @@ class BackupsManager: def __init__(self, application: "CuraApplication") -> None: self._application = application - def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]: + def shouldReinstallDownloadablePlugins(self) -> bool: + return True + + def createBackup(self, available_remote_plugins: frozenset[str] = frozenset()) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]: """ Get a back-up of the current configuration. @@ -31,7 +34,7 @@ class BackupsManager: self._disableAutoSave() backup = Backup(self._application) - backup.makeFromCurrent() + backup.makeFromCurrent(available_remote_plugins if self.shouldReinstallDownloadablePlugins() else frozenset()) self._enableAutoSave() # We don't return a Backup here because we want plugins only to interact with our API and not full objects. return backup.zip_file, backup.meta_data diff --git a/plugins/CuraDrive/src/CreateBackupJob.py b/plugins/CuraDrive/src/CreateBackupJob.py index 7d772769ed..cdd1d569c7 100644 --- a/plugins/CuraDrive/src/CreateBackupJob.py +++ b/plugins/CuraDrive/src/CreateBackupJob.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 Ultimaker B.V. +# Copyright (c) 2025 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. import json import threading @@ -13,11 +13,14 @@ from UM.Message import Message from UM.TaskManagement.HttpRequestManager import HttpRequestManager from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope from UM.i18n import i18nCatalog +from cura.ApplicationMetadata import CuraSDKVersion from cura.CuraApplication import CuraApplication from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope +import cura.UltimakerCloud.UltimakerCloudConstants as UltimakerCloudConstants catalog = i18nCatalog("cura") +PACKAGES_URL = f"{UltimakerCloudConstants.CuraCloudAPIRoot}/cura-packages/v{UltimakerCloudConstants.CuraCloudAPIVersion}/cura/v{CuraSDKVersion}/packages" class CreateBackupJob(Job): """Creates backup zip, requests upload url and uploads the backup file to cloud storage.""" @@ -40,23 +43,54 @@ class CreateBackupJob(Job): self._job_done = threading.Event() """Set when the job completes. Does not indicate success.""" self.backup_upload_error_message = "" - """After the job completes, an empty string indicates success. Othrerwise, the value is a translated message.""" + """After the job completes, an empty string indicates success. Otherwise, the value is a translated message.""" + + def _setPluginFetchErrorMessage(self, error_msg: str) -> None: + Logger.error(f"Fetching plugins for backup resulted in error: {error_msg}") + self.backup_upload_error_message = "Couldn't update currently available plugins, backup stopped." + self._upload_message.hide() + self._job_done.set() def run(self) -> None: - upload_message = Message(catalog.i18nc("@info:backup_status", "Creating your backup..."), + self._upload_message = Message(catalog.i18nc("@info:backup_status", "Fetch re-downloadable package-ids..."), title = self.MESSAGE_TITLE, progress = -1) - upload_message.show() + self._upload_message.show() + CuraApplication.getInstance().processEvents() + + if CuraApplication.getInstance().getCuraAPI().backups.shouldReinstallDownloadablePlugins(): + request_url = f"{PACKAGES_URL}?package_type=plugin" + scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance())) + HttpRequestManager.getInstance().get( + request_url, + scope=scope, + callback=self._continueRun, + error_callback=lambda reply, error: self._setPluginFetchErrorMessage(str(error)), + ) + else: + self._continueRun() + + def _continueRun(self, reply: "QNetworkReply" = None) -> None: + if reply is not None: + response_data = HttpRequestManager.readJSON(reply) + if "data" not in response_data: + self._setPluginFetchErrorMessage(f"Missing 'data' from response. Keys in response: {response_data.keys()}") + return + available_remote_plugins = frozenset({v["package_id"] for v in response_data["data"]}) + else: + available_remote_plugins = frozenset() + + self._upload_message.setText(catalog.i18nc("@info:backup_status", "Creating your backup...")) CuraApplication.getInstance().processEvents() cura_api = CuraApplication.getInstance().getCuraAPI() - self._backup_zip, backup_meta_data = cura_api.backups.createBackup() + self._backup_zip, backup_meta_data = cura_api.backups.createBackup(available_remote_plugins) if not self._backup_zip or not backup_meta_data: self.backup_upload_error_message = catalog.i18nc("@info:backup_status", "There was an error while creating your backup.") - upload_message.hide() + self._upload_message.hide() return - upload_message.setText(catalog.i18nc("@info:backup_status", "Uploading your backup...")) + self._upload_message.setText(catalog.i18nc("@info:backup_status", "Uploading your backup...")) CuraApplication.getInstance().processEvents() # Create an upload entry for the backup. @@ -66,11 +100,11 @@ class CreateBackupJob(Job): self._job_done.wait() if self.backup_upload_error_message == "": - upload_message.setText(catalog.i18nc("@info:backup_status", "Your backup has finished uploading.")) - upload_message.setProgress(None) # Hide progress bar + self._upload_message.setText(catalog.i18nc("@info:backup_status", "Your backup has finished uploading.")) + self._upload_message.setProgress(None) # Hide progress bar else: # some error occurred. This error is presented to the user by DrivePluginExtension - upload_message.hide() + self._upload_message.hide() def _requestUploadSlot(self, backup_metadata: Dict[str, Any], backup_size: int) -> None: """Request a backup upload slot from the API. From d167e3f28ed8843786414faa5a0331a5d3ee33fa Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Thu, 27 Feb 2025 16:52:27 +0100 Subject: [PATCH 02/31] Work in progress on pulling plugins out of the backups. It's now in a state where it can actually upload ... something (that should work). Not tested the restore yet. I did run into trouble with the max concurrent requests, which I had to up to [more than 4, now on 8] to get it to work -- I'm not sure if I'm just working around a bug here, or if that's expected behaviour. part of CURA-12156 --- cura/API/Backups.py | 4 +- cura/Backups/Backup.py | 67 ++++++++++++------ cura/Backups/BackupsManager.py | 5 +- plugins/CuraDrive/src/CreateBackupJob.py | 4 ++ plugins/CuraDrive/src/RestoreBackupJob.py | 82 +++++++++++++++++++++-- 5 files changed, 132 insertions(+), 30 deletions(-) diff --git a/cura/API/Backups.py b/cura/API/Backups.py index 07001d22e2..a52dcbfb6b 100644 --- a/cura/API/Backups.py +++ b/cura/API/Backups.py @@ -30,14 +30,14 @@ class Backups: return self.manager.createBackup(available_remote_plugins) - def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, Any]) -> None: + def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, Any], auto_close: bool = True) -> None: """Restore a back-up using the BackupsManager. :param zip_file: A ZIP file containing the actual back-up data. :param meta_data: Some metadata needed for restoring a back-up, like the Cura version number. """ - return self.manager.restoreBackup(zip_file, meta_data) + return self.manager.restoreBackup(zip_file, meta_data, auto_close=auto_close) def shouldReinstallDownloadablePlugins(self) -> bool: return self.manager.shouldReinstallDownloadablePlugins() diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 9f35e54ef1..1163169b94 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -1,5 +1,7 @@ # Copyright (c) 2025 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. +import tempfile + import json import io @@ -8,7 +10,7 @@ import re import shutil from copy import deepcopy from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile -from typing import Dict, Optional, TYPE_CHECKING, List +from typing import Callable, Dict, Optional, TYPE_CHECKING, List from UM import i18nCatalog from UM.Logger import Logger @@ -37,9 +39,6 @@ class Backup: SECRETS_SETTINGS = ["general/ultimaker_auth_data"] """Secret preferences that need to obfuscated when making a backup of Cura""" - TO_INSTALL_FILE = "packages.json" - """File that contains the 'to_install' dictionary, that manages plugins to be installed on next startup.""" - catalog = i18nCatalog("cura") """Re-use translation catalog""" @@ -74,7 +73,7 @@ class Backup: # Create an empty buffer and write the archive to it. buffer = io.BytesIO() - archive = self._makeArchive(buffer, version_data_dir) + archive = self._makeArchive(buffer, version_data_dir, available_remote_plugins) if archive is None: return files = archive.namelist() @@ -83,9 +82,7 @@ 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. 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) - # 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 + plugin_count = len([s for s in files if "plugin.json" in s]) # Store the archive and metadata so the BackupManager can fetch them when needed. self.zip_file = buffer.getvalue() self.meta_data = { @@ -98,19 +95,43 @@ class Backup: # Restore the obfuscated settings self._illuminate(**secrets) - def _fillToInstallsJson(self, file_path: str, reinstall_on_restore: dict[str, str], archive: ZipFile) -> None: - pass # TODO! + def _fillToInstallsJson(self, file_path: str, reinstall_on_restore: frozenset[str], add_to_archive: Callable[[str], None]) -> Optional[str]: + """ Moves all plugin-data (in a config-file) for plugins that could be (re)installed from the Marketplace from + 'installed' to 'to_installs' before adding that file to the archive. - def _findRedownloadablePlugins(self, available_remote_plugins: frozenset) -> dict[str, str]: + Note that the 'filename'-entry in the package-data (of the plugins) might not be valid anymore on restore. + We'll replace it on restore instead, as that's the time when the new package is downloaded. + + :param file_path: Absolute path to the packages-file. + :param reinstall_on_restore: A set of plugins that _can_ be reinstalled from the Marketplace. + :param add_to_archive: A function/lambda that takes a filename and adds it to the archive. + """ + with open(file_path, "r") as file: + data = json.load(file) + reinstall, keep_in = {}, {} + for install_id, install_info in data["installed"].items(): + (reinstall if install_id in reinstall_on_restore else keep_in)[install_id] = install_info + data["installed"] = keep_in + data["to_install"].update(reinstall) + if data is not None: + tmpfile = tempfile.NamedTemporaryFile(delete=False) + with open(tmpfile.name, "w") as outfile: + json.dump(data, outfile) + add_to_archive(tmpfile.name) + return tmpfile.name + return None + + def _findRedownloadablePlugins(self, available_remote_plugins: frozenset) -> (frozenset[str], frozenset[str]): """ Find all plugins that should be able to be reinstalled from the Marketplace. :param plugins_path: Path to all plugins in the user-space. - :return: Set of all package-id's of plugins that can be reinstalled from the Marketplace. + :return: Tuple of a set of plugin-ids and a set of plugin-paths. """ plugin_reg = PluginRegistry.getInstance() id = "id" - return {v["location"]: v[id] for v in plugin_reg.getAllMetaData() - if v[id] in available_remote_plugins and not plugin_reg.isBundledPlugin(v[id])} + plugins = [v for v in plugin_reg.getAllMetaData() + if v[id] in available_remote_plugins and not plugin_reg.isBundledPlugin(v[id])] + return frozenset([v[id] for v in plugins]), frozenset([v["location"] for v in plugins]) def _makeArchive(self, buffer: "io.BytesIO", root_path: str, available_remote_plugins: frozenset) -> Optional[ZipFile]: """Make a full archive from the given root path with the given name. @@ -119,20 +140,28 @@ class Backup: :return: The archive as bytes. """ ignore_string = re.compile("|".join(self.IGNORED_FILES + self.IGNORED_FOLDERS)) - reinstall_instead_plugins = self._findRedownloadablePlugins(available_remote_plugins) + reinstall_instead_ids, reinstall_instead_paths = self._findRedownloadablePlugins(available_remote_plugins) + tmpfiles = [] try: archive = ZipFile(buffer, "w", ZIP_DEFLATED) + add_path_to_archive = lambda path: archive.write(path, path[len(root_path) + len(os.sep):]) for root, folders, files in os.walk(root_path, topdown=True): - folders[:] = [f for f in folders if f not in reinstall_instead_plugins] + folders[:] = [f for f in folders if f not in reinstall_instead_paths] for item_name in folders + files: absolute_path = os.path.join(root, item_name) if ignore_string.search(absolute_path): continue - if item_name == self.TO_INSTALL_FILE: - self._fillToInstallsJson(absolute_path, reinstall_instead_plugins, archive) + if item_name == "packages.json": + tmpfiles.append( + self._fillToInstallsJson(absolute_path, reinstall_instead_ids, add_path_to_archive)) else: - archive.write(absolute_path, absolute_path[len(root_path) + len(os.sep):]) + add_path_to_archive(absolute_path) archive.close() + for tmpfile_path in tmpfiles: + try: + os.remove(tmpfile_path) + except IOError as ex: + Logger.warning(f"Couldn't remove temporary file '{tmpfile_path}' because '{ex}'.") return archive except (IOError, OSError, BadZipfile) as error: Logger.log("e", "Could not create archive from user data directory: %s", error) diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index 90dfc5e34e..67d6c84601 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -39,12 +39,13 @@ class BackupsManager: # We don't return a Backup here because we want plugins only to interact with our API and not full objects. return backup.zip_file, backup.meta_data - def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, str]) -> None: + def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, str], auto_close: bool = True) -> None: """ Restore a back-up from a given ZipFile. :param zip_file: A bytes object containing the actual back-up. :param meta_data: A dict containing some metadata that is needed to restore the back-up correctly. + :param auto_close: Normally, Cura will need to close immediately after restoring the back-up. """ if not meta_data.get("cura_release", None): @@ -57,7 +58,7 @@ class BackupsManager: backup = Backup(self._application, zip_file = zip_file, meta_data = meta_data) restored = backup.restore() - if restored: + if restored and auto_close: # 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. self._application.windowClosed(save_data = False) diff --git a/plugins/CuraDrive/src/CreateBackupJob.py b/plugins/CuraDrive/src/CreateBackupJob.py index cdd1d569c7..6297af305f 100644 --- a/plugins/CuraDrive/src/CreateBackupJob.py +++ b/plugins/CuraDrive/src/CreateBackupJob.py @@ -118,6 +118,8 @@ class CreateBackupJob(Job): } }).encode() + CuraApplication.getInstance().processEvents() # Needed?? + HttpRequestManager.getInstance().put( self._api_backup_url, data = payload, @@ -125,6 +127,8 @@ class CreateBackupJob(Job): error_callback = self._onUploadSlotCompleted, scope = self._json_cloud_scope) + CuraApplication.getInstance().processEvents() # Needed?? + def _onUploadSlotCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None: if HttpRequestManager.safeHttpStatus(reply) >= 300: replyText = HttpRequestManager.readText(reply) diff --git a/plugins/CuraDrive/src/RestoreBackupJob.py b/plugins/CuraDrive/src/RestoreBackupJob.py index 54c94b389e..c5fd1fceae 100644 --- a/plugins/CuraDrive/src/RestoreBackupJob.py +++ b/plugins/CuraDrive/src/RestoreBackupJob.py @@ -1,8 +1,12 @@ # Copyright (c) 2021 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import tempfile + +import json import base64 import hashlib +import os import threading from tempfile import NamedTemporaryFile from typing import Optional, Any, Dict @@ -12,9 +16,16 @@ from PyQt6.QtNetwork import QNetworkReply, QNetworkRequest from UM.Job import Job from UM.Logger import Logger from UM.PackageManager import catalog +from UM.Resources import Resources from UM.TaskManagement.HttpRequestManager import HttpRequestManager -from cura.CuraApplication import CuraApplication +from UM.Version import Version +from cura.ApplicationMetadata import CuraSDKVersion +from cura.CuraApplication import CuraApplication +from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope +import cura.UltimakerCloud.UltimakerCloudConstants as UltimakerCloudConstants + +PACKAGES_URL = f"{UltimakerCloudConstants.CuraCloudAPIRoot}/cura-packages/v{UltimakerCloudConstants.CuraCloudAPIVersion}/cura/v{CuraSDKVersion}/packages" class RestoreBackupJob(Job): """Downloads a backup and overwrites local configuration with the backup. @@ -60,8 +71,8 @@ class RestoreBackupJob(Job): # We store the file in a temporary path fist to ensure integrity. try: - temporary_backup_file = NamedTemporaryFile(delete = False) - with open(temporary_backup_file.name, "wb") as write_backup: + self._temporary_backup_file = NamedTemporaryFile(delete = False) + with open(self._temporary_backup_file.name, "wb") as write_backup: app = CuraApplication.getInstance() bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) while bytes_read: @@ -74,18 +85,75 @@ class RestoreBackupJob(Job): self._job_done.set() return - if not self._verifyMd5Hash(temporary_backup_file.name, self._backup.get("md5_hash", "")): + if not self._verifyMd5Hash(self._temporary_backup_file.name, self._backup.get("md5_hash", "")): # Don't restore the backup if the MD5 hashes do not match. # This can happen if the download was interrupted. Logger.log("w", "Remote and local MD5 hashes do not match, not restoring backup.") self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE # Tell Cura to place the backup back in the user data folder. - with open(temporary_backup_file.name, "rb") as read_backup: + metadata = self._backup.get("metadata", {}) + with open(self._temporary_backup_file.name, "rb") as read_backup: cura_api = CuraApplication.getInstance().getCuraAPI() - cura_api.backups.restoreBackup(read_backup.read(), self._backup.get("metadata", {})) + cura_api.backups.restoreBackup(read_backup.read(), metadata, auto_close=False) - self._job_done.set() + # Read packages data-file, to get the 'to_install' plugin-ids. + version_to_restore = Version(metadata.get("cura_release", "dev")) + version_str = f"{version_to_restore.getMajor()}.{version_to_restore.getMinor()}" + packages_path = os.path.abspath(os.path.join(os.path.abspath( + Resources.getConfigStoragePath()), "..", version_str, "packages.json")) + if not os.path.exists(packages_path): + self._job_done.set() + return + + to_install = set() + try: + with open(packages_path, "r") as packages_file: + packages_json = json.load(packages_file) + if "to_install" in packages_json and "package_id" in packages_json["to_install"]: + to_install.add(packages_json["to_install"]["package_id"]) + except IOError as ex: + pass # TODO! (log + message) + + if len(to_install) < 1: + self._job_done.set() + return + + # Download all re-installable plugins packages, so they can be put back on start-up. + redownload_errors = [] + def packageDownloadCallback(package_id: str, msg: "QNetworkReply", err: "QNetworkReply.NetworkError" = None) -> None: + if err is not None or HttpRequestManager.safeHttpStatus(msg) != 200: + redownload_errors.append(err) + to_install.remove(package_id) + + try: + with tempfile.NamedTemporaryFile(mode="wb+", suffix=".curapackage") as temp_file: + bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) + while bytes_read: + temp_file.write(bytes_read) + bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) + # self._app.processEvents() + # self._progress[package_id]["file_written"] = temp_file.name + if not CuraApplication.getInstance().getPackageManager().installPackage(temp_file.name): + redownload_errors.append(f"Couldn't install package '{package_id}'.") + except IOError as ex: + redownload_errors.append(f"Couldn't read package '{package_id}' because '{ex}'.") + + if len(to_install) < 1: + if len(redownload_errors) == 0: + self._job_done.set() + else: + print("|".join(redownload_errors)) # TODO: Message / Log instead. + self._job_done.set() # NOTE: Set job probably not the right call here... (depends on wether or not that in the end closes the app or not...) + + self._package_download_scope = UltimakerCloudScope(CuraApplication.getInstance()) + for package_id in to_install: + HttpRequestManager.getInstance().get( + f"{PACKAGES_URL}/{package_id}/download", + scope=self._package_download_scope, + callback=lambda msg: packageDownloadCallback(package_id, msg), + error_callback=lambda msg, err: packageDownloadCallback(package_id, msg, err) + ) @staticmethod def _verifyMd5Hash(file_path: str, known_hash: str) -> bool: From 82939b2644e598dab75970c9831b78233c444cb5 Mon Sep 17 00:00:00 2001 From: RedBlackAka <140876408+RedBlackAka@users.noreply.github.com> Date: Fri, 21 Feb 2025 15:53:00 +0100 Subject: [PATCH 03/31] Clean up Windows Start Menu --- packaging/NSIS/Ultimaker-Cura.nsi.jinja | 32 +++++++++---------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/packaging/NSIS/Ultimaker-Cura.nsi.jinja b/packaging/NSIS/Ultimaker-Cura.nsi.jinja index ac826af0d9..335626da12 100644 --- a/packaging/NSIS/Ultimaker-Cura.nsi.jinja +++ b/packaging/NSIS/Ultimaker-Cura.nsi.jinja @@ -16,13 +16,11 @@ !define REG_APP_PATH "Software\Microsoft\Windows\CurrentVersion\App Paths\${APP_NAME}-${VERSION}" !define UNINSTALL_PATH "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}-${VERSION}" -!define REG_START_MENU "Start Menu Folder" +!define REG_START_MENU "Start Menu Shortcut" ;Require administrator access RequestExecutionLevel admin -var SM_Folder - ###################################################################### VIProductVersion "${VIVERSION}" @@ -68,7 +66,6 @@ InstallDir "$PROGRAMFILES64\${APP_NAME}" !define MUI_STARTMENUPAGE_REGISTRY_ROOT "${REG_ROOT}" !define MUI_STARTMENUPAGE_REGISTRY_KEY "${UNINSTALL_PATH}" !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "${REG_START_MENU}" -!insertmacro MUI_PAGE_STARTMENU Application $SM_Folder !endif !insertmacro MUI_PAGE_INSTFILES @@ -108,25 +105,21 @@ WriteUninstaller "$INSTDIR\uninstall.exe" !ifdef REG_START_MENU !insertmacro MUI_STARTMENU_WRITE_BEGIN Application -CreateDirectory "$SMPROGRAMS\$SM_Folder" -CreateShortCut "$SMPROGRAMS\$SM_Folder\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" -CreateShortCut "$SMPROGRAMS\$SM_Folder\Uninstall ${APP_NAME}.lnk" "$INSTDIR\uninstall.exe" +CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" !ifdef WEB_SITE WriteIniStr "$INSTDIR\UltiMaker Cura website.url" "InternetShortcut" "URL" "${WEB_SITE}" -CreateShortCut "$SMPROGRAMS\$SM_Folder\UltiMaker Cura website.lnk" "$INSTDIR\UltiMaker Cura website.url" +CreateShortCut "$SMPROGRAMS\UltiMaker Cura website.lnk" "$INSTDIR\UltiMaker Cura website.url" !endif !insertmacro MUI_STARTMENU_WRITE_END !endif !ifndef REG_START_MENU -CreateDirectory "$SMPROGRAMS\{{ app_name }}" -CreateShortCut "$SMPROGRAMS\{{ app_name }}\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" -CreateShortCut "$SMPROGRAMS\{{ app_name }}\Uninstall ${APP_NAME}.lnk" "$INSTDIR\uninstall.exe" +CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" !ifdef WEB_SITE WriteIniStr "$INSTDIR\UltiMaker Cura website.url" "InternetShortcut" "URL" "${WEB_SITE}" -CreateShortCut "$SMPROGRAMS\{{ app_name }}\UltiMaker Cura website.lnk" "$INSTDIR\UltiMaker Cura website.url" +CreateShortCut "$SMPROGRAMS\UltiMaker Cura website.lnk" "$INSTDIR\UltiMaker Cura website.url" !endif !endif @@ -184,22 +177,19 @@ Delete "$INSTDIR\${APP_NAME} website.url" RmDir /r /REBOOTOK "$INSTDIR" !ifdef REG_START_MENU -!insertmacro MUI_STARTMENU_GETFOLDER "Application" $SM_Folder -Delete "$SMPROGRAMS\$SM_Folder\${APP_NAME}.lnk" -Delete "$SMPROGRAMS\$SM_Folder\Uninstall ${APP_NAME}.lnk" +Delete "$SMPROGRAMS\${APP_NAME}.lnk" +Delete "$SMPROGRAMS\Uninstall ${APP_NAME}.lnk" !ifdef WEB_SITE -Delete "$SMPROGRAMS\$SM_Folder\UltiMaker Cura website.lnk" +Delete "$SMPROGRAMS\UltiMaker Cura website.lnk" !endif -RmDir "$SMPROGRAMS\$SM_Folder" !endif !ifndef REG_START_MENU -Delete "$SMPROGRAMS\{{ app_name }}\${APP_NAME}.lnk" -Delete "$SMPROGRAMS\{{ app_name }}\Uninstall ${APP_NAME}.lnk" +Delete "$SMPROGRAMS\${APP_NAME}.lnk" +Delete "$SMPROGRAMS\Uninstall ${APP_NAME}.lnk" !ifdef WEB_SITE -Delete "$SMPROGRAMS\{{ app_name }}\UltiMaker Cura website.lnk" +Delete "$SMPROGRAMS\UltiMaker Cura website.lnk" !endif -RmDir "$SMPROGRAMS\{{ app_name }}" !endif !insertmacro APP_UNASSOCIATE "stl" "Cura.model" From c33a32209311f0fb96c60db25e6743141e2a4064 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 26 Mar 2025 08:57:43 +0100 Subject: [PATCH 04/31] Also remove website link. Would otherwise be hanging loose in the start-menu somehwere. done as part of CURA-12502 --- packaging/NSIS/Ultimaker-Cura.nsi.jinja | 24 +--------------------- packaging/NSIS/create_windows_installer.py | 3 +-- packaging/msi/create_windows_msi.py | 3 +-- 3 files changed, 3 insertions(+), 27 deletions(-) diff --git a/packaging/NSIS/Ultimaker-Cura.nsi.jinja b/packaging/NSIS/Ultimaker-Cura.nsi.jinja index 335626da12..8c5d48f9dd 100644 --- a/packaging/NSIS/Ultimaker-Cura.nsi.jinja +++ b/packaging/NSIS/Ultimaker-Cura.nsi.jinja @@ -1,9 +1,8 @@ -# Copyright (c) 2022 UltiMaker B.V. +# Copyright (c) 2025 UltiMaker # Cura's build system is released under the terms of the AGPLv3 or higher. !define APP_NAME "{{ app_name }}" !define COMP_NAME "{{ company }}" -!define WEB_SITE "{{ web_site }}" !define VERSION "{{ version }}" !define VIVERSION "{{ version_major }}.{{ version_minor }}.{{ version_patch }}.0" !define COPYRIGHT "Copyright (c) {{ year }} {{ company }}" @@ -107,20 +106,11 @@ WriteUninstaller "$INSTDIR\uninstall.exe" !insertmacro MUI_STARTMENU_WRITE_BEGIN Application CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" -!ifdef WEB_SITE -WriteIniStr "$INSTDIR\UltiMaker Cura website.url" "InternetShortcut" "URL" "${WEB_SITE}" -CreateShortCut "$SMPROGRAMS\UltiMaker Cura website.lnk" "$INSTDIR\UltiMaker Cura website.url" -!endif !insertmacro MUI_STARTMENU_WRITE_END !endif !ifndef REG_START_MENU CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" - -!ifdef WEB_SITE -WriteIniStr "$INSTDIR\UltiMaker Cura website.url" "InternetShortcut" "URL" "${WEB_SITE}" -CreateShortCut "$SMPROGRAMS\UltiMaker Cura website.lnk" "$INSTDIR\UltiMaker Cura website.url" -!endif !endif WriteRegStr ${REG_ROOT} "${REG_APP_PATH}" "" "$INSTDIR\${MAIN_APP_EXE}" @@ -131,9 +121,6 @@ WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayIcon" "$INSTDIR\${MAIN_APP_ WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayVersion" "${VERSION}" WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "Publisher" "${COMP_NAME}" -!ifdef WEB_SITE -WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "URLInfoAbout" "${WEB_SITE}" -!endif SectionEnd ###################################################################### @@ -170,26 +157,17 @@ RmDir "$INSTDIR\share\uranium" RmDir "$INSTDIR\share" Delete "$INSTDIR\uninstall.exe" -!ifdef WEB_SITE -Delete "$INSTDIR\${APP_NAME} website.url" -!endif RmDir /r /REBOOTOK "$INSTDIR" !ifdef REG_START_MENU Delete "$SMPROGRAMS\${APP_NAME}.lnk" Delete "$SMPROGRAMS\Uninstall ${APP_NAME}.lnk" -!ifdef WEB_SITE -Delete "$SMPROGRAMS\UltiMaker Cura website.lnk" -!endif !endif !ifndef REG_START_MENU Delete "$SMPROGRAMS\${APP_NAME}.lnk" Delete "$SMPROGRAMS\Uninstall ${APP_NAME}.lnk" -!ifdef WEB_SITE -Delete "$SMPROGRAMS\UltiMaker Cura website.lnk" -!endif !endif !insertmacro APP_UNASSOCIATE "stl" "Cura.model" diff --git a/packaging/NSIS/create_windows_installer.py b/packaging/NSIS/create_windows_installer.py index d15d62b951..e01c757dbb 100644 --- a/packaging/NSIS/create_windows_installer.py +++ b/packaging/NSIS/create_windows_installer.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 UltiMaker +# Copyright (c) 2025 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. @@ -51,7 +51,6 @@ def generate_nsi(source_path: str, dist_path: str, filename: str, version: str): version_minor = str(parsed_version.minor), version_patch = str(parsed_version.patch), company = "UltiMaker", - web_site = "https://ultimaker.com", year = datetime.now().year, cura_license_file = str(source_loc.joinpath("packaging", "cura_license.txt")), compression_method = "LZMA", # ZLIB, BZIP2 or LZMA diff --git a/packaging/msi/create_windows_msi.py b/packaging/msi/create_windows_msi.py index e44a9a924b..12c64ed24f 100644 --- a/packaging/msi/create_windows_msi.py +++ b/packaging/msi/create_windows_msi.py @@ -1,4 +1,4 @@ -# Copyright (c) 2022 UltiMaker +# Copyright (c) 2025 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. @@ -40,7 +40,6 @@ def generate_wxs(source_path: Path, dist_path: Path, filename: Path, app_name: s version_minor=str(parsed_version.minor), version_patch=str(parsed_version.patch), company="UltiMaker", - web_site="https://ultimaker.com", year=datetime.now().year, upgrade_code=str(uuid.uuid5(uuid.NAMESPACE_DNS, app_name)), cura_license_file=str(source_loc.joinpath("packaging", "msi", "cura_license.rtf")), From 460df87d1d0ad4289b5cdcbefd6a669526f7eec5 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Thu, 27 Mar 2025 10:47:18 +0100 Subject: [PATCH 05/31] Indicates that changing option requires a restart CURA-12486 Also move the note at the bottom of the page, because now labels from different categories use it, and this is where people should look for it. --- resources/qml/Preferences/GeneralPage.qml | 27 +++++++++++------------ 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/resources/qml/Preferences/GeneralPage.qml b/resources/qml/Preferences/GeneralPage.qml index 62cab53a78..42469c6cf6 100644 --- a/resources/qml/Preferences/GeneralPage.qml +++ b/resources/qml/Preferences/GeneralPage.qml @@ -360,17 +360,6 @@ UM.PreferencesPage } } - UM.Label - { - id: languageCaption - - //: Language change warning - text: catalog.i18nc("@label", "*You will need to restart the application for these changes to have effect.") - wrapMode: Text.WordWrap - font.italic: true - - } - Item { //: Spacer @@ -705,7 +694,7 @@ UM.PreferencesPage UM.CheckBox { id: singleInstanceCheckbox - text: catalog.i18nc("@option:check","Use a single instance of Cura") + text: catalog.i18nc("@option:check","Use a single instance of Cura *") checked: boolCheck(UM.Preferences.getValue("cura/single_instance")) onCheckedChanged: UM.Preferences.setValue("cura/single_instance", checked) @@ -1101,8 +1090,6 @@ UM.PreferencesPage } } - - /* Multi-buildplate functionality is disabled because it's broken. See CURA-4975 for the ticket to remove it. Item { //: Spacer @@ -1110,6 +1097,18 @@ UM.PreferencesPage width: UM.Theme.getSize("default_margin").height } + UM.Label + { + id: languageCaption + + //: Language change warning + text: catalog.i18nc("@label", "*You will need to restart the application for these changes to have effect.") + wrapMode: Text.WordWrap + font.italic: true + } + + /* Multi-buildplate functionality is disabled because it's broken. See CURA-4975 for the ticket to remove it. + Label { font.bold: true From b295ca7d041776d2f7c06be4146fbac0e043424f Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Tue, 1 Apr 2025 15:10:27 +0200 Subject: [PATCH 06/31] Workaround for process-events during multiple http requests. Would not work otherwise, but that should have been true in the old situation then as well? CURA-12156 --- plugins/CuraDrive/src/CreateBackupJob.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/CuraDrive/src/CreateBackupJob.py b/plugins/CuraDrive/src/CreateBackupJob.py index 6297af305f..67083d2e83 100644 --- a/plugins/CuraDrive/src/CreateBackupJob.py +++ b/plugins/CuraDrive/src/CreateBackupJob.py @@ -98,7 +98,12 @@ class CreateBackupJob(Job): backup_meta_data["description"] = "{}.backup.{}.cura.zip".format(timestamp, backup_meta_data["cura_release"]) self._requestUploadSlot(backup_meta_data, len(self._backup_zip)) - self._job_done.wait() + # Note: One 'process events' call wasn't enough with the changed situation somehow. + active_done_check = False + while not active_done_check: + CuraApplication.getInstance().processEvents() + active_done_check = self._job_done.wait(0.02) + if self.backup_upload_error_message == "": self._upload_message.setText(catalog.i18nc("@info:backup_status", "Your backup has finished uploading.")) self._upload_message.setProgress(None) # Hide progress bar @@ -117,9 +122,6 @@ class CreateBackupJob(Job): "metadata": backup_metadata } }).encode() - - CuraApplication.getInstance().processEvents() # Needed?? - HttpRequestManager.getInstance().put( self._api_backup_url, data = payload, @@ -127,8 +129,6 @@ class CreateBackupJob(Job): error_callback = self._onUploadSlotCompleted, scope = self._json_cloud_scope) - CuraApplication.getInstance().processEvents() # Needed?? - def _onUploadSlotCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None: if HttpRequestManager.safeHttpStatus(reply) >= 300: replyText = HttpRequestManager.readText(reply) From c857dab0f76196a803871da1b10a331458b262ee Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Tue, 1 Apr 2025 16:47:17 +0200 Subject: [PATCH 07/31] Logging (mostly on errors). CURA-12156 --- plugins/CuraDrive/src/RestoreBackupJob.py | 42 ++++++++++++++--------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/plugins/CuraDrive/src/RestoreBackupJob.py b/plugins/CuraDrive/src/RestoreBackupJob.py index c5fd1fceae..e7cd66daf8 100644 --- a/plugins/CuraDrive/src/RestoreBackupJob.py +++ b/plugins/CuraDrive/src/RestoreBackupJob.py @@ -1,11 +1,8 @@ -# Copyright (c) 2021 Ultimaker B.V. +# Copyright (c) 2025 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. -import tempfile - -import json - import base64 import hashlib +import json import os import threading from tempfile import NamedTemporaryFile @@ -49,7 +46,6 @@ class RestoreBackupJob(Job): self.restore_backup_error_message = "" def run(self) -> None: - url = self._backup.get("download_url") assert url is not None @@ -59,7 +55,11 @@ class RestoreBackupJob(Job): error_callback = self._onRestoreRequestCompleted ) - self._job_done.wait() # A job is considered finished when the run function completes + # Note: Just to be sure, use the same structure here as in CreateBackupJob. + active_done_check = False + while not active_done_check: + CuraApplication.getInstance().processEvents() + active_done_check = self._job_done.wait(0.02) def _onRestoreRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None: if not HttpRequestManager.replyIndicatesSuccess(reply, error): @@ -80,7 +80,7 @@ class RestoreBackupJob(Job): bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) app.processEvents() except EnvironmentError as e: - Logger.log("e", f"Unable to save backed up files due to computer limitations: {str(e)}") + Logger.error(f"Unable to save backed up files due to computer limitations: {str(e)}") self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE self._job_done.set() return @@ -88,8 +88,10 @@ class RestoreBackupJob(Job): if not self._verifyMd5Hash(self._temporary_backup_file.name, self._backup.get("md5_hash", "")): # Don't restore the backup if the MD5 hashes do not match. # This can happen if the download was interrupted. - Logger.log("w", "Remote and local MD5 hashes do not match, not restoring backup.") + Logger.error("Remote and local MD5 hashes do not match, not restoring backup.") self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE + self._job_done.set() + return # Tell Cura to place the backup back in the user data folder. metadata = self._backup.get("metadata", {}) @@ -103,6 +105,8 @@ class RestoreBackupJob(Job): packages_path = os.path.abspath(os.path.join(os.path.abspath( Resources.getConfigStoragePath()), "..", version_str, "packages.json")) if not os.path.exists(packages_path): + Logger.error(f"Can't find path '{packages_path}' to tell what packages should be redownloaded.") + self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE self._job_done.set() return @@ -113,9 +117,13 @@ class RestoreBackupJob(Job): if "to_install" in packages_json and "package_id" in packages_json["to_install"]: to_install.add(packages_json["to_install"]["package_id"]) except IOError as ex: - pass # TODO! (log + message) + Logger.error(f"Couldn't open '{packages_path}' because '{str(ex)}' to get packages to re-install.") + self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE + self._job_done.set() + return if len(to_install) < 1: + Logger.info("No packages to reinstall, early out.") self._job_done.set() return @@ -127,24 +135,26 @@ class RestoreBackupJob(Job): to_install.remove(package_id) try: - with tempfile.NamedTemporaryFile(mode="wb+", suffix=".curapackage") as temp_file: + with NamedTemporaryFile(mode="wb+", suffix=".curapackage") as temp_file: bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) while bytes_read: temp_file.write(bytes_read) bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) - # self._app.processEvents() - # self._progress[package_id]["file_written"] = temp_file.name + CuraApplication.getInstance().processEvents() if not CuraApplication.getInstance().getPackageManager().installPackage(temp_file.name): redownload_errors.append(f"Couldn't install package '{package_id}'.") except IOError as ex: - redownload_errors.append(f"Couldn't read package '{package_id}' because '{ex}'.") + redownload_errors.append(f"Couldn't process package '{package_id}' because '{ex}'.") if len(to_install) < 1: if len(redownload_errors) == 0: + Logger.info("All packages redownloaded!") self._job_done.set() else: - print("|".join(redownload_errors)) # TODO: Message / Log instead. - self._job_done.set() # NOTE: Set job probably not the right call here... (depends on wether or not that in the end closes the app or not...) + msgs = "\n - ".join(redownload_errors) + Logger.error(f"Couldn't re-install at least one package(s) because: {msgs}") + self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE + self._job_done.set() self._package_download_scope = UltimakerCloudScope(CuraApplication.getInstance()) for package_id in to_install: From 6458c17de5b2a92a3179e2747b75c6ea394f138e Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 2 Apr 2025 13:09:21 +0200 Subject: [PATCH 08/31] Save 'pluginless' bakcup correctly. - Fix: Save the tempfile to the archive under the 'original' name (it is a rewrite of) instead of saving it to the archive under it's own name, which skipped the original file completely in a sense (the info was there, but as a tempfile). - Fix: Also make sure the correct folders where ignored, as reinstall paths where the complete path, not the basename. part of CURA-12156 --- cura/Backups/Backup.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 1163169b94..a409c4c689 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -95,7 +95,7 @@ class Backup: # Restore the obfuscated settings self._illuminate(**secrets) - def _fillToInstallsJson(self, file_path: str, reinstall_on_restore: frozenset[str], add_to_archive: Callable[[str], None]) -> Optional[str]: + def _fillToInstallsJson(self, file_path: str, reinstall_on_restore: frozenset[str], add_to_archive: Callable[[str, str], None]) -> Optional[str]: """ Moves all plugin-data (in a config-file) for plugins that could be (re)installed from the Marketplace from 'installed' to 'to_installs' before adding that file to the archive. @@ -104,7 +104,7 @@ class Backup: :param file_path: Absolute path to the packages-file. :param reinstall_on_restore: A set of plugins that _can_ be reinstalled from the Marketplace. - :param add_to_archive: A function/lambda that takes a filename and adds it to the archive. + :param add_to_archive: A function/lambda that takes a filename and adds it to the archive (as the 2nd name). """ with open(file_path, "r") as file: data = json.load(file) @@ -117,7 +117,7 @@ class Backup: tmpfile = tempfile.NamedTemporaryFile(delete=False) with open(tmpfile.name, "w") as outfile: json.dump(data, outfile) - add_to_archive(tmpfile.name) + add_to_archive(tmpfile.name, file_path) return tmpfile.name return None @@ -144,18 +144,17 @@ class Backup: tmpfiles = [] try: archive = ZipFile(buffer, "w", ZIP_DEFLATED) - add_path_to_archive = lambda path: archive.write(path, path[len(root_path) + len(os.sep):]) + add_path_to_archive = lambda path, alt_path: archive.write(path, alt_path[len(root_path) + len(os.sep):]) for root, folders, files in os.walk(root_path, topdown=True): - folders[:] = [f for f in folders if f not in reinstall_instead_paths] for item_name in folders + files: absolute_path = os.path.join(root, item_name) - if ignore_string.search(absolute_path): + if ignore_string.search(absolute_path) or any([absolute_path.startswith(x) for x in reinstall_instead_paths]): continue if item_name == "packages.json": tmpfiles.append( self._fillToInstallsJson(absolute_path, reinstall_instead_ids, add_path_to_archive)) else: - add_path_to_archive(absolute_path) + add_path_to_archive(absolute_path, absolute_path) archive.close() for tmpfile_path in tmpfiles: try: From 1f4f432d49c2649521fde3233de4943bcacfb080 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 2 Apr 2025 13:13:25 +0200 Subject: [PATCH 09/31] Restore Backups: Fix reading which packages need to be reinstalled. Also save the version, so we can get the correct url when redownloading the package (see next commits). part of CURA-12156 --- plugins/CuraDrive/src/RestoreBackupJob.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/plugins/CuraDrive/src/RestoreBackupJob.py b/plugins/CuraDrive/src/RestoreBackupJob.py index e7cd66daf8..4127df7aa6 100644 --- a/plugins/CuraDrive/src/RestoreBackupJob.py +++ b/plugins/CuraDrive/src/RestoreBackupJob.py @@ -110,12 +110,17 @@ class RestoreBackupJob(Job): self._job_done.set() return - to_install = set() + to_install = {} try: with open(packages_path, "r") as packages_file: packages_json = json.load(packages_file) - if "to_install" in packages_json and "package_id" in packages_json["to_install"]: - to_install.add(packages_json["to_install"]["package_id"]) + if "to_install" in packages_json: + for package_data in packages_json["to_install"].values(): + if "package_info" not in package_data: + continue + package_info = package_data["package_info"] + if "package_id" in package_info and "sdk_version_semver" in package_info: + to_install[package_info["package_id"]] = package_info["sdk_version_semver"] except IOError as ex: Logger.error(f"Couldn't open '{packages_path}' because '{str(ex)}' to get packages to re-install.") self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE @@ -132,7 +137,7 @@ class RestoreBackupJob(Job): def packageDownloadCallback(package_id: str, msg: "QNetworkReply", err: "QNetworkReply.NetworkError" = None) -> None: if err is not None or HttpRequestManager.safeHttpStatus(msg) != 200: redownload_errors.append(err) - to_install.remove(package_id) + del to_install[package_id] try: with NamedTemporaryFile(mode="wb+", suffix=".curapackage") as temp_file: From 43d9e1d522e449c9880428f7d7fd0f8f3a111073 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 2 Apr 2025 13:15:35 +0200 Subject: [PATCH 10/31] Restore Backups: Fix handling the (re)downloaded tempfile. Was opening the tempfile for handling when it was still open for writing. Also the wrong net-reply got used (reply instead of msg). part of CURA-12156 --- plugins/CuraDrive/src/RestoreBackupJob.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/CuraDrive/src/RestoreBackupJob.py b/plugins/CuraDrive/src/RestoreBackupJob.py index 4127df7aa6..aa48ca5c18 100644 --- a/plugins/CuraDrive/src/RestoreBackupJob.py +++ b/plugins/CuraDrive/src/RestoreBackupJob.py @@ -140,14 +140,15 @@ class RestoreBackupJob(Job): del to_install[package_id] try: - with NamedTemporaryFile(mode="wb+", suffix=".curapackage") as temp_file: - bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) + with NamedTemporaryFile(mode="wb", suffix=".curapackage", delete=False) as temp_file: + bytes_read = msg.read(self.DISK_WRITE_BUFFER_SIZE) while bytes_read: temp_file.write(bytes_read) - bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) + bytes_read = msg.read(self.DISK_WRITE_BUFFER_SIZE) CuraApplication.getInstance().processEvents() - if not CuraApplication.getInstance().getPackageManager().installPackage(temp_file.name): - redownload_errors.append(f"Couldn't install package '{package_id}'.") + temp_file.close() + if not CuraApplication.getInstance().getPackageManager().installPackage(temp_file.name): + redownload_errors.append(f"Couldn't install package '{package_id}'.") except IOError as ex: redownload_errors.append(f"Couldn't process package '{package_id}' because '{ex}'.") From 1fb89e0e7d80635e60ce4bf38e2b1853c66d4ac8 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 2 Apr 2025 13:19:01 +0200 Subject: [PATCH 11/31] Restore Backups: Correctly handle requests to redownload plugins. - Fix: Need to fill the package's API version into the (template) URL, instead of using latest. - Fix: Loop was closing over the package ID, which caused the recieving function to always only get the last handled package ID instead of the one it needed to handle. part of CURA-12156 --- plugins/CuraDrive/src/RestoreBackupJob.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/plugins/CuraDrive/src/RestoreBackupJob.py b/plugins/CuraDrive/src/RestoreBackupJob.py index aa48ca5c18..817d819abf 100644 --- a/plugins/CuraDrive/src/RestoreBackupJob.py +++ b/plugins/CuraDrive/src/RestoreBackupJob.py @@ -22,7 +22,7 @@ from cura.CuraApplication import CuraApplication from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope import cura.UltimakerCloud.UltimakerCloudConstants as UltimakerCloudConstants -PACKAGES_URL = f"{UltimakerCloudConstants.CuraCloudAPIRoot}/cura-packages/v{UltimakerCloudConstants.CuraCloudAPIVersion}/cura/v{CuraSDKVersion}/packages" +PACKAGES_URL_TEMPLATE = f"{UltimakerCloudConstants.CuraCloudAPIRoot}/cura-packages/v{UltimakerCloudConstants.CuraCloudAPIVersion}/cura/v{{0}}/packages/{{1}}/download" class RestoreBackupJob(Job): """Downloads a backup and overwrites local configuration with the backup. @@ -163,13 +163,15 @@ class RestoreBackupJob(Job): self._job_done.set() self._package_download_scope = UltimakerCloudScope(CuraApplication.getInstance()) - for package_id in to_install: - HttpRequestManager.getInstance().get( - f"{PACKAGES_URL}/{package_id}/download", - scope=self._package_download_scope, - callback=lambda msg: packageDownloadCallback(package_id, msg), - error_callback=lambda msg, err: packageDownloadCallback(package_id, msg, err) - ) + for package_id, package_api_version in to_install.items(): + def handlePackageId(package_id: str = package_id): + HttpRequestManager.getInstance().get( + PACKAGES_URL_TEMPLATE.format(package_api_version, package_id), + scope=self._package_download_scope, + callback=lambda msg: packageDownloadCallback(package_id, msg), + error_callback=lambda msg, err: packageDownloadCallback(package_id, msg, err) + ) + handlePackageId(package_id) @staticmethod def _verifyMd5Hash(file_path: str, known_hash: str) -> bool: From 0ec825b1ac4e55e404f319df1a6ef50c4d322837 Mon Sep 17 00:00:00 2001 From: Paul Kuiper <46715907+pkuiper-ultimaker@users.noreply.github.com> Date: Fri, 4 Apr 2025 12:02:58 +0200 Subject: [PATCH 12/31] New configurations for the S line printers. PP-602 --- resources/definitions/ultimaker_s6.def.json | 53 ++++++++++++++++++ resources/definitions/ultimaker_s8.def.json | 2 +- .../ultimaker_s6_extruder_left.def.json | 31 ++++++++++ .../ultimaker_s6_extruder_right.def.json | 31 ++++++++++ resources/images/UltimakerS6backplate.png | Bin 0 -> 24368 bytes ..._nylon-cf-slide_0.2mm_engineering.inst.cfg | 18 ++++++ ...plus_0.6_petcf_0.2mm_engineering.inst.cfg} | 4 +- .../um_s8_aa_plus_0.4_abs_0.2mm.inst.cfg | 1 - .../um_s8_aa_plus_0.6_abs_0.2mm.inst.cfg | 23 ++++++++ .../um_s8_aa_plus_0.6_abs_0.3mm.inst.cfg | 25 +++++++++ .../um_s8_aa_plus_0.6_petg_0.2mm.inst.cfg | 20 +++++++ .../um_s8_aa_plus_0.6_petg_0.3mm.inst.cfg | 21 +++++++ .../um_s8_aa_plus_0.6_pla_0.2mm.inst.cfg | 20 +++++++ .../um_s8_aa_plus_0.6_pla_0.3mm.inst.cfg | 22 ++++++++ ...um_s8_aa_plus_0.6_tough-pla_0.2mm.inst.cfg | 20 +++++++ ...um_s8_aa_plus_0.6_tough-pla_0.3mm.inst.cfg | 22 ++++++++ ..._cc_plus_0.6_nylon-cf-slide_0.2mm.inst.cfg | 17 ++++++ ..._cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg | 17 ++++++ .../um_s8_cc_plus_0.6_petcf_0.2mm.inst.cfg | 18 ++++++ .../um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg | 20 +++++++ .../variants/ultimaker_s6_aa_plus04.inst.cfg | 17 ++++++ .../variants/ultimaker_s6_bb0.8.inst.cfg | 35 ++++++++++++ resources/variants/ultimaker_s6_bb04.inst.cfg | 19 +++++++ .../variants/ultimaker_s6_cc_plus04.inst.cfg | 17 ++++++ resources/variants/ultimaker_s6_dd04.inst.cfg | 17 ++++++ .../variants/ultimaker_s8_aa_plus06.inst.cfg | 17 ++++++ .../variants/ultimaker_s8_cc_plus06.inst.cfg | 17 ++++++ 27 files changed, 520 insertions(+), 4 deletions(-) create mode 100644 resources/definitions/ultimaker_s6.def.json create mode 100644 resources/extruders/ultimaker_s6_extruder_left.def.json create mode 100644 resources/extruders/ultimaker_s6_extruder_right.def.json create mode 100644 resources/images/UltimakerS6backplate.png create mode 100644 resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm_engineering.inst.cfg rename resources/intent/ultimaker_s8/{um_s8_aa_plus_0.4_nylon_0.2mm_engineering.inst.cfg => um_s8_cc_plus_0.6_petcf_0.2mm_engineering.inst.cfg} (86%) create mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_abs_0.2mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_abs_0.3mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_petg_0.2mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_petg_0.3mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_pla_0.2mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_pla_0.3mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_tough-pla_0.2mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_tough-pla_0.3mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm.inst.cfg create mode 100644 resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg create mode 100644 resources/variants/ultimaker_s6_aa_plus04.inst.cfg create mode 100644 resources/variants/ultimaker_s6_bb0.8.inst.cfg create mode 100644 resources/variants/ultimaker_s6_bb04.inst.cfg create mode 100644 resources/variants/ultimaker_s6_cc_plus04.inst.cfg create mode 100644 resources/variants/ultimaker_s6_dd04.inst.cfg create mode 100644 resources/variants/ultimaker_s8_aa_plus06.inst.cfg create mode 100644 resources/variants/ultimaker_s8_cc_plus06.inst.cfg diff --git a/resources/definitions/ultimaker_s6.def.json b/resources/definitions/ultimaker_s6.def.json new file mode 100644 index 0000000000..ec6c189eda --- /dev/null +++ b/resources/definitions/ultimaker_s6.def.json @@ -0,0 +1,53 @@ +{ + "version": 2, + "name": "UltiMaker S6", + "inherits": "ultimaker_s8", + "metadata": + { + "visible": true, + "author": "UltiMaker", + "manufacturer": "Ultimaker B.V.", + "file_formats": "application/x-ufp;text/x-gcode", + "platform": "ultimaker_s7_platform.obj", + "bom_numbers": [ + 10700 + ], + "firmware_update_info": + { + "check_urls": [ "https://software.ultimaker.com/releases/firmware/5078167/stable/um-update.swu.version" ], + "id": 5078167, + "update_url": "https://ultimaker.com/firmware?utm_source=cura&utm_medium=software&utm_campaign=fw-update" + }, + "first_start_actions": [ "DiscoverUM3Action" ], + "has_machine_quality": true, + "has_materials": true, + "has_variants": true, + "machine_extruder_trains": + { + "0": "ultimaker_s6_extruder_left", + "1": "ultimaker_s6_extruder_right" + }, + "nozzle_offsetting_for_disallowed_areas": false, + "platform_offset": [ + 0, + 0, + 0 + ], + "platform_texture": "UltimakerS6backplate.png", + "preferred_material": "ultimaker_pla_blue", + "preferred_variant_name": "AA+ 0.4", + "quality_definition": "ultimaker_s8", + "supported_actions": [ "DiscoverUM3Action" ], + "supports_material_export": true, + "supports_network_connection": true, + "supports_usb_connection": false, + "variants_name": "Print Core", + "variants_name_has_translation": true, + "weight": -2 + }, + "overrides": + { + "adhesion_type": { "value": "'brim'" }, + "machine_name": { "default_value": "Ultimaker S6" } + } +} \ No newline at end of file diff --git a/resources/definitions/ultimaker_s8.def.json b/resources/definitions/ultimaker_s8.def.json index 80e7986acf..5a4d44fa2f 100644 --- a/resources/definitions/ultimaker_s8.def.json +++ b/resources/definitions/ultimaker_s8.def.json @@ -412,7 +412,7 @@ "retraction_hop": { "value": 1 }, "retraction_hop_after_extruder_switch_height": { "value": 2 }, "retraction_hop_enabled": { "value": true }, - "retraction_min_travel": { "value": "5 if support_enable and support_structure=='tree' else line_width * 2" }, + "retraction_min_travel": { "value": "5 if support_enable and support_structure=='tree' else line_width * 2.5" }, "retraction_prime_speed": { "value": 15 }, "skin_edge_support_thickness": { "value": 0 }, "skin_material_flow": { "value": 95 }, diff --git a/resources/extruders/ultimaker_s6_extruder_left.def.json b/resources/extruders/ultimaker_s6_extruder_left.def.json new file mode 100644 index 0000000000..d3991222b2 --- /dev/null +++ b/resources/extruders/ultimaker_s6_extruder_left.def.json @@ -0,0 +1,31 @@ +{ + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": + { + "machine": "ultimaker_s6", + "position": "0" + }, + "overrides": + { + "extruder_nr": + { + "default_value": 0, + "maximum_value": "1" + }, + "extruder_prime_pos_x": { "default_value": -3 }, + "extruder_prime_pos_y": { "default_value": 6 }, + "extruder_prime_pos_z": { "default_value": 2 }, + "machine_extruder_end_pos_abs": { "default_value": true }, + "machine_extruder_end_pos_x": { "default_value": 330 }, + "machine_extruder_end_pos_y": { "default_value": 237 }, + "machine_extruder_start_code": { "value": "\"M214 D0 K{material_pressure_advance_factor} R0.04\"" }, + "machine_extruder_start_pos_abs": { "default_value": true }, + "machine_extruder_start_pos_x": { "default_value": 330 }, + "machine_extruder_start_pos_y": { "default_value": 237 }, + "machine_nozzle_head_distance": { "default_value": 2.7 }, + "machine_nozzle_offset_x": { "default_value": 0 }, + "machine_nozzle_offset_y": { "default_value": 0 } + } +} \ No newline at end of file diff --git a/resources/extruders/ultimaker_s6_extruder_right.def.json b/resources/extruders/ultimaker_s6_extruder_right.def.json new file mode 100644 index 0000000000..5c70f36741 --- /dev/null +++ b/resources/extruders/ultimaker_s6_extruder_right.def.json @@ -0,0 +1,31 @@ +{ + "version": 2, + "name": "Extruder 2", + "inherits": "fdmextruder", + "metadata": + { + "machine": "ultimaker_s6", + "position": "1" + }, + "overrides": + { + "extruder_nr": + { + "default_value": 1, + "maximum_value": "1" + }, + "extruder_prime_pos_x": { "default_value": 333 }, + "extruder_prime_pos_y": { "default_value": 6 }, + "extruder_prime_pos_z": { "default_value": 2 }, + "machine_extruder_end_pos_abs": { "default_value": true }, + "machine_extruder_end_pos_x": { "default_value": 330 }, + "machine_extruder_end_pos_y": { "default_value": 219 }, + "machine_extruder_start_code": { "value": "\"M214 D0 K{material_pressure_advance_factor} R0.04\"" }, + "machine_extruder_start_pos_abs": { "default_value": true }, + "machine_extruder_start_pos_x": { "default_value": 330 }, + "machine_extruder_start_pos_y": { "default_value": 219 }, + "machine_nozzle_head_distance": { "default_value": 4.2 }, + "machine_nozzle_offset_x": { "default_value": 22 }, + "machine_nozzle_offset_y": { "default_value": 0 } + } +} \ No newline at end of file diff --git a/resources/images/UltimakerS6backplate.png b/resources/images/UltimakerS6backplate.png new file mode 100644 index 0000000000000000000000000000000000000000..d6e83781ccd922ffe02d4a838c961cfbba98c9e5 GIT binary patch literal 24368 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 80 else 'triangles' +infill_wipe_dist = 0 +layer_height = 0.2 +machine_min_cool_heat_time_window = 15 +machine_nozzle_heat_up_speed = 1.5 +machine_nozzle_id = BB 0.8 +machine_nozzle_size = 0.8 +machine_nozzle_tip_outer_diameter = 2.0 +multiple_mesh_overlap = 0 +prime_tower_enable = False +prime_tower_wipe_enabled = True +raft_surface_layers = 1 +retraction_amount = 4.5 +retraction_hop = 2 +retraction_hop_only_when_collides = True +support_angle = 60 +support_z_distance = 0 +switch_extruder_prime_speed = 15 +switch_extruder_retraction_amount = 12 +top_bottom_thickness = 1 +wall_0_inset = 0 + diff --git a/resources/variants/ultimaker_s6_bb04.inst.cfg b/resources/variants/ultimaker_s6_bb04.inst.cfg new file mode 100644 index 0000000000..756d6fd1d4 --- /dev/null +++ b/resources/variants/ultimaker_s6_bb04.inst.cfg @@ -0,0 +1,19 @@ +[general] +definition = ultimaker_s6 +name = BB 0.4 +version = 4 + +[metadata] +hardware_type = nozzle +setting_version = 25 +type = variant + +[values] +machine_nozzle_heat_up_speed = 1.5 +machine_nozzle_id = BB 0.4 +machine_nozzle_tip_outer_diameter = 1.0 +retraction_amount = 4.5 +support_bottom_height = =layer_height * 2 +support_interface_enable = True +switch_extruder_retraction_amount = 12 + diff --git a/resources/variants/ultimaker_s6_cc_plus04.inst.cfg b/resources/variants/ultimaker_s6_cc_plus04.inst.cfg new file mode 100644 index 0000000000..61206eb39c --- /dev/null +++ b/resources/variants/ultimaker_s6_cc_plus04.inst.cfg @@ -0,0 +1,17 @@ +[general] +definition = ultimaker_s6 +name = CC+ 0.4 +version = 4 + +[metadata] +hardware_type = nozzle +setting_version = 25 +type = variant + +[values] +machine_nozzle_cool_down_speed = 0.9 +machine_nozzle_id = CC+ 0.4 +machine_nozzle_size = 0.4 +machine_nozzle_tip_outer_diameter = 1.2 +retraction_prime_speed = =retraction_speed + diff --git a/resources/variants/ultimaker_s6_dd04.inst.cfg b/resources/variants/ultimaker_s6_dd04.inst.cfg new file mode 100644 index 0000000000..3125db405e --- /dev/null +++ b/resources/variants/ultimaker_s6_dd04.inst.cfg @@ -0,0 +1,17 @@ +[general] +definition = ultimaker_s6 +name = DD 0.4 +version = 4 + +[metadata] +hardware_type = nozzle +setting_version = 25 +type = variant + +[values] +machine_nozzle_cool_down_speed = 0.9 +machine_nozzle_id = DD 0.4 +machine_nozzle_size = 0.4 +machine_nozzle_tip_outer_diameter = 1.2 +retraction_prime_speed = =retraction_speed + diff --git a/resources/variants/ultimaker_s8_aa_plus06.inst.cfg b/resources/variants/ultimaker_s8_aa_plus06.inst.cfg new file mode 100644 index 0000000000..1eabef191c --- /dev/null +++ b/resources/variants/ultimaker_s8_aa_plus06.inst.cfg @@ -0,0 +1,17 @@ +[general] +definition = ultimaker_s8 +name = AA+ 0.6 +version = 4 + +[metadata] +hardware_type = nozzle +setting_version = 25 +type = variant + +[values] +machine_nozzle_cool_down_speed = 0.9 +machine_nozzle_id = AA+ 0.6 +machine_nozzle_size = 0.6 +machine_nozzle_tip_outer_diameter = 1.2 +retraction_prime_speed = =retraction_speed + diff --git a/resources/variants/ultimaker_s8_cc_plus06.inst.cfg b/resources/variants/ultimaker_s8_cc_plus06.inst.cfg new file mode 100644 index 0000000000..2a1c43873f --- /dev/null +++ b/resources/variants/ultimaker_s8_cc_plus06.inst.cfg @@ -0,0 +1,17 @@ +[general] +definition = ultimaker_s8 +name = CC+ 0.6 +version = 4 + +[metadata] +hardware_type = nozzle +setting_version = 25 +type = variant + +[values] +machine_nozzle_cool_down_speed = 0.9 +machine_nozzle_id = CC+ 0.6 +machine_nozzle_size = 0.6 +machine_nozzle_tip_outer_diameter = 1.2 +retraction_prime_speed = =retraction_speed + From 90848b90e4466f58faa44388320af5e3acc048a4 Mon Sep 17 00:00:00 2001 From: Paul Kuiper <46715907+pkuiper-ultimaker@users.noreply.github.com> Date: Fri, 4 Apr 2025 12:11:37 +0200 Subject: [PATCH 13/31] Improve PVA support for S8 PP-602 --- .../ultimaker_s8/um_s8_bb0.4_pva_0.15mm.inst.cfg | 10 ++++++++-- .../ultimaker_s8/um_s8_bb0.4_pva_0.1mm.inst.cfg | 10 ++++++++-- .../ultimaker_s8/um_s8_bb0.4_pva_0.2mm.inst.cfg | 10 ++++++++-- .../ultimaker_s8/um_s8_bb0.4_pva_0.3mm.inst.cfg | 10 ++++++++-- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.15mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.15mm.inst.cfg index 9539bd42b1..b2e787724e 100644 --- a/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.15mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.15mm.inst.cfg @@ -13,17 +13,23 @@ weight = -1 [values] acceleration_prime_tower = 1500 +acceleration_support = 1500 brim_replaces_support = False build_volume_temperature = =70 if extruders_enabled_count > 1 else 35 cool_fan_enabled = =not (support_enable and (extruder_nr == support_infill_extruder_nr)) default_material_bed_temperature = =0 if extruders_enabled_count > 1 else 60 initial_layer_line_width_factor = 150 +jerk_prime_tower = 4000 +jerk_support = 4000 minimum_support_area = 4 +retraction_amount = 6.5 retraction_count_max = 5 skirt_brim_minimal_length = =min(2000, 175 / (layer_height * line_width)) -speed_prime_tower = 25 +speed_prime_tower = 50 speed_support = 50 -support_angle = 45 +speed_support_bottom = =2*speed_support_interface/5 +speed_support_interface = 50 +support_bottom_density = 70 support_infill_sparse_thickness = =2 * layer_height support_interface_enable = True support_z_distance = 0 diff --git a/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.1mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.1mm.inst.cfg index d5e6084c76..a6d3010f60 100644 --- a/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.1mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.1mm.inst.cfg @@ -13,18 +13,24 @@ weight = 0 [values] acceleration_prime_tower = 1500 +acceleration_support = 1500 brim_replaces_support = False build_volume_temperature = =70 if extruders_enabled_count > 1 else 35 cool_fan_enabled = =not (support_enable and (extruder_nr == support_infill_extruder_nr)) default_material_bed_temperature = =0 if extruders_enabled_count > 1 else 60 initial_layer_line_width_factor = 150 +jerk_prime_tower = 4000 +jerk_support = 4000 material_print_temperature = =default_material_print_temperature - 5 minimum_support_area = 4 +retraction_amount = 6.5 retraction_count_max = 5 skirt_brim_minimal_length = =min(2000, 175 / (layer_height * line_width)) -speed_prime_tower = 25 +speed_prime_tower = 50 speed_support = 50 -support_angle = 45 +speed_support_bottom = =2*speed_support_interface/5 +speed_support_interface = 50 +support_bottom_density = 70 support_infill_sparse_thickness = =2 * layer_height support_interface_enable = True support_z_distance = 0 diff --git a/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.2mm.inst.cfg index 3c29ca8186..9994a7c503 100644 --- a/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.2mm.inst.cfg @@ -13,18 +13,24 @@ weight = -2 [values] acceleration_prime_tower = 1500 +acceleration_support = 1500 brim_replaces_support = False build_volume_temperature = =70 if extruders_enabled_count > 1 else 35 cool_fan_enabled = =not (support_enable and (extruder_nr == support_infill_extruder_nr)) default_material_bed_temperature = =0 if extruders_enabled_count > 1 else 60 initial_layer_line_width_factor = 150 +jerk_prime_tower = 4000 +jerk_support = 4000 material_print_temperature = =default_material_print_temperature + 5 minimum_support_area = 4 +retraction_amount = 6.5 retraction_count_max = 5 skirt_brim_minimal_length = =min(2000, 175 / (layer_height * line_width)) -speed_prime_tower = 25 +speed_prime_tower = 50 speed_support = 50 -support_angle = 45 +speed_support_bottom = =2*speed_support_interface/5 +speed_support_interface = 50 +support_bottom_density = 70 support_interface_enable = True support_z_distance = 0 diff --git a/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.3mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.3mm.inst.cfg index a49cea6817..4c9ca9c2cc 100644 --- a/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.3mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_bb0.4_pva_0.3mm.inst.cfg @@ -13,18 +13,24 @@ weight = -3 [values] acceleration_prime_tower = 1500 +acceleration_support = 1500 brim_replaces_support = False build_volume_temperature = =70 if extruders_enabled_count > 1 else 35 cool_fan_enabled = =not (support_enable and (extruder_nr == support_infill_extruder_nr)) default_material_bed_temperature = =0 if extruders_enabled_count > 1 else 60 initial_layer_line_width_factor = 150 +jerk_prime_tower = 4000 +jerk_support = 4000 material_print_temperature = =default_material_print_temperature - 5 minimum_support_area = 4 +retraction_amount = 6.5 retraction_count_max = 5 skirt_brim_minimal_length = =min(2000, 175 / (layer_height * line_width)) -speed_prime_tower = 25 +speed_prime_tower = 50 speed_support = 50 -support_angle = 45 +speed_support_bottom = =2*speed_support_interface/5 +speed_support_interface = 50 +support_bottom_density = 70 support_infill_sparse_thickness = 0.3 support_interface_enable = True support_z_distance = 0 From e76e8432743ff8fa4df21f71a4853b181915f791 Mon Sep 17 00:00:00 2001 From: Paul Kuiper <46715907+pkuiper-ultimaker@users.noreply.github.com> Date: Tue, 8 Apr 2025 14:32:15 +0200 Subject: [PATCH 14/31] PC: Prevent holes in outside wall of PC prints due to air capture during unretracts: - reduce retract length, inner wall before outer wall, inner wall speed equal to outer wall. We also lowered the flow rate to 95% for PC because we noticed that it was over extruding. CPE: CPE is VERY fragile to printer over itself or bumps. Lower all speeds to 40mm/s, goto 'lines' infill type to prevent crossing over lines, 3 walls to prevent issues with infill to reach the outer wall. PP-607 --- .../ultimaker_s8/um_s8_aa_plus_0.4_cpe_0.2mm.inst.cfg | 8 +++++--- .../ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm.inst.cfg | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_cpe_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_cpe_0.2mm.inst.cfg index 25a277a06c..bff86d6fa4 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_cpe_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_cpe_0.2mm.inst.cfg @@ -13,8 +13,10 @@ weight = -2 [values] infill_overlap = 20 -infill_pattern = ='zigzag' if infill_sparse_density > 80 else 'gyroid' -speed_print = 100 -speed_wall_0 = =speed_print +infill_pattern = lines +speed_print = 40 +speed_wall = =speed_print +speed_wall_0 = =speed_wall support_interface_enable = True +wall_thickness = =wall_line_width_0 + 2*wall_line_width_x diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm.inst.cfg index f6c91375f9..ae64e07f03 100644 --- a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.4_pc_0.2mm.inst.cfg @@ -14,6 +14,8 @@ weight = -2 [values] cool_min_layer_time = 6 cool_min_layer_time_fan_speed_max = 12 -retraction_amount = 8 +inset_direction = inside_out +material_flow = 95 retraction_prime_speed = 15 +speed_wall_x = =speed_wall_0 From 93d9c084d757a137eb364f84612f8cbe711ed591 Mon Sep 17 00:00:00 2001 From: Paul Kuiper <46715907+pkuiper-ultimaker@users.noreply.github.com> Date: Tue, 8 Apr 2025 15:44:20 +0200 Subject: [PATCH 15/31] Add new configuration files for AA+ and CC+ 0.6 nozzles PP-602 --- .../variants/ultimaker_s6_aa_plus06.inst.cfg | 17 +++++++++++++++++ .../variants/ultimaker_s6_cc_plus06.inst.cfg | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 resources/variants/ultimaker_s6_aa_plus06.inst.cfg create mode 100644 resources/variants/ultimaker_s6_cc_plus06.inst.cfg diff --git a/resources/variants/ultimaker_s6_aa_plus06.inst.cfg b/resources/variants/ultimaker_s6_aa_plus06.inst.cfg new file mode 100644 index 0000000000..95401be2c3 --- /dev/null +++ b/resources/variants/ultimaker_s6_aa_plus06.inst.cfg @@ -0,0 +1,17 @@ +[general] +definition = ultimaker_s6 +name = AA+ 0.6 +version = 4 + +[metadata] +hardware_type = nozzle +setting_version = 25 +type = variant + +[values] +machine_nozzle_cool_down_speed = 0.9 +machine_nozzle_id = AA+ 0.6 +machine_nozzle_size = 0.6 +machine_nozzle_tip_outer_diameter = 1.2 +retraction_prime_speed = =retraction_speed + diff --git a/resources/variants/ultimaker_s6_cc_plus06.inst.cfg b/resources/variants/ultimaker_s6_cc_plus06.inst.cfg new file mode 100644 index 0000000000..93564bada0 --- /dev/null +++ b/resources/variants/ultimaker_s6_cc_plus06.inst.cfg @@ -0,0 +1,17 @@ +[general] +definition = ultimaker_s6 +name = CC+ 0.6 +version = 4 + +[metadata] +hardware_type = nozzle +setting_version = 25 +type = variant + +[values] +machine_nozzle_cool_down_speed = 0.9 +machine_nozzle_id = CC+ 0.6 +machine_nozzle_size = 0.6 +machine_nozzle_tip_outer_diameter = 1.2 +retraction_prime_speed = =retraction_speed + From 1b80649e213d6d15d67c294eb95f2cbc70cc2b6e Mon Sep 17 00:00:00 2001 From: Paul Kuiper <46715907+pkuiper-ultimaker@users.noreply.github.com> Date: Wed, 9 Apr 2025 11:32:19 +0200 Subject: [PATCH 16/31] Corrected review comment of Erwan. PP-602 --- resources/definitions/ultimaker_s6.def.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/definitions/ultimaker_s6.def.json b/resources/definitions/ultimaker_s6.def.json index ec6c189eda..bc0e6a0f4e 100644 --- a/resources/definitions/ultimaker_s6.def.json +++ b/resources/definitions/ultimaker_s6.def.json @@ -8,7 +8,7 @@ "author": "UltiMaker", "manufacturer": "Ultimaker B.V.", "file_formats": "application/x-ufp;text/x-gcode", - "platform": "ultimaker_s7_platform.obj", + "platform": "ultimaker_s5_platform.obj", "bom_numbers": [ 10700 ], @@ -30,8 +30,8 @@ "nozzle_offsetting_for_disallowed_areas": false, "platform_offset": [ 0, - 0, - 0 + -30, + -10 ], "platform_texture": "UltimakerS6backplate.png", "preferred_material": "ultimaker_pla_blue", @@ -48,6 +48,6 @@ "overrides": { "adhesion_type": { "value": "'brim'" }, - "machine_name": { "default_value": "Ultimaker S6" } + "machine_name": { "default_value": "UltiMaker S6" } } } \ No newline at end of file From a39033bc1fec46896ad1ec566227317e6373c4bc Mon Sep 17 00:00:00 2001 From: Paul Kuiper <46715907+pkuiper-ultimaker@users.noreply.github.com> Date: Wed, 9 Apr 2025 14:05:37 +0200 Subject: [PATCH 17/31] Capital M in the Ultimaker name of the S7 and S8 to be consistent with machines names released after the merger. PP-602 --- resources/definitions/ultimaker_s7.def.json | 2 +- resources/definitions/ultimaker_s8.def.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/ultimaker_s7.def.json b/resources/definitions/ultimaker_s7.def.json index 14d9b21168..41120c23df 100644 --- a/resources/definitions/ultimaker_s7.def.json +++ b/resources/definitions/ultimaker_s7.def.json @@ -47,7 +47,7 @@ "overrides": { "default_material_print_temperature": { "maximum_value_warning": "320" }, - "machine_name": { "default_value": "Ultimaker S7" }, + "machine_name": { "default_value": "UltiMaker S7" }, "material_print_temperature_layer_0": { "maximum_value_warning": "320" } } } \ No newline at end of file diff --git a/resources/definitions/ultimaker_s8.def.json b/resources/definitions/ultimaker_s8.def.json index 5a4d44fa2f..8fa2ab8459 100644 --- a/resources/definitions/ultimaker_s8.def.json +++ b/resources/definitions/ultimaker_s8.def.json @@ -385,7 +385,7 @@ "unit": "m/s\u00b3", "value": "20000 if machine_gcode_flavor == 'Cheetah' else 100" }, - "machine_name": { "default_value": "Ultimaker S8" }, + "machine_name": { "default_value": "UltiMaker S8" }, "machine_nozzle_cool_down_speed": { "default_value": 1.3 }, "machine_nozzle_heat_up_speed": { "default_value": 0.6 }, "machine_start_gcode": { "default_value": "M213 U0.1 ;undercut 0.1mm" }, From 3990fd5d2af362a1254d7b7e0300104445702cb4 Mon Sep 17 00:00:00 2001 From: Paul Kuiper <46715907+pkuiper-ultimaker@users.noreply.github.com> Date: Wed, 9 Apr 2025 14:10:38 +0200 Subject: [PATCH 18/31] Remove the BB0.8 for the S6 machine (for now). PP-602 --- .../variants/ultimaker_s6_bb0.8.inst.cfg | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 resources/variants/ultimaker_s6_bb0.8.inst.cfg diff --git a/resources/variants/ultimaker_s6_bb0.8.inst.cfg b/resources/variants/ultimaker_s6_bb0.8.inst.cfg deleted file mode 100644 index e4a97f7e9d..0000000000 --- a/resources/variants/ultimaker_s6_bb0.8.inst.cfg +++ /dev/null @@ -1,35 +0,0 @@ -[general] -definition = ultimaker_s6 -name = BB 0.8 -version = 4 - -[metadata] -hardware_type = nozzle -setting_version = 25 -type = variant - -[values] -brim_width = 3 -cool_fan_speed = 50 -infill_pattern = ='zigzag' if infill_sparse_density > 80 else 'triangles' -infill_wipe_dist = 0 -layer_height = 0.2 -machine_min_cool_heat_time_window = 15 -machine_nozzle_heat_up_speed = 1.5 -machine_nozzle_id = BB 0.8 -machine_nozzle_size = 0.8 -machine_nozzle_tip_outer_diameter = 2.0 -multiple_mesh_overlap = 0 -prime_tower_enable = False -prime_tower_wipe_enabled = True -raft_surface_layers = 1 -retraction_amount = 4.5 -retraction_hop = 2 -retraction_hop_only_when_collides = True -support_angle = 60 -support_z_distance = 0 -switch_extruder_prime_speed = 15 -switch_extruder_retraction_amount = 12 -top_bottom_thickness = 1 -wall_0_inset = 0 - From 14bf34d96a3fbccdc69c4c14fcef4f9486507e5d Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Tue, 15 Apr 2025 21:08:33 +0200 Subject: [PATCH 19/31] Adjust code to review comments. - Use delete-on-close instead. - Prevent infinite loops. part of CURA-12156 --- cura/Backups/Backup.py | 2 +- plugins/CuraDrive/src/CreateBackupJob.py | 6 +++--- plugins/CuraDrive/src/RestoreBackupJob.py | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index a409c4c689..1438ce293a 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -114,7 +114,7 @@ class Backup: data["installed"] = keep_in data["to_install"].update(reinstall) if data is not None: - tmpfile = tempfile.NamedTemporaryFile(delete=False) + tmpfile = tempfile.NamedTemporaryFile(delete_on_close=False) with open(tmpfile.name, "w") as outfile: json.dump(data, outfile) add_to_archive(tmpfile.name, file_path) diff --git a/plugins/CuraDrive/src/CreateBackupJob.py b/plugins/CuraDrive/src/CreateBackupJob.py index 67083d2e83..4820f886ab 100644 --- a/plugins/CuraDrive/src/CreateBackupJob.py +++ b/plugins/CuraDrive/src/CreateBackupJob.py @@ -99,10 +99,10 @@ class CreateBackupJob(Job): self._requestUploadSlot(backup_meta_data, len(self._backup_zip)) # Note: One 'process events' call wasn't enough with the changed situation somehow. - active_done_check = False - while not active_done_check: + for _ in range(5000): CuraApplication.getInstance().processEvents() - active_done_check = self._job_done.wait(0.02) + if self._job_done.wait(0.02): + break if self.backup_upload_error_message == "": self._upload_message.setText(catalog.i18nc("@info:backup_status", "Your backup has finished uploading.")) diff --git a/plugins/CuraDrive/src/RestoreBackupJob.py b/plugins/CuraDrive/src/RestoreBackupJob.py index 817d819abf..503b39547a 100644 --- a/plugins/CuraDrive/src/RestoreBackupJob.py +++ b/plugins/CuraDrive/src/RestoreBackupJob.py @@ -56,10 +56,10 @@ class RestoreBackupJob(Job): ) # Note: Just to be sure, use the same structure here as in CreateBackupJob. - active_done_check = False - while not active_done_check: + for _ in range(5000): CuraApplication.getInstance().processEvents() - active_done_check = self._job_done.wait(0.02) + if self._job_done.wait(0.02): + break def _onRestoreRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None: if not HttpRequestManager.replyIndicatesSuccess(reply, error): @@ -71,7 +71,7 @@ class RestoreBackupJob(Job): # We store the file in a temporary path fist to ensure integrity. try: - self._temporary_backup_file = NamedTemporaryFile(delete = False) + self._temporary_backup_file = NamedTemporaryFile(delete_on_close = False) with open(self._temporary_backup_file.name, "wb") as write_backup: app = CuraApplication.getInstance() bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE) From f45cbeb5f4a8e250a9e9d5d4532cd636de0ec37d Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 16 Apr 2025 13:18:40 +0200 Subject: [PATCH 20/31] Remove spurious (and maybe erroneous?) empty line. CURA-12502 --- packaging/NSIS/Ultimaker-Cura.nsi.jinja | 1 - 1 file changed, 1 deletion(-) diff --git a/packaging/NSIS/Ultimaker-Cura.nsi.jinja b/packaging/NSIS/Ultimaker-Cura.nsi.jinja index 8c5d48f9dd..53d8777e5f 100644 --- a/packaging/NSIS/Ultimaker-Cura.nsi.jinja +++ b/packaging/NSIS/Ultimaker-Cura.nsi.jinja @@ -105,7 +105,6 @@ WriteUninstaller "$INSTDIR\uninstall.exe" !ifdef REG_START_MENU !insertmacro MUI_STARTMENU_WRITE_BEGIN Application CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" - !insertmacro MUI_STARTMENU_WRITE_END !endif From 14375b171d100e2aa54aa669b658aac148c18f39 Mon Sep 17 00:00:00 2001 From: wawanbreton <601114+wawanbreton@users.noreply.github.com> Date: Thu, 17 Apr 2025 11:27:50 +0000 Subject: [PATCH 21/31] Apply printer-linter format --- resources/definitions/bambulab_a1mini.def.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/resources/definitions/bambulab_a1mini.def.json b/resources/definitions/bambulab_a1mini.def.json index 9afb31e8ea..0a6c1632d8 100644 --- a/resources/definitions/bambulab_a1mini.def.json +++ b/resources/definitions/bambulab_a1mini.def.json @@ -149,8 +149,6 @@ "meshfix_maximum_resolution": { "value": 0.4 }, "min_infill_area": { "default_value": 10 }, "optimize_wall_printing_order": { "value": false }, - "wall_overhang_angle": { "default_value": 10 }, - "wall_overhang_speed_factors": { "default_value": "[25,15,5,5]" }, "prime_tower_enable": { "default_value": true }, "prime_tower_min_volume": { "default_value": 250 }, "prime_tower_position_x": { "value": "resolveOrValue('prime_tower_size') + (resolveOrValue('prime_tower_base_size') if (resolveOrValue('adhesion_type') == 'raft' or resolveOrValue('prime_tower_brim_enable')) else 0) + max(max(extruderValues('travel_avoid_distance')) + max(extruderValues('machine_nozzle_offset_y')) + max(extruderValues('support_offset')) + (extruderValue(skirt_brim_extruder_nr, 'skirt_brim_line_width') * extruderValue(skirt_brim_extruder_nr, 'skirt_line_count') * extruderValue(skirt_brim_extruder_nr, 'initial_layer_line_width_factor') / 100 + extruderValue(skirt_brim_extruder_nr, 'skirt_gap') if resolveOrValue('adhesion_type') == 'skirt' else 0) + (resolveOrValue('draft_shield_dist') if resolveOrValue('draft_shield_enabled') else 0), max(map(abs, extruderValues('machine_nozzle_offset_y'))), 1) - (resolveOrValue('machine_depth') / 2 if resolveOrValue('machine_center_is_zero') else 0)" }, @@ -294,6 +292,7 @@ "wall_material_flow": { "value": 95 }, "wall_overhang_angle": { "value": 45 }, "wall_overhang_speed_factor": { "value": 50 }, + "wall_overhang_speed_factors": { "default_value": "[25,15,5,5]" }, "wall_x_material_flow": { "value": 100 }, "z_seam_corner": { "value": "'z_seam_corner_weighted'" }, "z_seam_position": { "value": "'backright'" }, From 2db896e80fcb7dcef778295e1ba778eaba77a963 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Tue, 22 Apr 2025 10:24:06 +0200 Subject: [PATCH 22/31] win/pacakging -- Start menu-macro is only needed for when in folder? Also it fails now since it seems like this macro _expects_ a folder to be set. CURA-12502 --- packaging/NSIS/Ultimaker-Cura.nsi.jinja | 3 --- 1 file changed, 3 deletions(-) diff --git a/packaging/NSIS/Ultimaker-Cura.nsi.jinja b/packaging/NSIS/Ultimaker-Cura.nsi.jinja index 53d8777e5f..9f61e6950c 100644 --- a/packaging/NSIS/Ultimaker-Cura.nsi.jinja +++ b/packaging/NSIS/Ultimaker-Cura.nsi.jinja @@ -61,7 +61,6 @@ InstallDir "$PROGRAMFILES64\${APP_NAME}" !ifdef REG_START_MENU !define MUI_STARTMENUPAGE_NODISABLE -!define MUI_STARTMENUPAGE_DEFAULTFOLDER "UltiMaker Cura" !define MUI_STARTMENUPAGE_REGISTRY_ROOT "${REG_ROOT}" !define MUI_STARTMENUPAGE_REGISTRY_KEY "${UNINSTALL_PATH}" !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "${REG_START_MENU}" @@ -103,9 +102,7 @@ SetOutPath "$INSTDIR" WriteUninstaller "$INSTDIR\uninstall.exe" !ifdef REG_START_MENU -!insertmacro MUI_STARTMENU_WRITE_BEGIN Application CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" -!insertmacro MUI_STARTMENU_WRITE_END !endif !ifndef REG_START_MENU From 7731ee4b241ece88d726c5a58c25ccc2968cfce7 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 22 Apr 2025 16:01:32 +0200 Subject: [PATCH 23/31] Basically working import from BambuStudio/OrcaSlicer 3MF CURA-12099 --- plugins/3MFReader/ThreeMFReader.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index 9d4ace1698..45ab2e7d2f 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -94,7 +94,7 @@ class ThreeMFReader(MeshReader): return temp_mat @staticmethod - def _convertSavitarNodeToUMNode(savitar_node: Savitar.SceneNode, file_name: str = "") -> Optional[SceneNode]: + def _convertSavitarNodeToUMNode(savitar_node: Savitar.SceneNode, file_name: str = "", archive: zipfile.ZipFile = None) -> Optional[SceneNode]: """Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node. :returns: Scene node. @@ -115,6 +115,10 @@ class ThreeMFReader(MeshReader): active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate + component_path = savitar_node.getComponentPath() + if component_path != "" and archive is not None: + savitar_node.parseComponentData(archive.open(component_path.lstrip("/")).read()) + um_node = CuraSceneNode() # This adds a SettingOverrideDecorator um_node.addDecorator(BuildPlateDecorator(active_build_plate)) try: @@ -143,7 +147,7 @@ class ThreeMFReader(MeshReader): um_node.setMeshData(mesh_data) for child in savitar_node.getChildren(): - child_node = ThreeMFReader._convertSavitarNodeToUMNode(child) + child_node = ThreeMFReader._convertSavitarNodeToUMNode(child, archive=archive) if child_node: um_node.addChild(child_node) @@ -232,7 +236,7 @@ class ThreeMFReader(MeshReader): CuraApplication.getInstance().getController().getScene().setMetaDataEntry(key, value) for node in scene_3mf.getSceneNodes(): - um_node = ThreeMFReader._convertSavitarNodeToUMNode(node, file_name) + um_node = ThreeMFReader._convertSavitarNodeToUMNode(node, file_name, archive) if um_node is None: continue From de8bf1a5dfe1767f8648ea1403e34c32aada5ad0 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 23 Apr 2025 08:49:36 +0200 Subject: [PATCH 24/31] Fix 3MF export imports. done as part of CURA-12099 --- plugins/3MFReader/__init__.py | 2 +- plugins/3MFWriter/ThreeMFWriter.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/3MFReader/__init__.py b/plugins/3MFReader/__init__.py index 5e2b68fce0..d468783a90 100644 --- a/plugins/3MFReader/__init__.py +++ b/plugins/3MFReader/__init__.py @@ -23,7 +23,7 @@ def getMetaData() -> Dict: if "3MFReader.ThreeMFReader" in sys.modules: metaData["mesh_reader"] = [ { - "extension": "3mf", + "extension": workspace_extension, "description": catalog.i18nc("@item:inlistbox", "3MF File") } ] diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index be0fa1a01b..61aec6b664 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -10,8 +10,6 @@ import threading from typing import Optional, cast, List, Dict, Pattern, Set -from Machines.Models.ExtrudersModel import ExtrudersModel -from Settings.ExtruderStack import ExtruderStack from UM.PluginRegistry import PluginRegistry from UM.Mesh.MeshWriter import MeshWriter from UM.Math.Vector import Vector @@ -26,7 +24,9 @@ from UM.Settings.ContainerRegistry import ContainerRegistry from cura.CuraApplication import CuraApplication from cura.CuraPackageManager import CuraPackageManager +from cura.Machines.Models.ExtrudersModel import ExtrudersModel from cura.Settings import CuraContainerStack +from cura.Settings.ExtruderStack import ExtruderStack from cura.Utils.Threading import call_on_qt_thread from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Snapshot import Snapshot From 9b10467f3e993bd55a53ee937561702572911c21 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 23 Apr 2025 16:22:32 +0200 Subject: [PATCH 25/31] Probably 'open as model' should not apply if the 3mf isn't a project file. This would cause the 3MF projects from other vendors, which isn't a 'project' to us in the sense that it isn't a Cura project, to have their models 'arranged', unless the file was opened via 'recent files', in which case the open mode was set to always ask instead. done as part of CURA-12099 --- cura/CuraApplication.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 6e4da621ff..87657af5f5 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -188,6 +188,7 @@ class CuraApplication(QtApplication): self._single_instance = None self._open_project_mode: Optional[str] = None + self._read_operation_is_project_file: Optional[bool] = None self._cura_formula_functions = None # type: Optional[CuraFormulaFunctions] @@ -2015,18 +2016,18 @@ class CuraApplication(QtApplication): self.deleteAll() break - is_project_file = self.checkIsValidProjectFile(file) + self._read_operation_is_project_file = self.checkIsValidProjectFile(file) if self._open_project_mode is None: self._open_project_mode = self.getPreferences().getValue("cura/choice_on_open_project") - if is_project_file and self._open_project_mode == "open_as_project": + if self._read_operation_is_project_file and self._open_project_mode == "open_as_project": # open as project immediately without presenting a dialog workspace_handler = self.getWorkspaceFileHandler() workspace_handler.readLocalFile(file, add_to_recent_files_hint = add_to_recent_files) return - if is_project_file and self._open_project_mode == "always_ask": + if self._read_operation_is_project_file and self._open_project_mode == "always_ask": # present a dialog asking to open as project or import models self.callLater(self.openProjectFile.emit, file, add_to_recent_files) return @@ -2164,7 +2165,7 @@ class CuraApplication(QtApplication): nodes_to_arrange.append(node) # If the file is a project,and models are to be loaded from a that project, # models inside file should be arranged in buildplate. - elif self._open_project_mode == "open_as_model": + elif self._read_operation_is_project_file and self._open_project_mode == "open_as_model": nodes_to_arrange.append(node) # This node is deep copied from some other node which already has a BuildPlateDecorator, but the deepcopy From 4ff70ac46f5cfbbffc98d8e207daa86524bb0563 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Thu, 24 Apr 2025 09:23:27 +0200 Subject: [PATCH 26/31] Fix exporting Bambu 3mf format to removable drive CURA-12099 --- .../RemovableDriveOutputDevice/RemovableDriveOutputDevice.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py index a9a0666d9c..2ddf8aa334 100644 --- a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py +++ b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py @@ -101,7 +101,8 @@ class RemovableDriveOutputDevice(OutputDevice): self._stream = open(file_name, "wt", buffering = 1, encoding = "utf-8") else: #Binary mode. self._stream = open(file_name, "wb", buffering = 1) - job = WriteFileJob(writer, self._stream, nodes, preferred_format["mode"]) + writer_args = {"mime_type": preferred_format["mime_type"]} + job = WriteFileJob(writer, self._stream, nodes, preferred_format["mode"], writer_args) job.setFileName(file_name) job.progress.connect(self._onProgress) job.finished.connect(self._onFinished) From 17ab7a4890f976d244af205f9ee1e5da07fe6be1 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Thu, 24 Apr 2025 09:55:23 +0200 Subject: [PATCH 27/31] Bump pySavitar version CURA-12099 --- conandata.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conandata.yml b/conandata.yml index 20392c6e44..3181d601e5 100644 --- a/conandata.yml +++ b/conandata.yml @@ -6,7 +6,7 @@ requirements: - "cura_binary_data/5.11.0-alpha.0@ultimaker/testing" - "fdm_materials/5.11.0-alpha.0@ultimaker/testing" - "dulcificum/5.10.0" - - "pysavitar/5.10.0" + - "pysavitar/5.11.0-alpha.0" - "pynest2d/5.10.0" requirements_internal: - "fdm_materials/5.11.0-alpha.0@ultimaker/testing" From 254087cb45d9eb8ec6c380c45b06c272ece23981 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Thu, 24 Apr 2025 12:57:58 +0200 Subject: [PATCH 28/31] Split-out bambu-specific elements to their own 3MF 'variant'. part of CURA-12099 --- plugins/3MFWriter/BambuLabVariant.py | 168 +++++++++++++++++++++++++++ plugins/3MFWriter/DefaultVariant.py | 33 ++++++ plugins/3MFWriter/ThreeMFVariant.py | 74 ++++++++++++ plugins/3MFWriter/ThreeMFWriter.py | 167 ++++++-------------------- 4 files changed, 308 insertions(+), 134 deletions(-) create mode 100644 plugins/3MFWriter/BambuLabVariant.py create mode 100644 plugins/3MFWriter/DefaultVariant.py create mode 100644 plugins/3MFWriter/ThreeMFVariant.py diff --git a/plugins/3MFWriter/BambuLabVariant.py b/plugins/3MFWriter/BambuLabVariant.py new file mode 100644 index 0000000000..e30cc110b7 --- /dev/null +++ b/plugins/3MFWriter/BambuLabVariant.py @@ -0,0 +1,168 @@ +# Copyright (c) 2025 UltiMaker +# Cura is released under the terms of the LGPLv3 or higher. + +import hashlib +import json +from io import StringIO +import xml.etree.ElementTree as ET +import zipfile + +from PyQt6.QtCore import Qt, QBuffer +from PyQt6.QtGui import QImage + +from UM.Application import Application +from UM.Logger import Logger +from UM.Mesh.MeshWriter import MeshWriter +from UM.PluginRegistry import PluginRegistry +from typing import cast + +from cura.CuraApplication import CuraApplication + +from .ThreeMFVariant import ThreeMFVariant +from UM.i18n import i18nCatalog +catalog = i18nCatalog("cura") + +# Path constants +METADATA_PATH = "Metadata" +THUMBNAIL_PATH_MULTIPLATE = f"{METADATA_PATH}/plate_1.png" +THUMBNAIL_PATH_MULTIPLATE_SMALL = f"{METADATA_PATH}/plate_1_small.png" +GCODE_PATH = f"{METADATA_PATH}/plate_1.gcode" +GCODE_MD5_PATH = f"{GCODE_PATH}.md5" +MODEL_SETTINGS_PATH = f"{METADATA_PATH}/model_settings.config" +PLATE_DESC_PATH = f"{METADATA_PATH}/plate_1.json" +SLICE_INFO_PATH = f"{METADATA_PATH}/slice_info.config" + +class BambuLabVariant(ThreeMFVariant): + """BambuLab specific implementation of the 3MF format.""" + + @property + def mime_type(self) -> str: + return "application/vnd.bambulab-package.3dmanufacturing-3dmodel+xml" + + def process_thumbnail(self, snapshot: QImage, thumbnail_buffer: QBuffer, + archive: zipfile.ZipFile, relations_element: ET.Element) -> None: + """Process the thumbnail for BambuLab variant.""" + # Write thumbnail + archive.writestr(zipfile.ZipInfo(THUMBNAIL_PATH_MULTIPLATE), thumbnail_buffer.data()) + + # Add relations elements for thumbnails + ET.SubElement(relations_element, "Relationship", + Target="/" + THUMBNAIL_PATH_MULTIPLATE, Id="rel-2", + Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail") + + ET.SubElement(relations_element, "Relationship", + Target="/" + THUMBNAIL_PATH_MULTIPLATE, Id="rel-4", + Type="http://schemas.bambulab.com/package/2021/cover-thumbnail-middle") + + # Create and save small thumbnail + small_snapshot = snapshot.scaled(128, 128, transformMode=Qt.TransformationMode.SmoothTransformation) + small_thumbnail_buffer = QBuffer() + small_thumbnail_buffer.open(QBuffer.OpenModeFlag.ReadWrite) + small_snapshot.save(small_thumbnail_buffer, "PNG") + + # Write small thumbnail + archive.writestr(zipfile.ZipInfo(THUMBNAIL_PATH_MULTIPLATE_SMALL), small_thumbnail_buffer.data()) + + # Add relation for small thumbnail + ET.SubElement(relations_element, "Relationship", + Target="/" + THUMBNAIL_PATH_MULTIPLATE_SMALL, Id="rel-5", + Type="http://schemas.bambulab.com/package/2021/cover-thumbnail-small") + + def add_extra_files(self, archive: zipfile.ZipFile, metadata_relations_element: ET.Element) -> None: + """Add BambuLab specific files to the archive.""" + self._storeGCode(archive, metadata_relations_element) + self._storeModelSettings(archive) + self._storePlateDesc(archive) + self._storeSliceInfo(archive) + + def _storeGCode(self, archive: zipfile.ZipFile, metadata_relations_element: ET.Element): + """Store GCode data in the archive.""" + gcode_textio = StringIO() + gcode_writer = cast(MeshWriter, PluginRegistry.getInstance().getPluginObject("GCodeWriter")) + success = gcode_writer.write(gcode_textio, None) + + if not success: + error_msg = catalog.i18nc("@info:error", "Can't write GCode to 3MF file") + self._writer.setInformation(error_msg) + Logger.error(error_msg) + raise Exception(error_msg) + + gcode_data = gcode_textio.getvalue().encode("UTF-8") + archive.writestr(zipfile.ZipInfo(GCODE_PATH), gcode_data) + + gcode_relation_element = ET.SubElement(metadata_relations_element, "Relationship", + Target=f"/{GCODE_PATH}", Id="rel-1", + Type="http://schemas.bambulab.com/package/2021/gcode") + + # Calculate and store the MD5 sum of the gcode data + md5_hash = hashlib.md5(gcode_data).hexdigest() + archive.writestr(zipfile.ZipInfo(GCODE_MD5_PATH), md5_hash.encode("UTF-8")) + + def _storeModelSettings(self, archive: zipfile.ZipFile): + """Store model settings in the archive.""" + config = ET.Element("config") + plate = ET.SubElement(config, "plate") + ET.SubElement(plate, "metadata", key="plater_id", value="1") + ET.SubElement(plate, "metadata", key="plater_name", value="") + ET.SubElement(plate, "metadata", key="locked", value="false") + ET.SubElement(plate, "metadata", key="filament_map_mode", value="Auto For Flush") + extruders_count = len(CuraApplication.getInstance().getExtruderManager().extruderIds) + ET.SubElement(plate, "metadata", key="filament_maps", value=" ".join("1" for _ in range(extruders_count))) + ET.SubElement(plate, "metadata", key="gcode_file", value=GCODE_PATH) + ET.SubElement(plate, "metadata", key="thumbnail_file", value=THUMBNAIL_PATH_MULTIPLATE) + ET.SubElement(plate, "metadata", key="pattern_bbox_file", value=PLATE_DESC_PATH) + + self._writer._storeElementTree(archive, MODEL_SETTINGS_PATH, config) + + def _storePlateDesc(self, archive: zipfile.ZipFile): + """Store plate description in the archive.""" + plate_desc = {} + + filament_ids = [] + filament_colors = [] + + for extruder in CuraApplication.getInstance().getExtruderManager().getUsedExtruderStacks(): + filament_ids.append(extruder.getValue("extruder_nr")) + filament_colors.append(self._writer._getMaterialColor(extruder)) + + plate_desc["filament_ids"] = filament_ids + plate_desc["filament_colors"] = filament_colors + plate_desc["first_extruder"] = CuraApplication.getInstance().getExtruderManager().getInitialExtruderNr() + plate_desc["is_seq_print"] = Application.getInstance().getGlobalContainerStack().getValue("print_sequence") == "one_at_a_time" + plate_desc["nozzle_diameter"] = CuraApplication.getInstance().getExtruderManager().getActiveExtruderStack().getValue("machine_nozzle_size") + plate_desc["version"] = 2 + + file = zipfile.ZipInfo(PLATE_DESC_PATH) + file.compress_type = zipfile.ZIP_DEFLATED + archive.writestr(file, json.dumps(plate_desc).encode("UTF-8")) + + def _storeSliceInfo(self, archive: zipfile.ZipFile): + """Store slice information in the archive.""" + config = ET.Element("config") + + header = ET.SubElement(config, "header") + ET.SubElement(header, "header_item", key="X-BBL-Client-Type", value="slicer") + ET.SubElement(header, "header_item", key="X-BBL-Client-Version", value="02.00.01.50") + + plate = ET.SubElement(config, "plate") + ET.SubElement(plate, "metadata", key="index", value="1") + ET.SubElement(plate, + "metadata", + key="nozzle_diameters", + value=str(CuraApplication.getInstance().getExtruderManager().getActiveExtruderStack().getValue("machine_nozzle_size"))) + + print_information = CuraApplication.getInstance().getPrintInformation() + for index, extruder in enumerate(Application.getInstance().getGlobalContainerStack().extruderList): + used_m = print_information.materialLengths[index] + used_g = print_information.materialWeights[index] + if used_m > 0.0 and used_g > 0.0: + ET.SubElement(plate, + "filament", + id=str(extruder.getValue("extruder_nr") + 1), + tray_info_idx="GFA00", + type=extruder.material.getMetaDataEntry("material", ""), + color=self._writer._getMaterialColor(extruder), + used_m=str(used_m), + used_g=str(used_g)) + + self._writer._storeElementTree(archive, SLICE_INFO_PATH, config) \ No newline at end of file diff --git a/plugins/3MFWriter/DefaultVariant.py b/plugins/3MFWriter/DefaultVariant.py new file mode 100644 index 0000000000..52dd381ed1 --- /dev/null +++ b/plugins/3MFWriter/DefaultVariant.py @@ -0,0 +1,33 @@ +# Copyright (c) 2025 UltiMaker +# Cura is released under the terms of the LGPLv3 or higher. + +import xml.etree.ElementTree as ET +import zipfile + +from PyQt6.QtCore import QBuffer +from PyQt6.QtGui import QImage + +from .ThreeMFVariant import ThreeMFVariant + +# Standard 3MF paths +METADATA_PATH = "Metadata" +THUMBNAIL_PATH = f"{METADATA_PATH}/thumbnail.png" + +class DefaultVariant(ThreeMFVariant): + """Default implementation of the 3MF format.""" + + @property + def mime_type(self) -> str: + return "application/vnd.ms-package.3dmanufacturing-3dmodel+xml" + + def process_thumbnail(self, snapshot: QImage, thumbnail_buffer: QBuffer, + archive: zipfile.ZipFile, relations_element: ET.Element) -> None: + """Process the thumbnail for default 3MF variant.""" + thumbnail_file = zipfile.ZipInfo(THUMBNAIL_PATH) + # Don't try to compress snapshot file, because the PNG is pretty much as compact as it will get + archive.writestr(thumbnail_file, thumbnail_buffer.data()) + + # Add thumbnail relation to _rels/.rels file + ET.SubElement(relations_element, "Relationship", + Target="/" + THUMBNAIL_PATH, Id="rel1", + Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail") \ No newline at end of file diff --git a/plugins/3MFWriter/ThreeMFVariant.py b/plugins/3MFWriter/ThreeMFVariant.py new file mode 100644 index 0000000000..9b7f0eadf4 --- /dev/null +++ b/plugins/3MFWriter/ThreeMFVariant.py @@ -0,0 +1,74 @@ +# Copyright (c) 2025 UltiMaker +# Cura is released under the terms of the LGPLv3 or higher. + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING +import xml.etree.ElementTree as ET +import zipfile + +from PyQt6.QtGui import QImage +from PyQt6.QtCore import QBuffer + +if TYPE_CHECKING: + from .ThreeMFWriter import ThreeMFWriter + +class ThreeMFVariant(ABC): + """Base class for 3MF format variants. + + Different vendors may have their own extensions to the 3MF format, + such as BambuLab's 3MF variant. This class provides an interface + for implementing these variants. + """ + + def __init__(self, writer: 'ThreeMFWriter'): + """ + :param writer: The ThreeMFWriter instance that will use this variant + """ + self._writer = writer + + @property + @abstractmethod + def mime_type(self) -> str: + """The MIME type for this 3MF variant.""" + pass + + def handles_mime_type(self, mime_type: str) -> bool: + """Check if this variant handles the given MIME type. + + :param mime_type: The MIME type to check + :return: True if this variant handles the MIME type, False otherwise + """ + return mime_type == self.mime_type + + def prepare_content_types(self, content_types: ET.Element) -> None: + """Prepare the content types XML element for this variant. + + :param content_types: The content types XML element + """ + pass + + def prepare_relations(self, relations_element: ET.Element) -> None: + """Prepare the relations XML element for this variant. + + :param relations_element: The relations XML element + """ + pass + + def process_thumbnail(self, snapshot: QImage, thumbnail_buffer: QBuffer, + archive: zipfile.ZipFile, relations_element: ET.Element) -> None: + """Process the thumbnail for this variant. + + :param snapshot: The snapshot image + :param thumbnail_buffer: Buffer containing the thumbnail data + :param archive: The zip archive to write to + :param relations_element: The relations XML element + """ + pass + + def add_extra_files(self, archive: zipfile.ZipFile, metadata_relations_element: ET.Element) -> None: + """Add any extra files required by this variant to the archive. + + :param archive: The zip archive to write to + :param metadata_relations_element: The metadata relations XML element + """ + pass \ No newline at end of file diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index 61aec6b664..9de117d947 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -1,14 +1,11 @@ -# Copyright (c) 2015-2022 Ultimaker B.V. +# Copyright (c) 2015-2025 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. -import hashlib - -from io import StringIO import json import re import threading -from typing import Optional, cast, List, Dict, Pattern, Set +from typing import Optional, cast, List, Dict, Set from UM.PluginRegistry import PluginRegistry from UM.Mesh.MeshWriter import MeshWriter @@ -52,21 +49,15 @@ import UM.Application from .SettingsExportModel import SettingsExportModel from .SettingsExportGroup import SettingsExportGroup +from .ThreeMFVariant import ThreeMFVariant +from .DefaultVariant import DefaultVariant +from .BambuLabVariant import BambuLabVariant from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") MODEL_PATH = "3D/3dmodel.model" PACKAGE_METADATA_PATH = "Cura/packages.json" -METADATA_PATH = "Metadata" -THUMBNAIL_PATH = f"{METADATA_PATH}/thumbnail.png" -THUMBNAIL_PATH_MULTIPLATE = f"{METADATA_PATH}/plate_1.png" -THUMBNAIL_PATH_MULTIPLATE_SMALL = f"{METADATA_PATH}/plate_1_small.png" -GCODE_PATH = f"{METADATA_PATH}/plate_1.gcode" -GCODE_MD5_PATH = f"{GCODE_PATH}.md5" -MODEL_SETTINGS_PATH = f"{METADATA_PATH}/model_settings.config" -PLATE_DESC_PATH = f"{METADATA_PATH}/plate_1.json" -SLICE_INFO_PATH = f"{METADATA_PATH}/slice_info.config" class ThreeMFWriter(MeshWriter): def __init__(self): @@ -83,6 +74,12 @@ class ThreeMFWriter(MeshWriter): self._store_archive = False self._lock = threading.Lock() + # Register available variants + self._variants = { + DefaultVariant(self).mime_type: DefaultVariant, + BambuLabVariant(self).mime_type: BambuLabVariant + } + @staticmethod def _convertMatrixToString(matrix): result = "" @@ -216,10 +213,23 @@ class ThreeMFWriter(MeshWriter): painter.end() + def _getVariant(self, mime_type: str) -> ThreeMFVariant: + """Get the appropriate variant for the given MIME type. + + :param mime_type: The MIME type to get the variant for + :return: An instance of the variant for the given MIME type + """ + variant_class = self._variants.get(mime_type, DefaultVariant) + return variant_class(self) + def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode, export_settings_model = None, **kwargs) -> bool: self._archive = None # Reset archive archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED) - add_extra_data = kwargs.get("mime_type", "") == "application/vnd.bambulab-package.3dmanufacturing-3dmodel+xml" + + # Determine which variant to use based on mime type in kwargs + mime_type = kwargs.get("mime_type", DefaultVariant(self).mime_type) + variant = self._getVariant(mime_type) + try: model_file = zipfile.ZipInfo(MODEL_PATH) # Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo. @@ -239,11 +249,12 @@ class ThreeMFWriter(MeshWriter): # Create Metadata/_rels/model_settings.config.rels metadata_relations_element = self._makeRelationsTree() - if add_extra_data: - self._storeGCode(archive, metadata_relations_element) - self._storeModelSettings(archive) - self._storePlateDesc(archive) - self._storeSliceInfo(archive) + # Let the variant add its specific files + variant.add_extra_files(archive, metadata_relations_element) + + # Let the variant prepare content types and relations + variant.prepare_content_types(content_types) + variant.prepare_relations(relations_element) # Attempt to add a thumbnail snapshot = self._createSnapshot() @@ -259,32 +270,8 @@ class ThreeMFWriter(MeshWriter): # Add PNG to content types file thumbnail_type = ET.SubElement(content_types, "Default", Extension="png", ContentType="image/png") - if add_extra_data: - archive.writestr(zipfile.ZipInfo(THUMBNAIL_PATH_MULTIPLATE), thumbnail_buffer.data()) - extra_thumbnail_relation_element = ET.SubElement(relations_element, "Relationship", - Target="/" + THUMBNAIL_PATH_MULTIPLATE, Id="rel-2", - Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail") - extra_thumbnail_relation_element_duplicate = ET.SubElement(relations_element, "Relationship", - Target="/" + THUMBNAIL_PATH_MULTIPLATE, Id="rel-4", - Type="http://schemas.bambulab.com/package/2021/cover-thumbnail-middle") - - small_snapshot = snapshot.scaled(128, 128, transformMode = Qt.TransformationMode.SmoothTransformation) - small_thumbnail_buffer = QBuffer() - small_thumbnail_buffer.open(QBuffer.OpenModeFlag.ReadWrite) - small_snapshot.save(small_thumbnail_buffer, "PNG") - archive.writestr(zipfile.ZipInfo(THUMBNAIL_PATH_MULTIPLATE_SMALL), small_thumbnail_buffer.data()) - thumbnail_small_relation_element = ET.SubElement(relations_element, "Relationship", - Target="/" + THUMBNAIL_PATH_MULTIPLATE_SMALL, Id="rel-5", - Type="http://schemas.bambulab.com/package/2021/cover-thumbnail-small") - else: - thumbnail_file = zipfile.ZipInfo(THUMBNAIL_PATH) - # Don't try to compress snapshot file, because the PNG is pretty much as compact as it will get - archive.writestr(thumbnail_file, thumbnail_buffer.data()) - - # Add thumbnail relation to _rels/.rels file - thumbnail_relation_element = ET.SubElement(relations_element, "Relationship", - Target="/" + THUMBNAIL_PATH, Id="rel1", - Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail") + # Let the variant process the thumbnail + variant.process_thumbnail(snapshot, thumbnail_buffer, archive, relations_element) # Write material metadata packages_metadata = self._getMaterialPackageMetadata() + self._getPluginPackageMetadata() @@ -369,94 +356,6 @@ class ThreeMFWriter(MeshWriter): file.compress_type = zipfile.ZIP_DEFLATED archive.writestr(file, b' \n' + ET.tostring(root_element)) - def _storeGCode(self, archive: zipfile.ZipFile, metadata_relations_element: ET.Element): - gcode_textio = StringIO() # We have to convert the g-code into bytes. - gcode_writer = cast(MeshWriter, PluginRegistry.getInstance().getPluginObject("GCodeWriter")) - success = gcode_writer.write(gcode_textio, None) - - if not success: - error_msg = catalog.i18nc("@info:error", "Can't write GCode to 3MF file") - self.setInformation(error_msg) - Logger.error(error_msg) - raise Exception(error_msg) - - gcode_data = gcode_textio.getvalue().encode("UTF-8") - archive.writestr(zipfile.ZipInfo(GCODE_PATH), gcode_data) - - gcode_relation_element = ET.SubElement(metadata_relations_element, "Relationship", - Target=f"/{GCODE_PATH}", Id="rel-1", - Type="http://schemas.bambulab.com/package/2021/gcode") - - # Calculate and store the MD5 sum of the gcode data - md5_hash = hashlib.md5(gcode_data).hexdigest() - archive.writestr(zipfile.ZipInfo(GCODE_MD5_PATH), md5_hash.encode("UTF-8")) - - def _storeModelSettings(self, archive: zipfile.ZipFile): - config = ET.Element("config") - plate = ET.SubElement(config, "plate") - plater_id = ET.SubElement(plate, "metadata", key="plater_id", value="1") - plater_id = ET.SubElement(plate, "metadata", key="plater_name", value="") - plater_id = ET.SubElement(plate, "metadata", key="locked", value="false") - plater_id = ET.SubElement(plate, "metadata", key="filament_map_mode", value="Auto For Flush") - extruders_count = len(CuraApplication.getInstance().getExtruderManager().extruderIds) - plater_id = ET.SubElement(plate, "metadata", key="filament_maps", value=" ".join("1" for _ in range(extruders_count))) - plater_id = ET.SubElement(plate, "metadata", key="gcode_file", value=GCODE_PATH) - plater_id = ET.SubElement(plate, "metadata", key="thumbnail_file", value=THUMBNAIL_PATH_MULTIPLATE) - plater_id = ET.SubElement(plate, "metadata", key="pattern_bbox_file", value=PLATE_DESC_PATH) - - self._storeElementTree(archive, MODEL_SETTINGS_PATH, config) - - def _storePlateDesc(self, archive: zipfile.ZipFile): - plate_desc = {} - - filament_ids = [] - filament_colors = [] - - for extruder in CuraApplication.getInstance().getExtruderManager().getUsedExtruderStacks(): - filament_ids.append(extruder.getValue("extruder_nr")) - filament_colors.append(self._getMaterialColor(extruder)) - - plate_desc["filament_ids"] = filament_ids - plate_desc["filament_colors"] = filament_colors - plate_desc["first_extruder"] = CuraApplication.getInstance().getExtruderManager().getInitialExtruderNr() - plate_desc["is_seq_print"] = Application.getInstance().getGlobalContainerStack().getValue("print_sequence") == "one_at_a_time" - plate_desc["nozzle_diameter"] = CuraApplication.getInstance().getExtruderManager().getActiveExtruderStack().getValue("machine_nozzle_size") - plate_desc["version"] = 2 - - file = zipfile.ZipInfo(PLATE_DESC_PATH) - file.compress_type = zipfile.ZIP_DEFLATED - archive.writestr(file, json.dumps(plate_desc).encode("UTF-8")) - - def _storeSliceInfo(self, archive: zipfile.ZipFile): - config = ET.Element("config") - - header = ET.SubElement(config, "header") - header_type = ET.SubElement(header, "header_item", key="X-BBL-Client-Type", value="slicer") - header_version = ET.SubElement(header, "header_item", key="X-BBL-Client-Version", value="02.00.01.50") - - plate = ET.SubElement(config, "plate") - index = ET.SubElement(plate, "metadata", key="index", value="1") - nozzle_diameter = ET.SubElement(plate, - "metadata", - key="nozzle_diameters", - value=str(CuraApplication.getInstance().getExtruderManager().getActiveExtruderStack().getValue("machine_nozzle_size"))) - - print_information = CuraApplication.getInstance().getPrintInformation() - for index, extruder in enumerate(Application.getInstance().getGlobalContainerStack().extruderList): - used_m = print_information.materialLengths[index] - used_g = print_information.materialWeights[index] - if used_m > 0.0 and used_g > 0.0: - filament = ET.SubElement(plate, - "filament", - id=str(extruder.getValue("extruder_nr") + 1), - tray_info_idx="GFA00", - type=extruder.material.getMetaDataEntry("material", ""), - color=self._getMaterialColor(extruder), - used_m=str(used_m), - used_g=str(used_g)) - - self._storeElementTree(archive, SLICE_INFO_PATH, config) - def _makeRelationsTree(self): return ET.Element("Relationships", xmlns=self._namespaces["relationships"]) From 371f280ab8f3d74e6cbecb59fd4d8a8d9b7e8ac3 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Thu, 24 Apr 2025 15:13:24 +0200 Subject: [PATCH 29/31] Add newlines to the end of new files. done as part of CURA-12099 --- plugins/3MFWriter/BambuLabVariant.py | 2 +- plugins/3MFWriter/DefaultVariant.py | 2 +- plugins/3MFWriter/ThreeMFVariant.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/3MFWriter/BambuLabVariant.py b/plugins/3MFWriter/BambuLabVariant.py index e30cc110b7..56337bbc31 100644 --- a/plugins/3MFWriter/BambuLabVariant.py +++ b/plugins/3MFWriter/BambuLabVariant.py @@ -165,4 +165,4 @@ class BambuLabVariant(ThreeMFVariant): used_m=str(used_m), used_g=str(used_g)) - self._writer._storeElementTree(archive, SLICE_INFO_PATH, config) \ No newline at end of file + self._writer._storeElementTree(archive, SLICE_INFO_PATH, config) diff --git a/plugins/3MFWriter/DefaultVariant.py b/plugins/3MFWriter/DefaultVariant.py index 52dd381ed1..7f5522c3af 100644 --- a/plugins/3MFWriter/DefaultVariant.py +++ b/plugins/3MFWriter/DefaultVariant.py @@ -30,4 +30,4 @@ class DefaultVariant(ThreeMFVariant): # Add thumbnail relation to _rels/.rels file ET.SubElement(relations_element, "Relationship", Target="/" + THUMBNAIL_PATH, Id="rel1", - Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail") \ No newline at end of file + Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail") diff --git a/plugins/3MFWriter/ThreeMFVariant.py b/plugins/3MFWriter/ThreeMFVariant.py index 9b7f0eadf4..d8f4e31770 100644 --- a/plugins/3MFWriter/ThreeMFVariant.py +++ b/plugins/3MFWriter/ThreeMFVariant.py @@ -71,4 +71,4 @@ class ThreeMFVariant(ABC): :param archive: The zip archive to write to :param metadata_relations_element: The metadata relations XML element """ - pass \ No newline at end of file + pass From 603df28c2adea8a47b470adf780419ecba57aff4 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 28 Apr 2025 13:36:54 +0200 Subject: [PATCH 30/31] Fix all exporters being broken CURA-12099 --- plugins/GCodeGzWriter/GCodeGzWriter.py | 2 +- plugins/GCodeWriter/GCodeWriter.py | 2 +- plugins/MakerbotWriter/MakerbotWriter.py | 2 +- plugins/UFPWriter/UFPWriter.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/GCodeGzWriter/GCodeGzWriter.py b/plugins/GCodeGzWriter/GCodeGzWriter.py index 2bbaaeb0a3..cb5b66fbdc 100644 --- a/plugins/GCodeGzWriter/GCodeGzWriter.py +++ b/plugins/GCodeGzWriter/GCodeGzWriter.py @@ -24,7 +24,7 @@ class GCodeGzWriter(MeshWriter): def __init__(self) -> None: super().__init__(add_to_recent_files = False) - def write(self, stream: BufferedIOBase, nodes: List[SceneNode], mode = MeshWriter.OutputMode.BinaryMode) -> bool: + def write(self, stream: BufferedIOBase, nodes: List[SceneNode], mode = MeshWriter.OutputMode.BinaryMode, **kwargs) -> bool: """Writes the gzipped g-code to a stream. Note that even though the function accepts a collection of nodes, the diff --git a/plugins/GCodeWriter/GCodeWriter.py b/plugins/GCodeWriter/GCodeWriter.py index 9fa4f88614..0eac653b56 100644 --- a/plugins/GCodeWriter/GCodeWriter.py +++ b/plugins/GCodeWriter/GCodeWriter.py @@ -56,7 +56,7 @@ class GCodeWriter(MeshWriter): self._application = Application.getInstance() - def write(self, stream, nodes, mode = MeshWriter.OutputMode.TextMode): + def write(self, stream, nodes, mode = MeshWriter.OutputMode.TextMode, **kwargs): """Writes the g-code for the entire scene to a stream. Note that even though the function accepts a collection of nodes, the diff --git a/plugins/MakerbotWriter/MakerbotWriter.py b/plugins/MakerbotWriter/MakerbotWriter.py index f35b53a84d..e4dae9376b 100644 --- a/plugins/MakerbotWriter/MakerbotWriter.py +++ b/plugins/MakerbotWriter/MakerbotWriter.py @@ -91,7 +91,7 @@ class MakerbotWriter(MeshWriter): return None - def write(self, stream: BufferedIOBase, nodes: List[SceneNode], mode=MeshWriter.OutputMode.BinaryMode) -> bool: + def write(self, stream: BufferedIOBase, nodes: List[SceneNode], mode=MeshWriter.OutputMode.BinaryMode, **kwargs) -> bool: metadata, file_format = self._getMeta(nodes) if mode != MeshWriter.OutputMode.BinaryMode: Logger.log("e", "MakerbotWriter does not support text mode.") diff --git a/plugins/UFPWriter/UFPWriter.py b/plugins/UFPWriter/UFPWriter.py index 0cf756b6a4..c5558c1140 100644 --- a/plugins/UFPWriter/UFPWriter.py +++ b/plugins/UFPWriter/UFPWriter.py @@ -51,7 +51,7 @@ class UFPWriter(MeshWriter): # Qt thread. The File read/write operations right now are executed on separated threads because they are scheduled # by the Job class. @call_on_qt_thread - def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode): + def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode, **kwargs): archive = VirtualFile() archive.openStream(stream, "application/x-ufp", OpenMode.WriteOnly) From 46216e12829060f0b7fbe64f8706bb7d0d13e2ca Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Tue, 29 Apr 2025 09:25:19 +0200 Subject: [PATCH 31/31] Renamed confusing 'Default' variant class to 'Cura3mf'. result of code review of CURA-12099 --- .../3MFWriter/{DefaultVariant.py => Cura3mfVariant.py} | 2 +- plugins/3MFWriter/ThreeMFWriter.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) rename plugins/3MFWriter/{DefaultVariant.py => Cura3mfVariant.py} (97%) diff --git a/plugins/3MFWriter/DefaultVariant.py b/plugins/3MFWriter/Cura3mfVariant.py similarity index 97% rename from plugins/3MFWriter/DefaultVariant.py rename to plugins/3MFWriter/Cura3mfVariant.py index 7f5522c3af..3ae766e651 100644 --- a/plugins/3MFWriter/DefaultVariant.py +++ b/plugins/3MFWriter/Cura3mfVariant.py @@ -13,7 +13,7 @@ from .ThreeMFVariant import ThreeMFVariant METADATA_PATH = "Metadata" THUMBNAIL_PATH = f"{METADATA_PATH}/thumbnail.png" -class DefaultVariant(ThreeMFVariant): +class Cura3mfVariant(ThreeMFVariant): """Default implementation of the 3MF format.""" @property diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index 9de117d947..1ca06ef2f4 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -50,7 +50,7 @@ import UM.Application from .SettingsExportModel import SettingsExportModel from .SettingsExportGroup import SettingsExportGroup from .ThreeMFVariant import ThreeMFVariant -from .DefaultVariant import DefaultVariant +from .Cura3mfVariant import Cura3mfVariant from .BambuLabVariant import BambuLabVariant from UM.i18n import i18nCatalog @@ -76,7 +76,7 @@ class ThreeMFWriter(MeshWriter): # Register available variants self._variants = { - DefaultVariant(self).mime_type: DefaultVariant, + Cura3mfVariant(self).mime_type: Cura3mfVariant, BambuLabVariant(self).mime_type: BambuLabVariant } @@ -219,7 +219,7 @@ class ThreeMFWriter(MeshWriter): :param mime_type: The MIME type to get the variant for :return: An instance of the variant for the given MIME type """ - variant_class = self._variants.get(mime_type, DefaultVariant) + variant_class = self._variants.get(mime_type, Cura3mfVariant) return variant_class(self) def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode, export_settings_model = None, **kwargs) -> bool: @@ -227,7 +227,7 @@ class ThreeMFWriter(MeshWriter): archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED) # Determine which variant to use based on mime type in kwargs - mime_type = kwargs.get("mime_type", DefaultVariant(self).mime_type) + mime_type = kwargs.get("mime_type", Cura3mfVariant(self).mime_type) variant = self._getVariant(mime_type) try: