mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-08-18 02:36:02 +08:00
Merge remote-tracking branch 'origin/CURA-12099_export-and-import-to-bambu-3mf' into CURA-12101_introduce-x1
This commit is contained in:
commit
52be6f3d2d
@ -6,7 +6,7 @@ requirements:
|
|||||||
- "cura_binary_data/5.11.0-alpha.0@ultimaker/testing"
|
- "cura_binary_data/5.11.0-alpha.0@ultimaker/testing"
|
||||||
- "fdm_materials/5.11.0-alpha.0@ultimaker/testing"
|
- "fdm_materials/5.11.0-alpha.0@ultimaker/testing"
|
||||||
- "dulcificum/5.10.0"
|
- "dulcificum/5.10.0"
|
||||||
- "pysavitar/5.10.0"
|
- "pysavitar/5.11.0-alpha.0"
|
||||||
- "pynest2d/5.10.0"
|
- "pynest2d/5.10.0"
|
||||||
requirements_internal:
|
requirements_internal:
|
||||||
- "fdm_materials/5.11.0-alpha.0@ultimaker/testing"
|
- "fdm_materials/5.11.0-alpha.0@ultimaker/testing"
|
||||||
|
@ -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.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from typing import Tuple, Optional, TYPE_CHECKING, Dict, Any
|
from typing import Tuple, Optional, TYPE_CHECKING, Dict, Any
|
||||||
|
|
||||||
@ -9,14 +9,10 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class Backups:
|
class Backups:
|
||||||
"""The back-ups API provides a version-proof bridge between Cura's
|
"""The back-ups API provides a version-proof bridge between Cura's BackupManager and plug-ins that hook into it.
|
||||||
|
|
||||||
BackupManager and plug-ins that hook into it.
|
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
from cura.API import CuraAPI
|
from cura.API import CuraAPI
|
||||||
api = CuraAPI()
|
api = CuraAPI()
|
||||||
api.backups.createBackup()
|
api.backups.createBackup()
|
||||||
@ -26,19 +22,22 @@ class Backups:
|
|||||||
def __init__(self, application: "CuraApplication") -> None:
|
def __init__(self, application: "CuraApplication") -> None:
|
||||||
self.manager = BackupsManager(application)
|
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.
|
"""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: 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.
|
"""Restore a back-up using the BackupsManager.
|
||||||
|
|
||||||
:param zip_file: A ZIP file containing the actual back-up data.
|
: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.
|
: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()
|
||||||
|
@ -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.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
@ -7,12 +10,13 @@ import re
|
|||||||
import shutil
|
import shutil
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile
|
from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile
|
||||||
from typing import Dict, Optional, TYPE_CHECKING, List
|
from typing import Callable, Dict, Optional, TYPE_CHECKING, List
|
||||||
|
|
||||||
from UM import i18nCatalog
|
from UM import i18nCatalog
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
from UM.Platform import Platform
|
from UM.Platform import Platform
|
||||||
|
from UM.PluginRegistry import PluginRegistry
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
from UM.Version import Version
|
from UM.Version import Version
|
||||||
|
|
||||||
@ -30,6 +34,7 @@ class Backup:
|
|||||||
"""These files should be ignored when making a backup."""
|
"""These files should be ignored when making a backup."""
|
||||||
|
|
||||||
IGNORED_FOLDERS = [] # type: List[str]
|
IGNORED_FOLDERS = [] # type: List[str]
|
||||||
|
"""These folders should be ignored when making a backup."""
|
||||||
|
|
||||||
SECRETS_SETTINGS = ["general/ultimaker_auth_data"]
|
SECRETS_SETTINGS = ["general/ultimaker_auth_data"]
|
||||||
"""Secret preferences that need to obfuscated when making a backup of Cura"""
|
"""Secret preferences that need to obfuscated when making a backup of Cura"""
|
||||||
@ -42,7 +47,7 @@ class Backup:
|
|||||||
self.zip_file = zip_file # type: Optional[bytes]
|
self.zip_file = zip_file # type: Optional[bytes]
|
||||||
self.meta_data = meta_data # type: Optional[Dict[str, str]]
|
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."""
|
"""Create a back-up from the current user config folder."""
|
||||||
|
|
||||||
cura_release = self._application.getVersion()
|
cura_release = self._application.getVersion()
|
||||||
@ -68,7 +73,7 @@ class Backup:
|
|||||||
|
|
||||||
# Create an empty buffer and write the archive to it.
|
# Create an empty buffer and write the archive to it.
|
||||||
buffer = io.BytesIO()
|
buffer = io.BytesIO()
|
||||||
archive = self._makeArchive(buffer, version_data_dir)
|
archive = self._makeArchive(buffer, version_data_dir, available_remote_plugins)
|
||||||
if archive is None:
|
if archive is None:
|
||||||
return
|
return
|
||||||
files = archive.namelist()
|
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.
|
machine_count = max(len([s for s in files if "machine_instances/" in s]) - 1, 0) # If people delete their profiles but not their preferences, it can still make a backup, and report -1 profiles. Server crashes on this.
|
||||||
material_count = max(len([s for s in files if "materials/" in s]) - 1, 0)
|
material_count = max(len([s for s in files if "materials/" in s]) - 1, 0)
|
||||||
profile_count = max(len([s for s in files if "quality_changes/" in s]) - 1, 0)
|
profile_count = max(len([s for s in files if "quality_changes/" in s]) - 1, 0)
|
||||||
# We don't store plugins anymore, since if you can make backups, you have an account (and the plugins are
|
plugin_count = len([s for s in files if "plugin.json" in s])
|
||||||
# on the marketplace anyway)
|
|
||||||
plugin_count = 0
|
|
||||||
# Store the archive and metadata so the BackupManager can fetch them when needed.
|
# Store the archive and metadata so the BackupManager can fetch them when needed.
|
||||||
self.zip_file = buffer.getvalue()
|
self.zip_file = buffer.getvalue()
|
||||||
self.meta_data = {
|
self.meta_data = {
|
||||||
@ -92,22 +95,72 @@ class Backup:
|
|||||||
# Restore the obfuscated settings
|
# Restore the obfuscated settings
|
||||||
self._illuminate(**secrets)
|
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.
|
"""Make a full archive from the given root path with the given name.
|
||||||
|
|
||||||
:param root_path: The root directory to archive recursively.
|
:param root_path: The root directory to archive recursively.
|
||||||
:return: The archive as bytes.
|
:return: The archive as bytes.
|
||||||
"""
|
"""
|
||||||
ignore_string = re.compile("|".join(self.IGNORED_FILES + self.IGNORED_FOLDERS))
|
ignore_string = re.compile("|".join(self.IGNORED_FILES + self.IGNORED_FOLDERS))
|
||||||
|
reinstall_instead_ids, reinstall_instead_paths = self._findRedownloadablePlugins(available_remote_plugins)
|
||||||
|
tmpfiles = []
|
||||||
try:
|
try:
|
||||||
archive = ZipFile(buffer, "w", ZIP_DEFLATED)
|
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:
|
for item_name in folders + files:
|
||||||
absolute_path = os.path.join(root, item_name)
|
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
|
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()
|
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
|
return archive
|
||||||
except (IOError, OSError, BadZipfile) as error:
|
except (IOError, OSError, BadZipfile) as error:
|
||||||
Logger.log("e", "Could not create archive from user data directory: %s", error)
|
Logger.log("e", "Could not create archive from user data directory: %s", error)
|
||||||
|
@ -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.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import Dict, Optional, Tuple, TYPE_CHECKING
|
from typing import Dict, Optional, Tuple, TYPE_CHECKING
|
||||||
@ -22,7 +22,10 @@ class BackupsManager:
|
|||||||
def __init__(self, application: "CuraApplication") -> None:
|
def __init__(self, application: "CuraApplication") -> None:
|
||||||
self._application = application
|
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.
|
Get a back-up of the current configuration.
|
||||||
|
|
||||||
@ -31,17 +34,18 @@ class BackupsManager:
|
|||||||
|
|
||||||
self._disableAutoSave()
|
self._disableAutoSave()
|
||||||
backup = Backup(self._application)
|
backup = Backup(self._application)
|
||||||
backup.makeFromCurrent()
|
backup.makeFromCurrent(available_remote_plugins if self.shouldReinstallDownloadablePlugins() else frozenset())
|
||||||
self._enableAutoSave()
|
self._enableAutoSave()
|
||||||
# We don't return a Backup here because we want plugins only to interact with our API and not full objects.
|
# 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
|
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.
|
Restore a back-up from a given ZipFile.
|
||||||
|
|
||||||
:param zip_file: A bytes object containing the actual back-up.
|
: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 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):
|
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)
|
backup = Backup(self._application, zip_file = zip_file, meta_data = meta_data)
|
||||||
restored = backup.restore()
|
restored = backup.restore()
|
||||||
|
|
||||||
if restored:
|
if restored and auto_close:
|
||||||
# At this point, Cura will need to restart for the changes to take effect.
|
# At this point, Cura will need to restart for the changes to take effect.
|
||||||
# We don't want to store the data at this point as that would override the just-restored backup.
|
# We don't want to store the data at this point as that would override the just-restored backup.
|
||||||
self._application.windowClosed(save_data = False)
|
self._application.windowClosed(save_data = False)
|
||||||
|
@ -188,6 +188,7 @@ class CuraApplication(QtApplication):
|
|||||||
|
|
||||||
self._single_instance = None
|
self._single_instance = None
|
||||||
self._open_project_mode: Optional[str] = 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]
|
self._cura_formula_functions = None # type: Optional[CuraFormulaFunctions]
|
||||||
|
|
||||||
@ -2015,18 +2016,18 @@ class CuraApplication(QtApplication):
|
|||||||
self.deleteAll()
|
self.deleteAll()
|
||||||
break
|
break
|
||||||
|
|
||||||
is_project_file = self.checkIsValidProjectFile(file)
|
self._read_operation_is_project_file = self.checkIsValidProjectFile(file)
|
||||||
|
|
||||||
if self._open_project_mode is None:
|
if self._open_project_mode is None:
|
||||||
self._open_project_mode = self.getPreferences().getValue("cura/choice_on_open_project")
|
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
|
# open as project immediately without presenting a dialog
|
||||||
workspace_handler = self.getWorkspaceFileHandler()
|
workspace_handler = self.getWorkspaceFileHandler()
|
||||||
workspace_handler.readLocalFile(file, add_to_recent_files_hint = add_to_recent_files)
|
workspace_handler.readLocalFile(file, add_to_recent_files_hint = add_to_recent_files)
|
||||||
return
|
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
|
# present a dialog asking to open as project or import models
|
||||||
self.callLater(self.openProjectFile.emit, file, add_to_recent_files)
|
self.callLater(self.openProjectFile.emit, file, add_to_recent_files)
|
||||||
return
|
return
|
||||||
@ -2164,7 +2165,7 @@ class CuraApplication(QtApplication):
|
|||||||
nodes_to_arrange.append(node)
|
nodes_to_arrange.append(node)
|
||||||
# If the file is a project,and models are to be loaded from a that project,
|
# If the file is a project,and models are to be loaded from a that project,
|
||||||
# models inside file should be arranged in buildplate.
|
# 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)
|
nodes_to_arrange.append(node)
|
||||||
|
|
||||||
# This node is deep copied from some other node which already has a BuildPlateDecorator, but the deepcopy
|
# This node is deep copied from some other node which already has a BuildPlateDecorator, but the deepcopy
|
||||||
|
@ -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.
|
# Cura's build system is released under the terms of the AGPLv3 or higher.
|
||||||
|
|
||||||
!define APP_NAME "{{ app_name }}"
|
!define APP_NAME "{{ app_name }}"
|
||||||
!define COMP_NAME "{{ company }}"
|
!define COMP_NAME "{{ company }}"
|
||||||
!define WEB_SITE "{{ web_site }}"
|
|
||||||
!define VERSION "{{ version }}"
|
!define VERSION "{{ version }}"
|
||||||
!define VIVERSION "{{ version_major }}.{{ version_minor }}.{{ version_patch }}.0"
|
!define VIVERSION "{{ version_major }}.{{ version_minor }}.{{ version_patch }}.0"
|
||||||
!define COPYRIGHT "Copyright (c) {{ year }} {{ company }}"
|
!define COPYRIGHT "Copyright (c) {{ year }} {{ company }}"
|
||||||
@ -16,13 +15,11 @@
|
|||||||
!define REG_APP_PATH "Software\Microsoft\Windows\CurrentVersion\App Paths\${APP_NAME}-${VERSION}"
|
!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 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
|
;Require administrator access
|
||||||
RequestExecutionLevel admin
|
RequestExecutionLevel admin
|
||||||
|
|
||||||
var SM_Folder
|
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
VIProductVersion "${VIVERSION}"
|
VIProductVersion "${VIVERSION}"
|
||||||
@ -64,11 +61,9 @@ InstallDir "$PROGRAMFILES64\${APP_NAME}"
|
|||||||
|
|
||||||
!ifdef REG_START_MENU
|
!ifdef REG_START_MENU
|
||||||
!define MUI_STARTMENUPAGE_NODISABLE
|
!define MUI_STARTMENUPAGE_NODISABLE
|
||||||
!define MUI_STARTMENUPAGE_DEFAULTFOLDER "UltiMaker Cura"
|
|
||||||
!define MUI_STARTMENUPAGE_REGISTRY_ROOT "${REG_ROOT}"
|
!define MUI_STARTMENUPAGE_REGISTRY_ROOT "${REG_ROOT}"
|
||||||
!define MUI_STARTMENUPAGE_REGISTRY_KEY "${UNINSTALL_PATH}"
|
!define MUI_STARTMENUPAGE_REGISTRY_KEY "${UNINSTALL_PATH}"
|
||||||
!define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "${REG_START_MENU}"
|
!define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "${REG_START_MENU}"
|
||||||
!insertmacro MUI_PAGE_STARTMENU Application $SM_Folder
|
|
||||||
!endif
|
!endif
|
||||||
|
|
||||||
!insertmacro MUI_PAGE_INSTFILES
|
!insertmacro MUI_PAGE_INSTFILES
|
||||||
@ -107,27 +102,11 @@ SetOutPath "$INSTDIR"
|
|||||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
!ifdef REG_START_MENU
|
!ifdef REG_START_MENU
|
||||||
!insertmacro MUI_STARTMENU_WRITE_BEGIN Application
|
CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}"
|
||||||
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
|
|
||||||
!endif
|
!endif
|
||||||
|
|
||||||
!ifndef REG_START_MENU
|
!ifndef REG_START_MENU
|
||||||
CreateDirectory "$SMPROGRAMS\{{ app_name }}"
|
CreateShortCut "$SMPROGRAMS\${APP_NAME}.lnk" "$INSTDIR\${MAIN_APP_EXE}"
|
||||||
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
|
|
||||||
!endif
|
!endif
|
||||||
|
|
||||||
WriteRegStr ${REG_ROOT} "${REG_APP_PATH}" "" "$INSTDIR\${MAIN_APP_EXE}"
|
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}" "DisplayVersion" "${VERSION}"
|
||||||
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "Publisher" "${COMP_NAME}"
|
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "Publisher" "${COMP_NAME}"
|
||||||
|
|
||||||
!ifdef WEB_SITE
|
|
||||||
WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "URLInfoAbout" "${WEB_SITE}"
|
|
||||||
!endif
|
|
||||||
SectionEnd
|
SectionEnd
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
@ -177,29 +153,17 @@ RmDir "$INSTDIR\share\uranium"
|
|||||||
RmDir "$INSTDIR\share"
|
RmDir "$INSTDIR\share"
|
||||||
|
|
||||||
Delete "$INSTDIR\uninstall.exe"
|
Delete "$INSTDIR\uninstall.exe"
|
||||||
!ifdef WEB_SITE
|
|
||||||
Delete "$INSTDIR\${APP_NAME} website.url"
|
|
||||||
!endif
|
|
||||||
|
|
||||||
RmDir /r /REBOOTOK "$INSTDIR"
|
RmDir /r /REBOOTOK "$INSTDIR"
|
||||||
|
|
||||||
!ifdef REG_START_MENU
|
!ifdef REG_START_MENU
|
||||||
!insertmacro MUI_STARTMENU_GETFOLDER "Application" $SM_Folder
|
Delete "$SMPROGRAMS\${APP_NAME}.lnk"
|
||||||
Delete "$SMPROGRAMS\$SM_Folder\${APP_NAME}.lnk"
|
Delete "$SMPROGRAMS\Uninstall ${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"
|
|
||||||
!endif
|
!endif
|
||||||
|
|
||||||
!ifndef REG_START_MENU
|
!ifndef REG_START_MENU
|
||||||
Delete "$SMPROGRAMS\{{ app_name }}\${APP_NAME}.lnk"
|
Delete "$SMPROGRAMS\${APP_NAME}.lnk"
|
||||||
Delete "$SMPROGRAMS\{{ app_name }}\Uninstall ${APP_NAME}.lnk"
|
Delete "$SMPROGRAMS\Uninstall ${APP_NAME}.lnk"
|
||||||
!ifdef WEB_SITE
|
|
||||||
Delete "$SMPROGRAMS\{{ app_name }}\UltiMaker Cura website.lnk"
|
|
||||||
!endif
|
|
||||||
RmDir "$SMPROGRAMS\{{ app_name }}"
|
|
||||||
!endif
|
!endif
|
||||||
|
|
||||||
!insertmacro APP_UNASSOCIATE "stl" "Cura.model"
|
!insertmacro APP_UNASSOCIATE "stl" "Cura.model"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2022 UltiMaker
|
# Copyright (c) 2025 UltiMaker
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# 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_minor = str(parsed_version.minor),
|
||||||
version_patch = str(parsed_version.patch),
|
version_patch = str(parsed_version.patch),
|
||||||
company = "UltiMaker",
|
company = "UltiMaker",
|
||||||
web_site = "https://ultimaker.com",
|
|
||||||
year = datetime.now().year,
|
year = datetime.now().year,
|
||||||
cura_license_file = str(source_loc.joinpath("packaging", "cura_license.txt")),
|
cura_license_file = str(source_loc.joinpath("packaging", "cura_license.txt")),
|
||||||
compression_method = "LZMA", # ZLIB, BZIP2 or LZMA
|
compression_method = "LZMA", # ZLIB, BZIP2 or LZMA
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
# Copyright (c) 2022 UltiMaker
|
# Copyright (c) 2025 UltiMaker
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# 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_minor=str(parsed_version.minor),
|
||||||
version_patch=str(parsed_version.patch),
|
version_patch=str(parsed_version.patch),
|
||||||
company="UltiMaker",
|
company="UltiMaker",
|
||||||
web_site="https://ultimaker.com",
|
|
||||||
year=datetime.now().year,
|
year=datetime.now().year,
|
||||||
upgrade_code=str(uuid.uuid5(uuid.NAMESPACE_DNS, app_name)),
|
upgrade_code=str(uuid.uuid5(uuid.NAMESPACE_DNS, app_name)),
|
||||||
cura_license_file=str(source_loc.joinpath("packaging", "msi", "cura_license.rtf")),
|
cura_license_file=str(source_loc.joinpath("packaging", "msi", "cura_license.rtf")),
|
||||||
|
@ -94,7 +94,7 @@ class ThreeMFReader(MeshReader):
|
|||||||
return temp_mat
|
return temp_mat
|
||||||
|
|
||||||
@staticmethod
|
@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.
|
"""Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node.
|
||||||
|
|
||||||
:returns: Scene node.
|
:returns: Scene node.
|
||||||
@ -115,6 +115,10 @@ class ThreeMFReader(MeshReader):
|
|||||||
|
|
||||||
active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
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 = CuraSceneNode() # This adds a SettingOverrideDecorator
|
||||||
um_node.addDecorator(BuildPlateDecorator(active_build_plate))
|
um_node.addDecorator(BuildPlateDecorator(active_build_plate))
|
||||||
try:
|
try:
|
||||||
@ -143,7 +147,7 @@ class ThreeMFReader(MeshReader):
|
|||||||
um_node.setMeshData(mesh_data)
|
um_node.setMeshData(mesh_data)
|
||||||
|
|
||||||
for child in savitar_node.getChildren():
|
for child in savitar_node.getChildren():
|
||||||
child_node = ThreeMFReader._convertSavitarNodeToUMNode(child)
|
child_node = ThreeMFReader._convertSavitarNodeToUMNode(child, archive=archive)
|
||||||
if child_node:
|
if child_node:
|
||||||
um_node.addChild(child_node)
|
um_node.addChild(child_node)
|
||||||
|
|
||||||
@ -232,7 +236,7 @@ class ThreeMFReader(MeshReader):
|
|||||||
CuraApplication.getInstance().getController().getScene().setMetaDataEntry(key, value)
|
CuraApplication.getInstance().getController().getScene().setMetaDataEntry(key, value)
|
||||||
|
|
||||||
for node in scene_3mf.getSceneNodes():
|
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:
|
if um_node is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ def getMetaData() -> Dict:
|
|||||||
if "3MFReader.ThreeMFReader" in sys.modules:
|
if "3MFReader.ThreeMFReader" in sys.modules:
|
||||||
metaData["mesh_reader"] = [
|
metaData["mesh_reader"] = [
|
||||||
{
|
{
|
||||||
"extension": "3mf",
|
"extension": workspace_extension,
|
||||||
"description": catalog.i18nc("@item:inlistbox", "3MF File")
|
"description": catalog.i18nc("@item:inlistbox", "3MF File")
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
168
plugins/3MFWriter/BambuLabVariant.py
Normal file
168
plugins/3MFWriter/BambuLabVariant.py
Normal 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)
|
33
plugins/3MFWriter/Cura3mfVariant.py
Normal file
33
plugins/3MFWriter/Cura3mfVariant.py
Normal 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")
|
74
plugins/3MFWriter/ThreeMFVariant.py
Normal file
74
plugins/3MFWriter/ThreeMFVariant.py
Normal 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
|
@ -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.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
import hashlib
|
|
||||||
|
|
||||||
from io import StringIO
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import threading
|
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.PluginRegistry import PluginRegistry
|
||||||
from UM.Mesh.MeshWriter import MeshWriter
|
from UM.Mesh.MeshWriter import MeshWriter
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
@ -28,7 +21,9 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
|
|||||||
|
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
from cura.CuraPackageManager import CuraPackageManager
|
from cura.CuraPackageManager import CuraPackageManager
|
||||||
|
from cura.Machines.Models.ExtrudersModel import ExtrudersModel
|
||||||
from cura.Settings import CuraContainerStack
|
from cura.Settings import CuraContainerStack
|
||||||
|
from cura.Settings.ExtruderStack import ExtruderStack
|
||||||
from cura.Utils.Threading import call_on_qt_thread
|
from cura.Utils.Threading import call_on_qt_thread
|
||||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
from cura.Snapshot import Snapshot
|
from cura.Snapshot import Snapshot
|
||||||
@ -54,21 +49,15 @@ import UM.Application
|
|||||||
|
|
||||||
from .SettingsExportModel import SettingsExportModel
|
from .SettingsExportModel import SettingsExportModel
|
||||||
from .SettingsExportGroup import SettingsExportGroup
|
from .SettingsExportGroup import SettingsExportGroup
|
||||||
|
from .ThreeMFVariant import ThreeMFVariant
|
||||||
|
from .Cura3mfVariant import Cura3mfVariant
|
||||||
|
from .BambuLabVariant import BambuLabVariant
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
MODEL_PATH = "3D/3dmodel.model"
|
MODEL_PATH = "3D/3dmodel.model"
|
||||||
PACKAGE_METADATA_PATH = "Cura/packages.json"
|
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):
|
class ThreeMFWriter(MeshWriter):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -85,6 +74,12 @@ class ThreeMFWriter(MeshWriter):
|
|||||||
self._store_archive = False
|
self._store_archive = False
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
|
# Register available variants
|
||||||
|
self._variants = {
|
||||||
|
Cura3mfVariant(self).mime_type: Cura3mfVariant,
|
||||||
|
BambuLabVariant(self).mime_type: BambuLabVariant
|
||||||
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _convertMatrixToString(matrix):
|
def _convertMatrixToString(matrix):
|
||||||
result = ""
|
result = ""
|
||||||
@ -218,10 +213,23 @@ class ThreeMFWriter(MeshWriter):
|
|||||||
|
|
||||||
painter.end()
|
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:
|
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode, export_settings_model = None, **kwargs) -> bool:
|
||||||
self._archive = None # Reset archive
|
self._archive = None # Reset archive
|
||||||
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
|
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:
|
try:
|
||||||
model_file = zipfile.ZipInfo(MODEL_PATH)
|
model_file = zipfile.ZipInfo(MODEL_PATH)
|
||||||
# Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo.
|
# 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
|
# Create Metadata/_rels/model_settings.config.rels
|
||||||
metadata_relations_element = self._makeRelationsTree()
|
metadata_relations_element = self._makeRelationsTree()
|
||||||
|
|
||||||
if add_extra_data:
|
# Let the variant add its specific files
|
||||||
self._storeGCode(archive, metadata_relations_element)
|
variant.add_extra_files(archive, metadata_relations_element)
|
||||||
self._storeModelSettings(archive)
|
|
||||||
self._storePlateDesc(archive)
|
# Let the variant prepare content types and relations
|
||||||
self._storeSliceInfo(archive)
|
variant.prepare_content_types(content_types)
|
||||||
|
variant.prepare_relations(relations_element)
|
||||||
|
|
||||||
# Attempt to add a thumbnail
|
# Attempt to add a thumbnail
|
||||||
snapshot = self._createSnapshot()
|
snapshot = self._createSnapshot()
|
||||||
@ -261,32 +270,8 @@ class ThreeMFWriter(MeshWriter):
|
|||||||
# Add PNG to content types file
|
# Add PNG to content types file
|
||||||
thumbnail_type = ET.SubElement(content_types, "Default", Extension="png", ContentType="image/png")
|
thumbnail_type = ET.SubElement(content_types, "Default", Extension="png", ContentType="image/png")
|
||||||
|
|
||||||
if add_extra_data:
|
# Let the variant process the thumbnail
|
||||||
archive.writestr(zipfile.ZipInfo(THUMBNAIL_PATH_MULTIPLATE), thumbnail_buffer.data())
|
variant.process_thumbnail(snapshot, thumbnail_buffer, archive, relations_element)
|
||||||
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")
|
|
||||||
|
|
||||||
# Write material metadata
|
# Write material metadata
|
||||||
packages_metadata = self._getMaterialPackageMetadata() + self._getPluginPackageMetadata()
|
packages_metadata = self._getMaterialPackageMetadata() + self._getPluginPackageMetadata()
|
||||||
@ -371,94 +356,6 @@ class ThreeMFWriter(MeshWriter):
|
|||||||
file.compress_type = zipfile.ZIP_DEFLATED
|
file.compress_type = zipfile.ZIP_DEFLATED
|
||||||
archive.writestr(file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(root_element))
|
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):
|
def _makeRelationsTree(self):
|
||||||
return ET.Element("Relationships", xmlns=self._namespaces["relationships"])
|
return ET.Element("Relationships", xmlns=self._namespaces["relationships"])
|
||||||
|
|
||||||
|
@ -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.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
import json
|
import json
|
||||||
import threading
|
import threading
|
||||||
@ -13,11 +13,14 @@ from UM.Message import Message
|
|||||||
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
|
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
|
||||||
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
|
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
from cura.ApplicationMetadata import CuraSDKVersion
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
|
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
|
||||||
|
import cura.UltimakerCloud.UltimakerCloudConstants as UltimakerCloudConstants
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
PACKAGES_URL = f"{UltimakerCloudConstants.CuraCloudAPIRoot}/cura-packages/v{UltimakerCloudConstants.CuraCloudAPIVersion}/cura/v{CuraSDKVersion}/packages"
|
||||||
|
|
||||||
class CreateBackupJob(Job):
|
class CreateBackupJob(Job):
|
||||||
"""Creates backup zip, requests upload url and uploads the backup file to cloud storage."""
|
"""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()
|
self._job_done = threading.Event()
|
||||||
"""Set when the job completes. Does not indicate success."""
|
"""Set when the job completes. Does not indicate success."""
|
||||||
self.backup_upload_error_message = ""
|
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:
|
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,
|
title = self.MESSAGE_TITLE,
|
||||||
progress = -1)
|
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()
|
CuraApplication.getInstance().processEvents()
|
||||||
cura_api = CuraApplication.getInstance().getCuraAPI()
|
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:
|
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.")
|
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
|
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()
|
CuraApplication.getInstance().processEvents()
|
||||||
|
|
||||||
# Create an upload entry for the backup.
|
# 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"])
|
backup_meta_data["description"] = "{}.backup.{}.cura.zip".format(timestamp, backup_meta_data["cura_release"])
|
||||||
self._requestUploadSlot(backup_meta_data, len(self._backup_zip))
|
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 == "":
|
if self.backup_upload_error_message == "":
|
||||||
upload_message.setText(catalog.i18nc("@info:backup_status", "Your backup has finished uploading."))
|
self._upload_message.setText(catalog.i18nc("@info:backup_status", "Your backup has finished uploading."))
|
||||||
upload_message.setProgress(None) # Hide progress bar
|
self._upload_message.setProgress(None) # Hide progress bar
|
||||||
else:
|
else:
|
||||||
# some error occurred. This error is presented to the user by DrivePluginExtension
|
# 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:
|
def _requestUploadSlot(self, backup_metadata: Dict[str, Any], backup_size: int) -> None:
|
||||||
"""Request a backup upload slot from the API.
|
"""Request a backup upload slot from the API.
|
||||||
@ -83,7 +122,6 @@ class CreateBackupJob(Job):
|
|||||||
"metadata": backup_metadata
|
"metadata": backup_metadata
|
||||||
}
|
}
|
||||||
}).encode()
|
}).encode()
|
||||||
|
|
||||||
HttpRequestManager.getInstance().put(
|
HttpRequestManager.getInstance().put(
|
||||||
self._api_backup_url,
|
self._api_backup_url,
|
||||||
data = payload,
|
data = payload,
|
||||||
|
@ -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.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
import threading
|
import threading
|
||||||
from tempfile import NamedTemporaryFile
|
from tempfile import NamedTemporaryFile
|
||||||
from typing import Optional, Any, Dict
|
from typing import Optional, Any, Dict
|
||||||
@ -12,9 +13,16 @@ from PyQt6.QtNetwork import QNetworkReply, QNetworkRequest
|
|||||||
from UM.Job import Job
|
from UM.Job import Job
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.PackageManager import catalog
|
from UM.PackageManager import catalog
|
||||||
|
from UM.Resources import Resources
|
||||||
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
|
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):
|
class RestoreBackupJob(Job):
|
||||||
"""Downloads a backup and overwrites local configuration with the backup.
|
"""Downloads a backup and overwrites local configuration with the backup.
|
||||||
@ -38,7 +46,6 @@ class RestoreBackupJob(Job):
|
|||||||
self.restore_backup_error_message = ""
|
self.restore_backup_error_message = ""
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
|
|
||||||
url = self._backup.get("download_url")
|
url = self._backup.get("download_url")
|
||||||
assert url is not None
|
assert url is not None
|
||||||
|
|
||||||
@ -48,7 +55,11 @@ class RestoreBackupJob(Job):
|
|||||||
error_callback = self._onRestoreRequestCompleted
|
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:
|
def _onRestoreRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None:
|
||||||
if not HttpRequestManager.replyIndicatesSuccess(reply, error):
|
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.
|
# We store the file in a temporary path fist to ensure integrity.
|
||||||
try:
|
try:
|
||||||
temporary_backup_file = NamedTemporaryFile(delete = False)
|
self._temporary_backup_file = NamedTemporaryFile(delete_on_close = False)
|
||||||
with open(temporary_backup_file.name, "wb") as write_backup:
|
with open(self._temporary_backup_file.name, "wb") as write_backup:
|
||||||
app = CuraApplication.getInstance()
|
app = CuraApplication.getInstance()
|
||||||
bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
|
bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
|
||||||
while bytes_read:
|
while bytes_read:
|
||||||
@ -69,23 +80,98 @@ class RestoreBackupJob(Job):
|
|||||||
bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
|
bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
|
||||||
app.processEvents()
|
app.processEvents()
|
||||||
except EnvironmentError as e:
|
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.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE
|
||||||
self._job_done.set()
|
self._job_done.set()
|
||||||
return
|
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.
|
# Don't restore the backup if the MD5 hashes do not match.
|
||||||
# This can happen if the download was interrupted.
|
# 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.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.
|
# 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 = 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
|
@staticmethod
|
||||||
def _verifyMd5Hash(file_path: str, known_hash: str) -> bool:
|
def _verifyMd5Hash(file_path: str, known_hash: str) -> bool:
|
||||||
|
@ -24,7 +24,7 @@ class GCodeGzWriter(MeshWriter):
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(add_to_recent_files = False)
|
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.
|
"""Writes the gzipped g-code to a stream.
|
||||||
|
|
||||||
Note that even though the function accepts a collection of nodes, the
|
Note that even though the function accepts a collection of nodes, the
|
||||||
|
@ -56,7 +56,7 @@ class GCodeWriter(MeshWriter):
|
|||||||
|
|
||||||
self._application = Application.getInstance()
|
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.
|
"""Writes the g-code for the entire scene to a stream.
|
||||||
|
|
||||||
Note that even though the function accepts a collection of nodes, the
|
Note that even though the function accepts a collection of nodes, the
|
||||||
|
@ -91,7 +91,7 @@ class MakerbotWriter(MeshWriter):
|
|||||||
|
|
||||||
return None
|
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)
|
metadata, file_format = self._getMeta(nodes)
|
||||||
if mode != MeshWriter.OutputMode.BinaryMode:
|
if mode != MeshWriter.OutputMode.BinaryMode:
|
||||||
Logger.log("e", "MakerbotWriter does not support text mode.")
|
Logger.log("e", "MakerbotWriter does not support text mode.")
|
||||||
|
@ -101,7 +101,8 @@ class RemovableDriveOutputDevice(OutputDevice):
|
|||||||
self._stream = open(file_name, "wt", buffering = 1, encoding = "utf-8")
|
self._stream = open(file_name, "wt", buffering = 1, encoding = "utf-8")
|
||||||
else: #Binary mode.
|
else: #Binary mode.
|
||||||
self._stream = open(file_name, "wb", buffering = 1)
|
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.setFileName(file_name)
|
||||||
job.progress.connect(self._onProgress)
|
job.progress.connect(self._onProgress)
|
||||||
job.finished.connect(self._onFinished)
|
job.finished.connect(self._onFinished)
|
||||||
|
@ -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
|
# Qt thread. The File read/write operations right now are executed on separated threads because they are scheduled
|
||||||
# by the Job class.
|
# by the Job class.
|
||||||
@call_on_qt_thread
|
@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 = VirtualFile()
|
||||||
archive.openStream(stream, "application/x-ufp", OpenMode.WriteOnly)
|
archive.openStream(stream, "application/x-ufp", OpenMode.WriteOnly)
|
||||||
|
|
||||||
|
53
resources/definitions/ultimaker_s6.def.json
Normal file
53
resources/definitions/ultimaker_s6.def.json
Normal 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" }
|
||||||
|
}
|
||||||
|
}
|
@ -47,7 +47,7 @@
|
|||||||
"overrides":
|
"overrides":
|
||||||
{
|
{
|
||||||
"default_material_print_temperature": { "maximum_value_warning": "320" },
|
"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" }
|
"material_print_temperature_layer_0": { "maximum_value_warning": "320" }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -385,7 +385,7 @@
|
|||||||
"unit": "m/s\u00b3",
|
"unit": "m/s\u00b3",
|
||||||
"value": "20000 if machine_gcode_flavor == 'Cheetah' else 100"
|
"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_cool_down_speed": { "default_value": 1.3 },
|
||||||
"machine_nozzle_heat_up_speed": { "default_value": 0.6 },
|
"machine_nozzle_heat_up_speed": { "default_value": 0.6 },
|
||||||
"machine_start_gcode": { "default_value": "M213 U0.1 ;undercut 0.1mm" },
|
"machine_start_gcode": { "default_value": "M213 U0.1 ;undercut 0.1mm" },
|
||||||
@ -412,7 +412,7 @@
|
|||||||
"retraction_hop": { "value": 1 },
|
"retraction_hop": { "value": 1 },
|
||||||
"retraction_hop_after_extruder_switch_height": { "value": 2 },
|
"retraction_hop_after_extruder_switch_height": { "value": 2 },
|
||||||
"retraction_hop_enabled": { "value": true },
|
"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 },
|
"retraction_prime_speed": { "value": 15 },
|
||||||
"skin_edge_support_thickness": { "value": 0 },
|
"skin_edge_support_thickness": { "value": 0 },
|
||||||
"skin_material_flow": { "value": 95 },
|
"skin_material_flow": { "value": 95 },
|
||||||
|
31
resources/extruders/ultimaker_s6_extruder_left.def.json
Normal file
31
resources/extruders/ultimaker_s6_extruder_left.def.json
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
31
resources/extruders/ultimaker_s6_extruder_right.def.json
Normal file
31
resources/extruders/ultimaker_s6_extruder_right.def.json
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
BIN
resources/images/UltimakerS6backplate.png
Normal file
BIN
resources/images/UltimakerS6backplate.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
@ -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
|
||||||
|
|
@ -5,11 +5,11 @@ version = 4
|
|||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
intent_category = engineering
|
intent_category = engineering
|
||||||
material = generic_nylon
|
material = generic_petcf
|
||||||
quality_type = draft
|
quality_type = draft
|
||||||
setting_version = 25
|
setting_version = 25
|
||||||
type = intent
|
type = intent
|
||||||
variant = AA+ 0.4
|
variant = CC+ 0.6
|
||||||
|
|
||||||
[values]
|
[values]
|
||||||
infill_sparse_density = 20
|
infill_sparse_density = 20
|
@ -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
|
Item
|
||||||
{
|
{
|
||||||
//: Spacer
|
//: Spacer
|
||||||
@ -705,7 +694,7 @@ UM.PreferencesPage
|
|||||||
UM.CheckBox
|
UM.CheckBox
|
||||||
{
|
{
|
||||||
id: singleInstanceCheckbox
|
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"))
|
checked: boolCheck(UM.Preferences.getValue("cura/single_instance"))
|
||||||
onCheckedChanged: UM.Preferences.setValue("cura/single_instance", checked)
|
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
|
Item
|
||||||
{
|
{
|
||||||
//: Spacer
|
//: Spacer
|
||||||
@ -1110,6 +1097,18 @@ UM.PreferencesPage
|
|||||||
width: UM.Theme.getSize("default_margin").height
|
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
|
Label
|
||||||
{
|
{
|
||||||
font.bold: true
|
font.bold: true
|
||||||
|
@ -15,7 +15,6 @@ weight = -2
|
|||||||
cool_min_layer_time = 4
|
cool_min_layer_time = 4
|
||||||
cool_min_layer_time_fan_speed_max = 9
|
cool_min_layer_time_fan_speed_max = 9
|
||||||
cool_min_temperature = =material_print_temperature - 10
|
cool_min_temperature = =material_print_temperature - 10
|
||||||
material_print_temperature = =default_material_print_temperature + 5
|
|
||||||
retraction_prime_speed = 15
|
retraction_prime_speed = 15
|
||||||
support_structure = tree
|
support_structure = tree
|
||||||
|
|
||||||
|
@ -13,8 +13,10 @@ weight = -2
|
|||||||
|
|
||||||
[values]
|
[values]
|
||||||
infill_overlap = 20
|
infill_overlap = 20
|
||||||
infill_pattern = ='zigzag' if infill_sparse_density > 80 else 'gyroid'
|
infill_pattern = lines
|
||||||
speed_print = 100
|
speed_print = 40
|
||||||
speed_wall_0 = =speed_print
|
speed_wall = =speed_print
|
||||||
|
speed_wall_0 = =speed_wall
|
||||||
support_interface_enable = True
|
support_interface_enable = True
|
||||||
|
wall_thickness = =wall_line_width_0 + 2*wall_line_width_x
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -13,17 +13,23 @@ weight = -1
|
|||||||
|
|
||||||
[values]
|
[values]
|
||||||
acceleration_prime_tower = 1500
|
acceleration_prime_tower = 1500
|
||||||
|
acceleration_support = 1500
|
||||||
brim_replaces_support = False
|
brim_replaces_support = False
|
||||||
build_volume_temperature = =70 if extruders_enabled_count > 1 else 35
|
build_volume_temperature = =70 if extruders_enabled_count > 1 else 35
|
||||||
cool_fan_enabled = =not (support_enable and (extruder_nr == support_infill_extruder_nr))
|
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
|
default_material_bed_temperature = =0 if extruders_enabled_count > 1 else 60
|
||||||
initial_layer_line_width_factor = 150
|
initial_layer_line_width_factor = 150
|
||||||
|
jerk_prime_tower = 4000
|
||||||
|
jerk_support = 4000
|
||||||
minimum_support_area = 4
|
minimum_support_area = 4
|
||||||
|
retraction_amount = 6.5
|
||||||
retraction_count_max = 5
|
retraction_count_max = 5
|
||||||
skirt_brim_minimal_length = =min(2000, 175 / (layer_height * line_width))
|
skirt_brim_minimal_length = =min(2000, 175 / (layer_height * line_width))
|
||||||
speed_prime_tower = 25
|
speed_prime_tower = 50
|
||||||
speed_support = 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_infill_sparse_thickness = =2 * layer_height
|
||||||
support_interface_enable = True
|
support_interface_enable = True
|
||||||
support_z_distance = 0
|
support_z_distance = 0
|
||||||
|
@ -13,18 +13,24 @@ weight = 0
|
|||||||
|
|
||||||
[values]
|
[values]
|
||||||
acceleration_prime_tower = 1500
|
acceleration_prime_tower = 1500
|
||||||
|
acceleration_support = 1500
|
||||||
brim_replaces_support = False
|
brim_replaces_support = False
|
||||||
build_volume_temperature = =70 if extruders_enabled_count > 1 else 35
|
build_volume_temperature = =70 if extruders_enabled_count > 1 else 35
|
||||||
cool_fan_enabled = =not (support_enable and (extruder_nr == support_infill_extruder_nr))
|
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
|
default_material_bed_temperature = =0 if extruders_enabled_count > 1 else 60
|
||||||
initial_layer_line_width_factor = 150
|
initial_layer_line_width_factor = 150
|
||||||
|
jerk_prime_tower = 4000
|
||||||
|
jerk_support = 4000
|
||||||
material_print_temperature = =default_material_print_temperature - 5
|
material_print_temperature = =default_material_print_temperature - 5
|
||||||
minimum_support_area = 4
|
minimum_support_area = 4
|
||||||
|
retraction_amount = 6.5
|
||||||
retraction_count_max = 5
|
retraction_count_max = 5
|
||||||
skirt_brim_minimal_length = =min(2000, 175 / (layer_height * line_width))
|
skirt_brim_minimal_length = =min(2000, 175 / (layer_height * line_width))
|
||||||
speed_prime_tower = 25
|
speed_prime_tower = 50
|
||||||
speed_support = 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_infill_sparse_thickness = =2 * layer_height
|
||||||
support_interface_enable = True
|
support_interface_enable = True
|
||||||
support_z_distance = 0
|
support_z_distance = 0
|
||||||
|
@ -13,18 +13,24 @@ weight = -2
|
|||||||
|
|
||||||
[values]
|
[values]
|
||||||
acceleration_prime_tower = 1500
|
acceleration_prime_tower = 1500
|
||||||
|
acceleration_support = 1500
|
||||||
brim_replaces_support = False
|
brim_replaces_support = False
|
||||||
build_volume_temperature = =70 if extruders_enabled_count > 1 else 35
|
build_volume_temperature = =70 if extruders_enabled_count > 1 else 35
|
||||||
cool_fan_enabled = =not (support_enable and (extruder_nr == support_infill_extruder_nr))
|
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
|
default_material_bed_temperature = =0 if extruders_enabled_count > 1 else 60
|
||||||
initial_layer_line_width_factor = 150
|
initial_layer_line_width_factor = 150
|
||||||
|
jerk_prime_tower = 4000
|
||||||
|
jerk_support = 4000
|
||||||
material_print_temperature = =default_material_print_temperature + 5
|
material_print_temperature = =default_material_print_temperature + 5
|
||||||
minimum_support_area = 4
|
minimum_support_area = 4
|
||||||
|
retraction_amount = 6.5
|
||||||
retraction_count_max = 5
|
retraction_count_max = 5
|
||||||
skirt_brim_minimal_length = =min(2000, 175 / (layer_height * line_width))
|
skirt_brim_minimal_length = =min(2000, 175 / (layer_height * line_width))
|
||||||
speed_prime_tower = 25
|
speed_prime_tower = 50
|
||||||
speed_support = 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_interface_enable = True
|
||||||
support_z_distance = 0
|
support_z_distance = 0
|
||||||
|
|
||||||
|
@ -13,18 +13,24 @@ weight = -3
|
|||||||
|
|
||||||
[values]
|
[values]
|
||||||
acceleration_prime_tower = 1500
|
acceleration_prime_tower = 1500
|
||||||
|
acceleration_support = 1500
|
||||||
brim_replaces_support = False
|
brim_replaces_support = False
|
||||||
build_volume_temperature = =70 if extruders_enabled_count > 1 else 35
|
build_volume_temperature = =70 if extruders_enabled_count > 1 else 35
|
||||||
cool_fan_enabled = =not (support_enable and (extruder_nr == support_infill_extruder_nr))
|
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
|
default_material_bed_temperature = =0 if extruders_enabled_count > 1 else 60
|
||||||
initial_layer_line_width_factor = 150
|
initial_layer_line_width_factor = 150
|
||||||
|
jerk_prime_tower = 4000
|
||||||
|
jerk_support = 4000
|
||||||
material_print_temperature = =default_material_print_temperature - 5
|
material_print_temperature = =default_material_print_temperature - 5
|
||||||
minimum_support_area = 4
|
minimum_support_area = 4
|
||||||
|
retraction_amount = 6.5
|
||||||
retraction_count_max = 5
|
retraction_count_max = 5
|
||||||
skirt_brim_minimal_length = =min(2000, 175 / (layer_height * line_width))
|
skirt_brim_minimal_length = =min(2000, 175 / (layer_height * line_width))
|
||||||
speed_prime_tower = 25
|
speed_prime_tower = 50
|
||||||
speed_support = 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_infill_sparse_thickness = 0.3
|
||||||
support_interface_enable = True
|
support_interface_enable = True
|
||||||
support_z_distance = 0
|
support_z_distance = 0
|
||||||
|
@ -14,6 +14,8 @@ weight = -2
|
|||||||
[values]
|
[values]
|
||||||
cool_min_layer_time = 6
|
cool_min_layer_time = 6
|
||||||
cool_min_layer_time_fan_speed_max = 12
|
cool_min_layer_time_fan_speed_max = 12
|
||||||
retraction_amount = 8
|
inset_direction = inside_out
|
||||||
|
material_flow = 95
|
||||||
retraction_prime_speed = 15
|
retraction_prime_speed = 15
|
||||||
|
speed_wall_x = =speed_wall_0
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
17
resources/variants/ultimaker_s6_aa_plus04.inst.cfg
Normal file
17
resources/variants/ultimaker_s6_aa_plus04.inst.cfg
Normal 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
|
||||||
|
|
17
resources/variants/ultimaker_s6_aa_plus06.inst.cfg
Normal file
17
resources/variants/ultimaker_s6_aa_plus06.inst.cfg
Normal 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
|
||||||
|
|
19
resources/variants/ultimaker_s6_bb04.inst.cfg
Normal file
19
resources/variants/ultimaker_s6_bb04.inst.cfg
Normal 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
|
||||||
|
|
17
resources/variants/ultimaker_s6_cc_plus04.inst.cfg
Normal file
17
resources/variants/ultimaker_s6_cc_plus04.inst.cfg
Normal 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
|
||||||
|
|
17
resources/variants/ultimaker_s6_cc_plus06.inst.cfg
Normal file
17
resources/variants/ultimaker_s6_cc_plus06.inst.cfg
Normal 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
|
||||||
|
|
17
resources/variants/ultimaker_s6_dd04.inst.cfg
Normal file
17
resources/variants/ultimaker_s6_dd04.inst.cfg
Normal 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
|
||||||
|
|
17
resources/variants/ultimaker_s8_aa_plus06.inst.cfg
Normal file
17
resources/variants/ultimaker_s8_aa_plus06.inst.cfg
Normal 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
|
||||||
|
|
17
resources/variants/ultimaker_s8_cc_plus06.inst.cfg
Normal file
17
resources/variants/ultimaker_s8_cc_plus06.inst.cfg
Normal 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
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user