Merge remote-tracking branch 'origin/CURA-12099_export-and-import-to-bambu-3mf' into CURA-12101_introduce-x1

This commit is contained in:
Erwan MATHIEU 2025-04-29 10:22:57 +02:00
commit 52be6f3d2d
57 changed files with 1145 additions and 283 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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")
}
]

View File

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

View File

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

View File

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

View File

@ -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'<?xml version="1.0" encoding="UTF-8"?> \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"])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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