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" diff --git a/cura/API/Backups.py b/cura/API/Backups.py index 1940d38a36..a52dcbfb6b 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,19 +22,22 @@ 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: + 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 19655df531..1438ce293a 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -1,5 +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 io import os @@ -7,12 +10,13 @@ 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 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,6 +34,7 @@ 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""" @@ -42,7 +47,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() @@ -68,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() @@ -77,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 = { @@ -92,22 +95,72 @@ 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: 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. + + 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 (as the 2nd name). + """ + 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_on_close=False) + with open(tmpfile.name, "w") as outfile: + json.dump(data, outfile) + add_to_archive(tmpfile.name, file_path) + 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: Tuple of a set of plugin-ids and a set of plugin-paths. + """ + plugin_reg = PluginRegistry.getInstance() + id = "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. :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_ids, reinstall_instead_paths = self._findRedownloadablePlugins(available_remote_plugins) + tmpfiles = [] try: archive = ZipFile(buffer, "w", ZIP_DEFLATED) - for root, folders, files in os.walk(root_path): + 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): 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 - archive.write(absolute_path, absolute_path[len(root_path) + len(os.sep):]) + 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, 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 6c4670edb6..67d6c84601 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,17 +34,18 @@ 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 - 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): @@ -54,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/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 diff --git a/packaging/NSIS/Ultimaker-Cura.nsi.jinja b/packaging/NSIS/Ultimaker-Cura.nsi.jinja index ac826af0d9..9f61e6950c 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 }}" @@ -16,13 +15,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}" @@ -64,11 +61,9 @@ 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}" -!insertmacro MUI_PAGE_STARTMENU Application $SM_Folder !endif !insertmacro MUI_PAGE_INSTFILES @@ -107,27 +102,11 @@ SetOutPath "$INSTDIR" 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" - -!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" -!endif -!insertmacro MUI_STARTMENU_WRITE_END +CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" !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" - -!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" -!endif +CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}" !endif WriteRegStr ${REG_ROOT} "${REG_APP_PATH}" "" "$INSTDIR\${MAIN_APP_EXE}" @@ -138,9 +117,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 ###################################################################### @@ -177,29 +153,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 -!insertmacro MUI_STARTMENU_GETFOLDER "Application" $SM_Folder -Delete "$SMPROGRAMS\$SM_Folder\${APP_NAME}.lnk" -Delete "$SMPROGRAMS\$SM_Folder\Uninstall ${APP_NAME}.lnk" -!ifdef WEB_SITE -Delete "$SMPROGRAMS\$SM_Folder\UltiMaker Cura website.lnk" -!endif -RmDir "$SMPROGRAMS\$SM_Folder" +Delete "$SMPROGRAMS\${APP_NAME}.lnk" +Delete "$SMPROGRAMS\Uninstall ${APP_NAME}.lnk" !endif !ifndef REG_START_MENU -Delete "$SMPROGRAMS\{{ app_name }}\${APP_NAME}.lnk" -Delete "$SMPROGRAMS\{{ app_name }}\Uninstall ${APP_NAME}.lnk" -!ifdef WEB_SITE -Delete "$SMPROGRAMS\{{ app_name }}\UltiMaker Cura website.lnk" -!endif -RmDir "$SMPROGRAMS\{{ app_name }}" +Delete "$SMPROGRAMS\${APP_NAME}.lnk" +Delete "$SMPROGRAMS\Uninstall ${APP_NAME}.lnk" !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")), 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 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/BambuLabVariant.py b/plugins/3MFWriter/BambuLabVariant.py new file mode 100644 index 0000000000..56337bbc31 --- /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) diff --git a/plugins/3MFWriter/Cura3mfVariant.py b/plugins/3MFWriter/Cura3mfVariant.py new file mode 100644 index 0000000000..3ae766e651 --- /dev/null +++ b/plugins/3MFWriter/Cura3mfVariant.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 Cura3mfVariant(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") diff --git a/plugins/3MFWriter/ThreeMFVariant.py b/plugins/3MFWriter/ThreeMFVariant.py new file mode 100644 index 0000000000..d8f4e31770 --- /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 diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index 20c7e5d05c..558b36576f 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -1,19 +1,12 @@ -# 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, Set, TYPE_CHECKING +from typing import Optional, cast, List, Dict, Set -if TYPE_CHECKING: - from Settings.ExtruderStack import ExtruderStack - -from Machines.Models.ExtrudersModel import ExtrudersModel from UM.PluginRegistry import PluginRegistry from UM.Mesh.MeshWriter import MeshWriter from UM.Math.Vector import Vector @@ -28,7 +21,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 @@ -54,21 +49,15 @@ import UM.Application from .SettingsExportModel import SettingsExportModel from .SettingsExportGroup import SettingsExportGroup +from .ThreeMFVariant import ThreeMFVariant +from .Cura3mfVariant import Cura3mfVariant +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): @@ -85,6 +74,12 @@ class ThreeMFWriter(MeshWriter): self._store_archive = False self._lock = threading.Lock() + # Register available variants + self._variants = { + Cura3mfVariant(self).mime_type: Cura3mfVariant, + BambuLabVariant(self).mime_type: BambuLabVariant + } + @staticmethod def _convertMatrixToString(matrix): result = "" @@ -218,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, Cura3mfVariant) + 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", Cura3mfVariant(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. @@ -241,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() @@ -261,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() @@ -371,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"]) diff --git a/plugins/CuraDrive/src/CreateBackupJob.py b/plugins/CuraDrive/src/CreateBackupJob.py index 7d772769ed..4820f886ab 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. @@ -64,13 +98,18 @@ 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. + for _ in range(5000): + CuraApplication.getInstance().processEvents() + if self._job_done.wait(0.02): + break + 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. @@ -83,7 +122,6 @@ class CreateBackupJob(Job): "metadata": backup_metadata } }).encode() - HttpRequestManager.getInstance().put( self._api_backup_url, data = payload, diff --git a/plugins/CuraDrive/src/RestoreBackupJob.py b/plugins/CuraDrive/src/RestoreBackupJob.py index 54c94b389e..503b39547a 100644 --- a/plugins/CuraDrive/src/RestoreBackupJob.py +++ b/plugins/CuraDrive/src/RestoreBackupJob.py @@ -1,8 +1,9 @@ -# Copyright (c) 2021 Ultimaker B.V. +# Copyright (c) 2025 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. - import base64 import hashlib +import json +import os import threading from tempfile import NamedTemporaryFile from typing import Optional, Any, Dict @@ -12,9 +13,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_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. @@ -38,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 @@ -48,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. + for _ in range(5000): + CuraApplication.getInstance().processEvents() + 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): @@ -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_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) while bytes_read: @@ -69,23 +80,98 @@ 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 - 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.") + 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. - 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): + 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 + + to_install = {} + try: + with open(packages_path, "r") as packages_file: + packages_json = json.load(packages_file) + 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 + self._job_done.set() + return + + if len(to_install) < 1: + Logger.info("No packages to reinstall, early out.") + 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) + del to_install[package_id] + + try: + 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 = msg.read(self.DISK_WRITE_BUFFER_SIZE) + CuraApplication.getInstance().processEvents() + 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}'.") + + if len(to_install) < 1: + if len(redownload_errors) == 0: + Logger.info("All packages redownloaded!") + self._job_done.set() + else: + 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, 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: 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/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) 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) diff --git a/resources/definitions/ultimaker_s6.def.json b/resources/definitions/ultimaker_s6.def.json new file mode 100644 index 0000000000..bc0e6a0f4e --- /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_s5_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, + -30, + -10 + ], + "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_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 80e7986acf..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" }, @@ -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 0000000000..d6e83781cc Binary files /dev/null and b/resources/images/UltimakerS6backplate.png differ diff --git a/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm_engineering.inst.cfg new file mode 100644 index 0000000000..88a2326843 --- /dev/null +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm_engineering.inst.cfg @@ -0,0 +1,18 @@ +[general] +definition = ultimaker_s8 +name = Accurate +version = 4 + +[metadata] +intent_category = engineering +material = generic_nylon-cf-slide +quality_type = draft +setting_version = 25 +type = intent +variant = CC+ 0.6 + +[values] +infill_sparse_density = 20 +top_bottom_thickness = =wall_thickness +wall_thickness = =line_width * 3 + diff --git a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_nylon_0.2mm_engineering.inst.cfg b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm_engineering.inst.cfg similarity index 86% rename from resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_nylon_0.2mm_engineering.inst.cfg rename to resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm_engineering.inst.cfg index 2878f4df54..dc9870b7ff 100644 --- a/resources/intent/ultimaker_s8/um_s8_aa_plus_0.4_nylon_0.2mm_engineering.inst.cfg +++ b/resources/intent/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm_engineering.inst.cfg @@ -5,11 +5,11 @@ version = 4 [metadata] intent_category = engineering -material = generic_nylon +material = generic_petcf quality_type = draft setting_version = 25 type = intent -variant = AA+ 0.4 +variant = CC+ 0.6 [values] infill_sparse_density = 20 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 diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm.inst.cfg index 8c76290ed3..37767673aa 100644 --- a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm.inst.cfg +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.4_abs_0.2mm.inst.cfg @@ -15,7 +15,6 @@ weight = -2 cool_min_layer_time = 4 cool_min_layer_time_fan_speed_max = 9 cool_min_temperature = =material_print_temperature - 10 -material_print_temperature = =default_material_print_temperature + 5 retraction_prime_speed = 15 support_structure = tree 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_aa_plus_0.6_abs_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_abs_0.2mm.inst.cfg new file mode 100644 index 0000000000..928293a327 --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_abs_0.2mm.inst.cfg @@ -0,0 +1,23 @@ +[general] +definition = ultimaker_s8 +name = Fast +version = 4 + +[metadata] +material = generic_abs +quality_type = draft +setting_version = 25 +type = quality +variant = AA+ 0.6 +weight = -2 + +[values] +bridge_skin_material_flow = 200 +bridge_wall_material_flow = 200 +cool_min_layer_time = 4 +cool_min_layer_time_fan_speed_max = 9 +cool_min_temperature = =material_print_temperature - 10 +retraction_prime_speed = 15 +support_interface_enable = False +support_structure = tree + diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_abs_0.3mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_abs_0.3mm.inst.cfg new file mode 100644 index 0000000000..a5a83362cc --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_abs_0.3mm.inst.cfg @@ -0,0 +1,25 @@ +[general] +definition = ultimaker_s8 +name = Extra Fast +version = 4 + +[metadata] +material = generic_abs +quality_type = verydraft +setting_version = 25 +type = quality +variant = AA+ 0.6 +weight = -3 + +[values] +bridge_skin_material_flow = 200 +bridge_wall_material_flow = 200 +cool_min_layer_time = 4 +cool_min_layer_time_fan_speed_max = 9 +cool_min_temperature = =material_print_temperature - 10 +material_print_temperature = =default_material_print_temperature + 10 +retraction_prime_speed = 15 +support_interface_enable = False +support_structure = tree +wall_line_width_0 = =line_width * (1 + magic_spiralize * 0.25) + diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_petg_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_petg_0.2mm.inst.cfg new file mode 100644 index 0000000000..be16bb734f --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_petg_0.2mm.inst.cfg @@ -0,0 +1,20 @@ +[general] +definition = ultimaker_s8 +name = Fast +version = 4 + +[metadata] +material = generic_petg +quality_type = draft +setting_version = 25 +type = quality +variant = AA+ 0.6 +weight = -2 + +[values] +bridge_skin_material_flow = 200 +bridge_wall_material_flow = 200 +cool_min_layer_time = 4 +material_print_temperature = =default_material_print_temperature + 5 +support_interface_enable = False + diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_petg_0.3mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_petg_0.3mm.inst.cfg new file mode 100644 index 0000000000..d058e1aef5 --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_petg_0.3mm.inst.cfg @@ -0,0 +1,21 @@ +[general] +definition = ultimaker_s8 +name = Extra Fast +version = 4 + +[metadata] +material = generic_petg +quality_type = verydraft +setting_version = 25 +type = quality +variant = AA+ 0.6 +weight = -3 + +[values] +bridge_skin_material_flow = 200 +bridge_wall_material_flow = 200 +cool_min_layer_time = 4 +material_print_temperature = =default_material_print_temperature + 10 +support_interface_enable = False +wall_line_width_0 = =line_width * (1 + magic_spiralize * 0.25) + diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_pla_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_pla_0.2mm.inst.cfg new file mode 100644 index 0000000000..a5f990f3d8 --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_pla_0.2mm.inst.cfg @@ -0,0 +1,20 @@ +[general] +definition = ultimaker_s8 +name = Fast +version = 4 + +[metadata] +material = generic_pla +quality_type = draft +setting_version = 25 +type = quality +variant = AA+ 0.6 +weight = -2 + +[values] +bridge_skin_material_flow = 200 +bridge_wall_material_flow = 200 +retraction_prime_speed = =retraction_speed +support_interface_enable = False +support_structure = tree + diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_pla_0.3mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_pla_0.3mm.inst.cfg new file mode 100644 index 0000000000..295b7efbfc --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_pla_0.3mm.inst.cfg @@ -0,0 +1,22 @@ +[general] +definition = ultimaker_s8 +name = Extra Fast +version = 4 + +[metadata] +material = generic_pla +quality_type = verydraft +setting_version = 25 +type = quality +variant = AA+ 0.6 +weight = -3 + +[values] +bridge_skin_material_flow = 200 +bridge_wall_material_flow = 200 +material_print_temperature = =default_material_print_temperature + 20 +retraction_prime_speed = =retraction_speed +support_interface_enable = False +support_structure = tree +wall_line_width_0 = =line_width * (1 + magic_spiralize * 0.25) + diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_tough-pla_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_tough-pla_0.2mm.inst.cfg new file mode 100644 index 0000000000..f934c2bfa0 --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_tough-pla_0.2mm.inst.cfg @@ -0,0 +1,20 @@ +[general] +definition = ultimaker_s8 +name = Fast +version = 4 + +[metadata] +material = generic_tough_pla +quality_type = draft +setting_version = 25 +type = quality +variant = AA+ 0.6 +weight = -2 + +[values] +bridge_skin_material_flow = 200 +bridge_wall_material_flow = 200 +retraction_prime_speed = =retraction_speed +support_interface_enable = False +support_structure = tree + diff --git a/resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_tough-pla_0.3mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_tough-pla_0.3mm.inst.cfg new file mode 100644 index 0000000000..aa81282888 --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_aa_plus_0.6_tough-pla_0.3mm.inst.cfg @@ -0,0 +1,22 @@ +[general] +definition = ultimaker_s8 +name = Extra Fast +version = 4 + +[metadata] +material = generic_tough_pla +quality_type = verydraft +setting_version = 25 +type = quality +variant = AA+ 0.6 +weight = -3 + +[values] +bridge_skin_material_flow = 200 +bridge_wall_material_flow = 200 +material_print_temperature = =default_material_print_temperature + 20 +retraction_prime_speed = =retraction_speed +support_interface_enable = False +support_structure = tree +wall_line_width_0 = =line_width * (1 + magic_spiralize * 0.25) + 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 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 diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm.inst.cfg new file mode 100644 index 0000000000..195c11a4e0 --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.2mm.inst.cfg @@ -0,0 +1,17 @@ +[general] +definition = ultimaker_s8 +name = Fast +version = 4 + +[metadata] +material = generic_nylon-cf-slide +quality_type = draft +setting_version = 25 +type = quality +variant = CC+ 0.6 +weight = -2 + +[values] +cool_min_layer_time_fan_speed_max = 11 +retraction_prime_speed = 15 + diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg new file mode 100644 index 0000000000..4f4f7f7eff --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_nylon-cf-slide_0.3mm.inst.cfg @@ -0,0 +1,17 @@ +[general] +definition = ultimaker_s8 +name = Extra Fast +version = 4 + +[metadata] +material = generic_nylon-cf-slide +quality_type = verydraft +setting_version = 25 +type = quality +variant = CC+ 0.6 +weight = -3 + +[values] +cool_min_layer_time_fan_speed_max = 11 +retraction_prime_speed = 15 + diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm.inst.cfg new file mode 100644 index 0000000000..52dc6f1a5e --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.2mm.inst.cfg @@ -0,0 +1,18 @@ +[general] +definition = ultimaker_s8 +name = Fast +version = 4 + +[metadata] +material = generic_petcf +quality_type = draft +setting_version = 25 +type = quality +variant = CC+ 0.6 +weight = -2 + +[values] +bridge_skin_material_flow = 200 +bridge_wall_material_flow = 200 +support_interface_enable = False + diff --git a/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg new file mode 100644 index 0000000000..cd42a93fec --- /dev/null +++ b/resources/quality/ultimaker_s8/um_s8_cc_plus_0.6_petcf_0.3mm.inst.cfg @@ -0,0 +1,20 @@ +[general] +definition = ultimaker_s8 +name = Extra Fast +version = 4 + +[metadata] +material = generic_petcf +quality_type = verydraft +setting_version = 25 +type = quality +variant = CC+ 0.6 +weight = -3 + +[values] +bridge_skin_material_flow = 200 +bridge_wall_material_flow = 200 +material_print_temperature = =default_material_print_temperature + 10 +support_interface_enable = False +wall_line_width_0 = =line_width * (1 + magic_spiralize * 0.25) + diff --git a/resources/variants/ultimaker_s6_aa_plus04.inst.cfg b/resources/variants/ultimaker_s6_aa_plus04.inst.cfg new file mode 100644 index 0000000000..5aba46a964 --- /dev/null +++ b/resources/variants/ultimaker_s6_aa_plus04.inst.cfg @@ -0,0 +1,17 @@ +[general] +definition = ultimaker_s6 +name = AA+ 0.4 +version = 4 + +[metadata] +hardware_type = nozzle +setting_version = 25 +type = variant + +[values] +machine_nozzle_cool_down_speed = 0.9 +machine_nozzle_id = AA+ 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_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_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_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 + 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 +