Merge branch 'main' into Cura_ankermake_august

This commit is contained in:
Saumya Jain 2024-03-08 10:41:16 +01:00 committed by GitHub
commit c3fa20c751
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
311 changed files with 13326 additions and 870 deletions

View file

@ -4,7 +4,7 @@ labels: ["Type: Bug", "Status: Triage", "Slicing Error :collision:"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
### 💥 Slicing Crash Analysis Tool 💥 ### 💥 Slicing Crash Analysis Tool 💥
We are taking steps to analyze an increase in reported crashes more systematically. We'll need some help with that. 😇 We are taking steps to analyze an increase in reported crashes more systematically. We'll need some help with that. 😇
Before filling out the report below, we want you to try a special Cura 5.7 Alpha. Before filling out the report below, we want you to try a special Cura 5.7 Alpha.

View file

@ -55,7 +55,7 @@ jobs:
needs: [ conan-recipe-version ] needs: [ conan-recipe-version ]
with: with:
recipe_id_full: ${{ needs.conan-recipe-version.outputs.recipe_id_full }} recipe_id_full: ${{ needs.conan-recipe-version.outputs.recipe_id_full }}
conan_extra_args: '-g VirtualPythonEnv -o cura:devtools=True -c tools.build:skip_test=False' conan_extra_args: '-g VirtualPythonEnv -o cura:devtools=True -c tools.build:skip_test=False --options "*:enable_sentry=False"'
unit_test_cmd: 'pytest --junitxml=junit_cura.xml' unit_test_cmd: 'pytest --junitxml=junit_cura.xml'
unit_test_dir: 'tests' unit_test_dir: 'tests'
conan_generator_dir: './venv/bin' conan_generator_dir: './venv/bin'

View file

@ -55,7 +55,8 @@ exe = EXE(
target_arch={{ target_arch }}, target_arch={{ target_arch }},
codesign_identity=os.getenv('CODESIGN_IDENTITY', None), codesign_identity=os.getenv('CODESIGN_IDENTITY', None),
entitlements_file={{ entitlements_file }}, entitlements_file={{ entitlements_file }},
icon={{ icon }} icon={{ icon }},
contents_directory='.'
) )
coll = COLLECT( coll = COLLECT(
@ -70,188 +71,7 @@ coll = COLLECT(
) )
{% if macos == true %} {% if macos == true %}
# PyInstaller seems to copy everything in the resource folder for the MacOS, this causes issues with codesigning and notarizing app = BUNDLE(
# The folder structure should adhere to the one specified in Table 2-5
# https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html#//apple_ref/doc/uid/10000123i-CH101-SW1
# The class below is basically ducktyping the BUNDLE class of PyInstaller and using our own `assemble` method for more fine-grain and specific
# control. Some code of the method below is copied from:
# https://github.com/pyinstaller/pyinstaller/blob/22d1d2a5378228744cc95f14904dae1664df32c4/PyInstaller/building/osx.py#L115
#-----------------------------------------------------------------------------
# Copyright (c) 2005-2022, PyInstaller Development Team.
#
# Distributed under the terms of the GNU General Public License (version 2
# or later) with exception for distributing the bootloader.
#
# The full license is in the file COPYING.txt, distributed with this software.
#
# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
#-----------------------------------------------------------------------------
import plistlib
import shutil
import PyInstaller.utils.osx as osxutils
from pathlib import Path
from PyInstaller.building.osx import BUNDLE
from PyInstaller.building.utils import (_check_path_overlap, _rmtree, add_suffix_to_extension, checkCache)
from PyInstaller.building.datastruct import logger
from PyInstaller.building.icon import normalize_icon_type
class UMBUNDLE(BUNDLE):
def assemble(self):
from PyInstaller.config import CONF
if _check_path_overlap(self.name) and os.path.isdir(self.name):
_rmtree(self.name)
logger.info("Building BUNDLE %s", self.tocbasename)
# Create a minimal Mac bundle structure.
macos_path = Path(self.name, "Contents", "MacOS")
resources_path = Path(self.name, "Contents", "Resources")
frameworks_path = Path(self.name, "Contents", "Frameworks")
os.makedirs(macos_path)
os.makedirs(resources_path)
os.makedirs(frameworks_path)
# Makes sure the icon exists and attempts to convert to the proper format if applicable
self.icon = normalize_icon_type(self.icon, ("icns",), "icns", CONF["workpath"])
# Ensure icon path is absolute
self.icon = os.path.abspath(self.icon)
# Copy icns icon to Resources directory.
shutil.copy(self.icon, os.path.join(self.name, 'Contents', 'Resources'))
# Key/values for a minimal Info.plist file
info_plist_dict = {
"CFBundleDisplayName": self.appname,
"CFBundleName": self.appname,
# Required by 'codesign' utility.
# The value for CFBundleIdentifier is used as the default unique name of your program for Code Signing
# purposes. It even identifies the APP for access to restricted OS X areas like Keychain.
#
# The identifier used for signing must be globally unique. The usual form for this identifier is a
# hierarchical name in reverse DNS notation, starting with the toplevel domain, followed by the company
# name, followed by the department within the company, and ending with the product name. Usually in the
# form: com.mycompany.department.appname
# CLI option --osx-bundle-identifier sets this value.
"CFBundleIdentifier": self.bundle_identifier,
"CFBundleExecutable": os.path.basename(self.exename),
"CFBundleIconFile": os.path.basename(self.icon),
"CFBundleInfoDictionaryVersion": "6.0",
"CFBundlePackageType": "APPL",
"CFBundleVersionString": self.version,
"CFBundleShortVersionString": self.version,
}
# Set some default values. But they still can be overwritten by the user.
if self.console:
# Setting EXE console=True implies LSBackgroundOnly=True.
info_plist_dict['LSBackgroundOnly'] = True
else:
# Let's use high resolution by default.
info_plist_dict['NSHighResolutionCapable'] = True
# Merge info_plist settings from spec file
if isinstance(self.info_plist, dict) and self.info_plist:
info_plist_dict.update(self.info_plist)
plist_filename = os.path.join(self.name, "Contents", "Info.plist")
with open(plist_filename, "wb") as plist_fh:
plistlib.dump(info_plist_dict, plist_fh)
links = []
_QT_BASE_PATH = {'PySide2', 'PySide6', 'PyQt5', 'PyQt6', 'PySide6'}
for inm, fnm, typ in self.toc:
# Adjust name for extensions, if applicable
inm, fnm, typ = add_suffix_to_extension(inm, fnm, typ)
inm = Path(inm)
fnm = Path(fnm)
# Copy files from cache. This ensures that are used files with relative paths to dynamic library
# dependencies (@executable_path)
if typ in ('EXTENSION', 'BINARY') or (typ == 'DATA' and inm.suffix == '.so'):
if any(['.' in p for p in inm.parent.parts]):
inm = Path(inm.name)
fnm = Path(checkCache(
str(fnm),
strip = self.strip,
upx = self.upx,
upx_exclude = self.upx_exclude,
dist_nm = str(inm),
target_arch = self.target_arch,
codesign_identity = self.codesign_identity,
entitlements_file = self.entitlements_file,
strict_arch_validation = (typ == 'EXTENSION'),
))
frame_dst = frameworks_path.joinpath(inm)
if not frame_dst.exists():
if frame_dst.is_dir():
os.makedirs(frame_dst, exist_ok = True)
else:
os.makedirs(frame_dst.parent, exist_ok = True)
shutil.copy(fnm, frame_dst, follow_symlinks = True)
macos_dst = macos_path.joinpath(inm)
if not macos_dst.exists():
if macos_dst.is_dir():
os.makedirs(macos_dst, exist_ok = True)
else:
os.makedirs(macos_dst.parent, exist_ok = True)
# Create relative symlink to the framework
symlink_to = Path(*[".." for p in macos_dst.relative_to(macos_path).parts], "Frameworks").joinpath(
frame_dst.relative_to(frameworks_path))
try:
macos_dst.symlink_to(symlink_to)
except FileExistsError:
pass
else:
if typ == 'DATA':
if any(['.' in p for p in inm.parent.parts]) or inm.suffix == '.so':
# Skip info dist egg and some not needed folders in tcl and tk, since they all contain dots in their files
logger.warning(f"Skipping DATA file {inm}")
continue
res_dst = resources_path.joinpath(inm)
if not res_dst.exists():
if res_dst.is_dir():
os.makedirs(res_dst, exist_ok = True)
else:
os.makedirs(res_dst.parent, exist_ok = True)
shutil.copy(fnm, res_dst, follow_symlinks = True)
macos_dst = macos_path.joinpath(inm)
if not macos_dst.exists():
if macos_dst.is_dir():
os.makedirs(macos_dst, exist_ok = True)
else:
os.makedirs(macos_dst.parent, exist_ok = True)
# Create relative symlink to the resource
symlink_to = Path(*[".." for p in macos_dst.relative_to(macos_path).parts], "Resources").joinpath(
res_dst.relative_to(resources_path))
try:
macos_dst.symlink_to(symlink_to)
except FileExistsError:
pass
else:
macos_dst = macos_path.joinpath(inm)
if not macos_dst.exists():
if macos_dst.is_dir():
os.makedirs(macos_dst, exist_ok = True)
else:
os.makedirs(macos_dst.parent, exist_ok = True)
shutil.copy(fnm, macos_dst, follow_symlinks = True)
# Sign the bundle
logger.info('Signing the BUNDLE...')
try:
osxutils.sign_binary(self.name, self.codesign_identity, self.entitlements_file, deep = True)
except Exception as e:
logger.warning(f"Error while signing the bundle: {e}")
logger.warning("You will need to sign the bundle manually!")
logger.info(f"Building BUNDLE {self.tocbasename} completed successfully.")
app = UMBUNDLE(
coll, coll,
name='{{ display_name }}.app', name='{{ display_name }}.app',
icon={{ icon }}, icon={{ icon }},
@ -271,9 +91,10 @@ app = UMBUNDLE(
'CFBundleURLSchemes': ['cura', 'slicer'], 'CFBundleURLSchemes': ['cura', 'slicer'],
}], }],
'CFBundleDocumentTypes': [{ 'CFBundleDocumentTypes': [{
'CFBundleTypeRole': 'Viewer', 'CFBundleTypeRole': 'Viewer',
'CFBundleTypeExtensions': ['*'], 'CFBundleTypeExtensions': ['stl', 'obj', '3mf', 'gcode', 'ufp'],
'CFBundleTypeName': 'Model Files', 'CFBundleTypeName': 'Model Files',
}] }]
}, },
){% endif %} )
{% endif %}

View file

@ -4,7 +4,7 @@ requirements:
- "curaengine/(latest)@ultimaker/testing" - "curaengine/(latest)@ultimaker/testing"
- "cura_binary_data/(latest)@ultimaker/testing" - "cura_binary_data/(latest)@ultimaker/testing"
- "fdm_materials/(latest)@ultimaker/testing" - "fdm_materials/(latest)@ultimaker/testing"
- "curaengine_plugin_gradual_flow/0.1.0-beta.2" - "curaengine_plugin_gradual_flow/0.1.0-beta.3"
- "dulcificum/latest@ultimaker/testing" - "dulcificum/latest@ultimaker/testing"
- "pysavitar/5.3.0" - "pysavitar/5.3.0"
- "pynest2d/5.3.0" - "pynest2d/5.3.0"
@ -118,7 +118,6 @@ pyinstaller:
- "sqlite3" - "sqlite3"
- "trimesh" - "trimesh"
- "win32ctypes" - "win32ctypes"
- "PyQt6"
- "PyQt6.QtNetwork" - "PyQt6.QtNetwork"
- "PyQt6.sip" - "PyQt6.sip"
- "stl" - "stl"
@ -160,6 +159,10 @@ pycharm_targets:
module_name: Cura module_name: Cura
name: pytest in TestGCodeListDecorator.py name: pytest in TestGCodeListDecorator.py
script_name: tests/TestGCodeListDecorator.py script_name: tests/TestGCodeListDecorator.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestHitChecker.py
script_name: tests/TestHitChecker.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja - jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura module_name: Cura
name: pytest in TestIntentManager.py name: pytest in TestIntentManager.py
@ -188,6 +191,10 @@ pycharm_targets:
module_name: Cura module_name: Cura
name: pytest in TestPrintInformation.py name: pytest in TestPrintInformation.py
script_name: tests/TestPrintInformation.py script_name: tests/TestPrintInformation.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestPrintOrderManager.py
script_name: tests/TestPrintOrderManager.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja - jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura module_name: Cura
name: pytest in TestProfileRequirements.py name: pytest in TestProfileRequirements.py

View file

@ -350,6 +350,7 @@ class CuraConan(ConanFile):
self.requires("cpython/3.10.4@ultimaker/stable") self.requires("cpython/3.10.4@ultimaker/stable")
self.requires("clipper/6.4.2@ultimaker/stable") self.requires("clipper/6.4.2@ultimaker/stable")
self.requires("openssl/3.2.0") self.requires("openssl/3.2.0")
self.requires("protobuf/3.21.12")
self.requires("boost/1.82.0") self.requires("boost/1.82.0")
self.requires("spdlog/1.12.0") self.requires("spdlog/1.12.0")
self.requires("fmt/10.1.1") self.requires("fmt/10.1.1")

View file

@ -1,6 +1,5 @@
# Copyright (c) 2023 UltiMaker # Copyright (c) 2023 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 List, cast from typing import List, cast
from PyQt6.QtCore import QObject, QUrl, pyqtSignal, pyqtProperty from PyQt6.QtCore import QObject, QUrl, pyqtSignal, pyqtProperty
@ -33,7 +32,6 @@ from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOper
from UM.Logger import Logger from UM.Logger import Logger
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
class CuraActions(QObject): class CuraActions(QObject):
def __init__(self, parent: QObject = None) -> None: def __init__(self, parent: QObject = None) -> None:
super().__init__(parent) super().__init__(parent)
@ -273,7 +271,11 @@ class CuraActions(QObject):
# deselect currently selected nodes, and select the new nodes # deselect currently selected nodes, and select the new nodes
for node in Selection.getAllSelectedObjects(): for node in Selection.getAllSelectedObjects():
Selection.remove(node) Selection.remove(node)
numberOfFixedNodes = len(fixed_nodes)
for node in nodes: for node in nodes:
numberOfFixedNodes += 1
node.printOrder = numberOfFixedNodes
Selection.add(node) Selection.add(node)
def _openUrl(self, url: QUrl) -> None: def _openUrl(self, url: QUrl) -> None:

View file

@ -104,7 +104,8 @@ from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager
from cura.UI import CuraSplashScreen, MachineActionManager, PrintInformation from cura.UI import CuraSplashScreen, PrintInformation
from cura.UI.MachineActionManager import MachineActionManager
from cura.UI.AddPrinterPagesModel import AddPrinterPagesModel from cura.UI.AddPrinterPagesModel import AddPrinterPagesModel
from cura.UI.MachineSettingsManager import MachineSettingsManager from cura.UI.MachineSettingsManager import MachineSettingsManager
from cura.UI.ObjectsModel import ObjectsModel from cura.UI.ObjectsModel import ObjectsModel
@ -125,6 +126,7 @@ from .Machines.Models.CompatibleMachineModel import CompatibleMachineModel
from .Machines.Models.MachineListModel import MachineListModel from .Machines.Models.MachineListModel import MachineListModel
from .Machines.Models.ActiveIntentQualitiesModel import ActiveIntentQualitiesModel from .Machines.Models.ActiveIntentQualitiesModel import ActiveIntentQualitiesModel
from .Machines.Models.IntentSelectionModel import IntentSelectionModel from .Machines.Models.IntentSelectionModel import IntentSelectionModel
from .PrintOrderManager import PrintOrderManager
from .SingleInstance import SingleInstance from .SingleInstance import SingleInstance
if TYPE_CHECKING: if TYPE_CHECKING:
@ -179,6 +181,7 @@ class CuraApplication(QtApplication):
# Variables set from CLI # Variables set from CLI
self._files_to_open = [] self._files_to_open = []
self._urls_to_open = []
self._use_single_instance = False self._use_single_instance = False
self._single_instance = None self._single_instance = None
@ -186,7 +189,7 @@ class CuraApplication(QtApplication):
self._cura_formula_functions = None # type: Optional[CuraFormulaFunctions] self._cura_formula_functions = None # type: Optional[CuraFormulaFunctions]
self._machine_action_manager = None # type: Optional[MachineActionManager.MachineActionManager] self._machine_action_manager: Optional[MachineActionManager] = None
self.empty_container = None # type: EmptyInstanceContainer self.empty_container = None # type: EmptyInstanceContainer
self.empty_definition_changes_container = None # type: EmptyInstanceContainer self.empty_definition_changes_container = None # type: EmptyInstanceContainer
@ -202,6 +205,7 @@ class CuraApplication(QtApplication):
self._container_manager = None self._container_manager = None
self._object_manager = None self._object_manager = None
self._print_order_manager = None
self._extruders_model = None self._extruders_model = None
self._extruders_model_with_optional = None self._extruders_model_with_optional = None
self._build_plate_model = None self._build_plate_model = None
@ -333,7 +337,7 @@ class CuraApplication(QtApplication):
for filename in self._cli_args.file: for filename in self._cli_args.file:
url = QUrl(filename) url = QUrl(filename)
if url.scheme() in self._supported_url_schemes: if url.scheme() in self._supported_url_schemes:
self._open_url_queue.append(url) self._urls_to_open.append(url)
else: else:
self._files_to_open.append(os.path.abspath(filename)) self._files_to_open.append(os.path.abspath(filename))
@ -352,11 +356,11 @@ class CuraApplication(QtApplication):
self.__addAllEmptyContainers() self.__addAllEmptyContainers()
self.__setLatestResouceVersionsForVersionUpgrade() self.__setLatestResouceVersionsForVersionUpgrade()
self._machine_action_manager = MachineActionManager.MachineActionManager(self) self._machine_action_manager = MachineActionManager(self)
self._machine_action_manager.initialize() self._machine_action_manager.initialize()
def __sendCommandToSingleInstance(self): def __sendCommandToSingleInstance(self):
self._single_instance = SingleInstance(self, self._files_to_open) self._single_instance = SingleInstance(self, self._files_to_open, self._urls_to_open)
# If we use single instance, try to connect to the single instance server, send commands, and then exit. # If we use single instance, try to connect to the single instance server, send commands, and then exit.
# If we cannot find an existing single instance server, this is the only instance, so just keep going. # If we cannot find an existing single instance server, this is the only instance, so just keep going.
@ -373,9 +377,15 @@ class CuraApplication(QtApplication):
Resources.addExpectedDirNameInData(dir_name) Resources.addExpectedDirNameInData(dir_name)
app_root = os.path.abspath(os.path.join(os.path.dirname(sys.executable))) app_root = os.path.abspath(os.path.join(os.path.dirname(sys.executable)))
Resources.addSecureSearchPath(os.path.join(app_root, "share", "cura", "resources"))
Resources.addSecureSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources")) if platform.system() == "Darwin":
Resources.addSecureSearchPath(os.path.join(app_root, "Resources", "share", "cura", "resources"))
Resources.addSecureSearchPath(
os.path.join(self._app_install_dir, "Resources", "share", "cura", "resources"))
else:
Resources.addSecureSearchPath(os.path.join(app_root, "share", "cura", "resources"))
Resources.addSecureSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources"))
if not hasattr(sys, "frozen"): if not hasattr(sys, "frozen"):
cura_data_root = os.environ.get('CURA_DATA_ROOT', None) cura_data_root = os.environ.get('CURA_DATA_ROOT', None)
if cura_data_root: if cura_data_root:
@ -607,6 +617,7 @@ class CuraApplication(QtApplication):
preferences.addPreference("view/invert_zoom", False) preferences.addPreference("view/invert_zoom", False)
preferences.addPreference("view/filter_current_build_plate", False) preferences.addPreference("view/filter_current_build_plate", False)
preferences.addPreference("view/navigation_style", "cura")
preferences.addPreference("cura/sidebar_collapsed", False) preferences.addPreference("cura/sidebar_collapsed", False)
preferences.addPreference("cura/favorite_materials", "") preferences.addPreference("cura/favorite_materials", "")
@ -899,6 +910,7 @@ class CuraApplication(QtApplication):
# initialize info objects # initialize info objects
self._print_information = PrintInformation.PrintInformation(self) self._print_information = PrintInformation.PrintInformation(self)
self._cura_actions = CuraActions.CuraActions(self) self._cura_actions = CuraActions.CuraActions(self)
self._print_order_manager = PrintOrderManager(self.getObjectsModel().getNodes)
self.processEvents() self.processEvents()
# Initialize setting visibility presets model. # Initialize setting visibility presets model.
self._setting_visibility_presets_model = SettingVisibilityPresetsModel(self.getPreferences(), parent = self) self._setting_visibility_presets_model = SettingVisibilityPresetsModel(self.getPreferences(), parent = self)
@ -956,6 +968,8 @@ class CuraApplication(QtApplication):
self.callLater(self._openFile, file_name) self.callLater(self._openFile, file_name)
for file_name in self._open_file_queue: # Open all the files that were queued up while plug-ins were loading. for file_name in self._open_file_queue: # Open all the files that were queued up while plug-ins were loading.
self.callLater(self._openFile, file_name) self.callLater(self._openFile, file_name)
for url in self._urls_to_open:
self.callLater(self._openUrl, url)
for url in self._open_url_queue: for url in self._open_url_queue:
self.callLater(self._openUrl, url) self.callLater(self._openUrl, url)
@ -979,6 +993,7 @@ class CuraApplication(QtApplication):
t.setEnabledAxis([ToolHandle.XAxis, ToolHandle.YAxis, ToolHandle.ZAxis]) t.setEnabledAxis([ToolHandle.XAxis, ToolHandle.YAxis, ToolHandle.ZAxis])
Selection.selectionChanged.connect(self.onSelectionChanged) Selection.selectionChanged.connect(self.onSelectionChanged)
self._print_order_manager.printOrderChanged.connect(self._onPrintOrderChanged)
# Set default background color for scene # Set default background color for scene
self.getRenderer().setBackgroundColor(QColor(245, 245, 245)) self.getRenderer().setBackgroundColor(QColor(245, 245, 245))
@ -1068,6 +1083,10 @@ class CuraApplication(QtApplication):
def getTextManager(self, *args) -> "TextManager": def getTextManager(self, *args) -> "TextManager":
return self._text_manager return self._text_manager
@pyqtSlot()
def setWorkplaceDropToBuildplate(self):
return self._physics.setAppAllModelDropDown()
def getCuraFormulaFunctions(self, *args) -> "CuraFormulaFunctions": def getCuraFormulaFunctions(self, *args) -> "CuraFormulaFunctions":
if self._cura_formula_functions is None: if self._cura_formula_functions is None:
self._cura_formula_functions = CuraFormulaFunctions(self) self._cura_formula_functions = CuraFormulaFunctions(self)
@ -1094,6 +1113,10 @@ class CuraApplication(QtApplication):
self._object_manager = ObjectsModel(self) self._object_manager = ObjectsModel(self)
return self._object_manager return self._object_manager
@pyqtSlot(str, result = "QVariantList")
def getSupportedActionMachineList(self, definition_id: str) -> List["MachineAction"]:
return self._machine_action_manager.getSupportedActions(self._machine_manager.getDefinitionByMachineId(definition_id))
@pyqtSlot(result = QObject) @pyqtSlot(result = QObject)
def getExtrudersModel(self, *args) -> "ExtrudersModel": def getExtrudersModel(self, *args) -> "ExtrudersModel":
if self._extruders_model is None: if self._extruders_model is None:
@ -1119,6 +1142,16 @@ class CuraApplication(QtApplication):
self._build_plate_model = BuildPlateModel(self) self._build_plate_model = BuildPlateModel(self)
return self._build_plate_model return self._build_plate_model
@pyqtSlot()
def exportUcp(self):
writer = self.getMeshFileHandler().getWriter("3MFWriter")
if writer is None:
Logger.warning("3mf writer is not enabled")
return
writer.exportUcp()
def getCuraSceneController(self, *args) -> CuraSceneController: def getCuraSceneController(self, *args) -> CuraSceneController:
if self._cura_scene_controller is None: if self._cura_scene_controller is None:
self._cura_scene_controller = CuraSceneController.createCuraSceneController() self._cura_scene_controller = CuraSceneController.createCuraSceneController()
@ -1129,18 +1162,16 @@ class CuraApplication(QtApplication):
self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager() self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager()
return self._setting_inheritance_manager return self._setting_inheritance_manager
def getMachineActionManager(self, *args: Any) -> MachineActionManager.MachineActionManager: @pyqtSlot(result = QObject)
def getMachineActionManager(self, *args: Any) -> MachineActionManager:
"""Get the machine action manager """Get the machine action manager
We ignore any *args given to this, as we also register the machine manager as qml singleton. We ignore any *args given to this, as we also register the machine manager as qml singleton.
It wants to give this function an engine and script engine, but we don't care about that. It wants to give this function an engine and script engine, but we don't care about that.
""" """
return cast(MachineActionManager.MachineActionManager, self._machine_action_manager) return self._machine_action_manager
@pyqtSlot(result = QObject)
def getMachineActionManagerQml(self)-> MachineActionManager.MachineActionManager:
return cast(QObject, self._machine_action_manager)
@pyqtSlot(result = QObject) @pyqtSlot(result = QObject)
def getMaterialManagementModel(self) -> MaterialManagementModel: def getMaterialManagementModel(self) -> MaterialManagementModel:
@ -1250,6 +1281,7 @@ class CuraApplication(QtApplication):
self.processEvents() self.processEvents()
engine.rootContext().setContextProperty("Printer", self) engine.rootContext().setContextProperty("Printer", self)
engine.rootContext().setContextProperty("CuraApplication", self) engine.rootContext().setContextProperty("CuraApplication", self)
engine.rootContext().setContextProperty("PrintOrderManager", self._print_order_manager)
engine.rootContext().setContextProperty("PrintInformation", self._print_information) engine.rootContext().setContextProperty("PrintInformation", self._print_information)
engine.rootContext().setContextProperty("CuraActions", self._cura_actions) engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
engine.rootContext().setContextProperty("CuraSDKVersion", ApplicationMetadata.CuraSDKVersion) engine.rootContext().setContextProperty("CuraSDKVersion", ApplicationMetadata.CuraSDKVersion)
@ -1264,7 +1296,7 @@ class CuraApplication(QtApplication):
qmlRegisterSingletonType(IntentManager, "Cura", 1, 6, self.getIntentManager, "IntentManager") qmlRegisterSingletonType(IntentManager, "Cura", 1, 6, self.getIntentManager, "IntentManager")
qmlRegisterSingletonType(SettingInheritanceManager, "Cura", 1, 0, self.getSettingInheritanceManager, "SettingInheritanceManager") qmlRegisterSingletonType(SettingInheritanceManager, "Cura", 1, 0, self.getSettingInheritanceManager, "SettingInheritanceManager")
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, self.getSimpleModeSettingsManagerWrapper, "SimpleModeSettingsManager") qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, self.getSimpleModeSettingsManagerWrapper, "SimpleModeSettingsManager")
qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, self.getMachineActionManagerWrapper, "MachineActionManager") qmlRegisterSingletonType(MachineActionManager, "Cura", 1, 0, self.getMachineActionManagerWrapper, "MachineActionManager")
self.processEvents() self.processEvents()
qmlRegisterType(NetworkingUtil, "Cura", 1, 5, "NetworkingUtil") qmlRegisterType(NetworkingUtil, "Cura", 1, 5, "NetworkingUtil")
@ -1745,8 +1777,12 @@ class CuraApplication(QtApplication):
Selection.remove(node) Selection.remove(node)
Selection.add(group_node) Selection.add(group_node)
all_nodes = self.getObjectsModel().getNodes()
PrintOrderManager.updatePrintOrdersAfterGroupOperation(all_nodes, group_node, selected_nodes)
@pyqtSlot() @pyqtSlot()
def ungroupSelected(self) -> None: def ungroupSelected(self) -> None:
all_nodes = self.getObjectsModel().getNodes()
selected_objects = Selection.getAllSelectedObjects().copy() selected_objects = Selection.getAllSelectedObjects().copy()
for node in selected_objects: for node in selected_objects:
if node.callDecoration("isGroup"): if node.callDecoration("isGroup"):
@ -1754,21 +1790,30 @@ class CuraApplication(QtApplication):
group_parent = node.getParent() group_parent = node.getParent()
children = node.getChildren().copy() children = node.getChildren().copy()
for child in children:
# Ungroup only 1 level deep
if child.getParent() != node:
continue
# Ungroup only 1 level deep
children_to_ungroup = list(filter(lambda child: child.getParent() == node, children))
for child in children_to_ungroup:
# Set the parent of the children to the parent of the group-node # Set the parent of the children to the parent of the group-node
op.addOperation(SetParentOperation(child, group_parent)) op.addOperation(SetParentOperation(child, group_parent))
# Add all individual nodes to the selection # Add all individual nodes to the selection
Selection.add(child) Selection.add(child)
PrintOrderManager.updatePrintOrdersAfterUngroupOperation(all_nodes, node, children_to_ungroup)
op.push() op.push()
# Note: The group removes itself from the scene once all its children have left it, # Note: The group removes itself from the scene once all its children have left it,
# see GroupDecorator._onChildrenChanged # see GroupDecorator._onChildrenChanged
def _onPrintOrderChanged(self) -> None:
# update object list
scene = self.getController().getScene()
scene.sceneChanged.emit(scene.getRoot())
# reset if already was sliced
Application.getInstance().getBackend().needsSlicing()
Application.getInstance().getBackend().tickle()
def _createSplashScreen(self) -> Optional[CuraSplashScreen.CuraSplashScreen]: def _createSplashScreen(self) -> Optional[CuraSplashScreen.CuraSplashScreen]:
if self._is_headless: if self._is_headless:
return None return None
@ -1932,6 +1977,17 @@ class CuraApplication(QtApplication):
openProjectFile = pyqtSignal(QUrl, bool, arguments = ["project_file", "add_to_recent_files"]) # Emitted when a project file is about to open. openProjectFile = pyqtSignal(QUrl, bool, arguments = ["project_file", "add_to_recent_files"]) # Emitted when a project file is about to open.
@pyqtSlot(QUrl, bool)
def readLocalUcpFile(self, file: QUrl, add_to_recent_files: bool = True):
file_name = QUrl(file).toLocalFile()
workspace_reader = self.getWorkspaceFileHandler()
if workspace_reader is None:
Logger.warning(f"Workspace reader not found, cannot read file {file_name}.")
return
workspace_reader.readLocalFile(file, add_to_recent_files)
@pyqtSlot(QUrl, str, bool) @pyqtSlot(QUrl, str, bool)
@pyqtSlot(QUrl, str) @pyqtSlot(QUrl, str)
@pyqtSlot(QUrl) @pyqtSlot(QUrl)
@ -2137,6 +2193,12 @@ class CuraApplication(QtApplication):
def addNonSliceableExtension(self, extension): def addNonSliceableExtension(self, extension):
self._non_sliceable_extensions.append(extension) self._non_sliceable_extensions.append(extension)
@pyqtSlot(str, result = bool)
def isProjectUcp(self, file_url) -> bool:
file_path = QUrl(file_url).toLocalFile()
workspace_reader = self.getWorkspaceFileHandler().getReaderForFile(file_path)
return workspace_reader.getIsProjectUcp()
@pyqtSlot(str, result=bool) @pyqtSlot(str, result=bool)
def checkIsValidProjectFile(self, file_url): def checkIsValidProjectFile(self, file_url):
"""Checks if the given file URL is a valid project file. """ """Checks if the given file URL is a valid project file. """
@ -2146,6 +2208,8 @@ class CuraApplication(QtApplication):
if workspace_reader is None: if workspace_reader is None:
return False # non-project files won't get a reader return False # non-project files won't get a reader
try: try:
if workspace_reader.getPluginId() == "3MFReader":
workspace_reader.clearOpenAsUcp()
result = workspace_reader.preRead(file_path, show_dialog=False) result = workspace_reader.preRead(file_path, show_dialog=False)
return result == WorkspaceReader.PreReadResult.accepted return result == WorkspaceReader.PreReadResult.accepted
except: except:

88
cura/HitChecker.py Normal file
View file

@ -0,0 +1,88 @@
from typing import List, Dict
from cura.Scene.CuraSceneNode import CuraSceneNode
class HitChecker:
"""Checks if nodes can be printed without causing any collisions and interference"""
def __init__(self, nodes: List[CuraSceneNode]) -> None:
self._hit_map = self._buildHitMap(nodes)
def anyTwoNodesBlockEachOther(self, nodes: List[CuraSceneNode]) -> bool:
"""Returns True if any 2 nodes block each other"""
for a in nodes:
for b in nodes:
if self._hit_map[a][b] and self._hit_map[b][a]:
return True
return False
def canPrintBefore(self, node: CuraSceneNode, other_nodes: List[CuraSceneNode]) -> bool:
"""Returns True if node doesn't block other_nodes and can be printed before them"""
no_hits = all(not self._hit_map[node][other_node] for other_node in other_nodes)
return no_hits
def canPrintAfter(self, node: CuraSceneNode, other_nodes: List[CuraSceneNode]) -> bool:
"""Returns True if node doesn't hit other nodes and can be printed after them"""
no_hits = all(not self._hit_map[other_node][node] for other_node in other_nodes)
return no_hits
def calculateScore(self, a: CuraSceneNode, b: CuraSceneNode) -> int:
"""Calculate score simply sums the number of other objects it 'blocks'
:param a: node
:param b: node
:return: sum of the number of other objects
"""
score_a = sum(self._hit_map[a].values())
score_b = sum(self._hit_map[b].values())
return score_a - score_b
def canPrintNodesInProvidedOrder(self, ordered_nodes: List[CuraSceneNode]) -> bool:
"""Returns True If nodes don't have any hits in provided order"""
for node_index, node in enumerate(ordered_nodes):
nodes_before = ordered_nodes[:node_index - 1] if node_index - 1 >= 0 else []
nodes_after = ordered_nodes[node_index + 1:] if node_index + 1 < len(ordered_nodes) else []
if not self.canPrintBefore(node, nodes_after) or not self.canPrintAfter(node, nodes_before):
return False
return True
@staticmethod
def _buildHitMap(nodes: List[CuraSceneNode]) -> Dict[CuraSceneNode, CuraSceneNode]:
"""Pre-computes all hits between all objects
:nodes: nodes that need to be checked for collisions
:return: dictionary where hit_map[node1][node2] is False if there node1 can be printed before node2
"""
hit_map = {j: {i: HitChecker._checkHit(j, i) for i in nodes} for j in nodes}
return hit_map
@staticmethod
def _checkHit(a: CuraSceneNode, b: CuraSceneNode) -> bool:
"""Checks if a can be printed before b
:param a: node
:param b: node
:return: False if a can be printed before b
"""
if a == b:
return False
a_hit_hull = a.callDecoration("getConvexHullBoundary")
b_hit_hull = b.callDecoration("getConvexHullHeadFull")
overlap = a_hit_hull.intersectsPolygon(b_hit_hull)
if overlap:
return True
# Adhesion areas must never overlap, regardless of printing order
# This would cause over-extrusion
a_hit_hull = a.callDecoration("getAdhesionArea")
b_hit_hull = b.callDecoration("getAdhesionArea")
overlap = a_hit_hull.intersectsPolygon(b_hit_hull)
if overlap:
return True
else:
return False

View file

@ -5,7 +5,7 @@
# online cloud connected printers are represented within this ListModel. Additional information such as the number of # online cloud connected printers are represented within this ListModel. Additional information such as the number of
# connected printers for each printer type is gathered. # connected printers for each printer type is gathered.
from typing import Optional, List, cast from typing import Optional, List, cast, Dict, Any
from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSlot, pyqtProperty, pyqtSignal from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSlot, pyqtProperty, pyqtSignal
@ -30,10 +30,10 @@ class MachineListModel(ListModel):
ComponentTypeRole = Qt.ItemDataRole.UserRole + 8 ComponentTypeRole = Qt.ItemDataRole.UserRole + 8
IsNetworkedMachineRole = Qt.ItemDataRole.UserRole + 9 IsNetworkedMachineRole = Qt.ItemDataRole.UserRole + 9
def __init__(self, parent: Optional[QObject] = None, machines_filter: List[GlobalStack] = None, listenToChanges: bool = True) -> None: def __init__(self, parent: Optional[QObject] = None, machines_filter: List[GlobalStack] = None, listenToChanges: bool = True, showCloudPrinters: bool = False) -> None:
super().__init__(parent) super().__init__(parent)
self._show_cloud_printers = False self._show_cloud_printers = showCloudPrinters
self._machines_filter = machines_filter self._machines_filter = machines_filter
self._catalog = i18nCatalog("cura") self._catalog = i18nCatalog("cura")
@ -159,3 +159,8 @@ class MachineListModel(ListModel):
"machineCount": machine_count, "machineCount": machine_count,
"catergory": "connected" if is_online else "other", "catergory": "connected" if is_online else "other",
}) })
def getItems(self) -> Dict[str, Any]:
if self.count > 0:
return self.items
return {}

View file

@ -16,6 +16,7 @@ from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To downlo
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S" TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
REQUEST_TIMEOUT = 5 # Seconds
class AuthorizationHelpers: class AuthorizationHelpers:
@ -53,7 +54,8 @@ class AuthorizationHelpers:
data = urllib.parse.urlencode(data).encode("UTF-8"), data = urllib.parse.urlencode(data).encode("UTF-8"),
headers_dict = headers, headers_dict = headers,
callback = lambda response: self.parseTokenResponse(response, callback), callback = lambda response: self.parseTokenResponse(response, callback),
error_callback = lambda response, _: self.parseTokenResponse(response, callback) error_callback = lambda response, _: self.parseTokenResponse(response, callback),
timeout = REQUEST_TIMEOUT
) )
def getAccessTokenUsingRefreshToken(self, refresh_token: str, callback: Callable[[AuthenticationResponse], None]) -> None: def getAccessTokenUsingRefreshToken(self, refresh_token: str, callback: Callable[[AuthenticationResponse], None]) -> None:
@ -77,7 +79,9 @@ class AuthorizationHelpers:
data = urllib.parse.urlencode(data).encode("UTF-8"), data = urllib.parse.urlencode(data).encode("UTF-8"),
headers_dict = headers, headers_dict = headers,
callback = lambda response: self.parseTokenResponse(response, callback), callback = lambda response: self.parseTokenResponse(response, callback),
error_callback = lambda response, _: self.parseTokenResponse(response, callback) error_callback = lambda response, _: self.parseTokenResponse(response, callback),
urgent = True,
timeout = REQUEST_TIMEOUT
) )
def parseTokenResponse(self, token_response: QNetworkReply, callback: Callable[[AuthenticationResponse], None]) -> None: def parseTokenResponse(self, token_response: QNetworkReply, callback: Callable[[AuthenticationResponse], None]) -> None:
@ -122,7 +126,8 @@ class AuthorizationHelpers:
check_token_url, check_token_url,
headers_dict = headers, headers_dict = headers,
callback = lambda reply: self._parseUserProfile(reply, success_callback, failed_callback), callback = lambda reply: self._parseUserProfile(reply, success_callback, failed_callback),
error_callback = lambda _, _2: failed_callback() if failed_callback is not None else None error_callback = lambda _, _2: failed_callback() if failed_callback is not None else None,
timeout = REQUEST_TIMEOUT
) )
def _parseUserProfile(self, reply: QNetworkReply, success_callback: Optional[Callable[[UserProfile], None]], failed_callback: Optional[Callable[[], None]] = None) -> None: def _parseUserProfile(self, reply: QNetworkReply, success_callback: Optional[Callable[[UserProfile], None]], failed_callback: Optional[Callable[[], None]] = None) -> None:

View file

@ -1,4 +1,4 @@
# Copyright (c) 2021 Ultimaker B.V. # Copyright (c) 2024 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
@ -6,13 +6,14 @@ from datetime import datetime, timedelta
from typing import Callable, Dict, Optional, TYPE_CHECKING, Union from typing import Callable, Dict, Optional, TYPE_CHECKING, Union
from urllib.parse import urlencode, quote_plus from urllib.parse import urlencode, quote_plus
from PyQt6.QtCore import QUrl from PyQt6.QtCore import QUrl, QTimer
from PyQt6.QtGui import QDesktopServices from PyQt6.QtGui import QDesktopServices
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
from UM.Signal import Signal from UM.Signal import Signal
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To download log-in tokens.
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers, TOKEN_TIMESTAMP_FORMAT from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers, TOKEN_TIMESTAMP_FORMAT
from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer
from cura.OAuth2.Models import AuthenticationResponse, BaseModel from cura.OAuth2.Models import AuthenticationResponse, BaseModel
@ -25,6 +26,8 @@ if TYPE_CHECKING:
MYCLOUD_LOGOFF_URL = "https://account.ultimaker.com/logoff?utm_source=cura&utm_medium=software&utm_campaign=change-account-before-adding-printers" MYCLOUD_LOGOFF_URL = "https://account.ultimaker.com/logoff?utm_source=cura&utm_medium=software&utm_campaign=change-account-before-adding-printers"
REFRESH_TOKEN_MAX_RETRIES = 15
REFRESH_TOKEN_RETRY_INTERVAL = 1000
class AuthorizationService: class AuthorizationService:
"""The authorization service is responsible for handling the login flow, storing user credentials and providing """The authorization service is responsible for handling the login flow, storing user credentials and providing
@ -57,6 +60,12 @@ class AuthorizationService:
self.onAuthStateChanged.connect(self._authChanged) self.onAuthStateChanged.connect(self._authChanged)
self._refresh_token_retries = 0
self._refresh_token_retry_timer = QTimer()
self._refresh_token_retry_timer.setInterval(REFRESH_TOKEN_RETRY_INTERVAL)
self._refresh_token_retry_timer.setSingleShot(True)
self._refresh_token_retry_timer.timeout.connect(self.refreshAccessToken)
def _authChanged(self, logged_in): def _authChanged(self, logged_in):
if logged_in and self._unable_to_get_data_message is not None: if logged_in and self._unable_to_get_data_message is not None:
self._unable_to_get_data_message.hide() self._unable_to_get_data_message.hide()
@ -167,16 +176,29 @@ class AuthorizationService:
return return
def process_auth_data(response: AuthenticationResponse) -> None: def process_auth_data(response: AuthenticationResponse) -> None:
self._currently_refreshing_token = False
if response.success: if response.success:
self._refresh_token_retries = 0
self._storeAuthData(response) self._storeAuthData(response)
HttpRequestManager.getInstance().setDelayRequests(False)
self.onAuthStateChanged.emit(logged_in = True) self.onAuthStateChanged.emit(logged_in = True)
else: else:
Logger.warning("Failed to get a new access token from the server.") if self._refresh_token_retries >= REFRESH_TOKEN_MAX_RETRIES:
self.onAuthStateChanged.emit(logged_in = False) self._refresh_token_retries = 0
Logger.warning("Failed to get a new access token from the server, giving up.")
HttpRequestManager.getInstance().setDelayRequests(False)
self.onAuthStateChanged.emit(logged_in = False)
else:
# Retry a bit later, network may be offline right now and will hopefully be back soon
Logger.warning("Failed to get a new access token from the server, retrying later.")
self._refresh_token_retries += 1
self._refresh_token_retry_timer.start()
if self._currently_refreshing_token: if self._currently_refreshing_token:
Logger.debug("Was already busy refreshing token. Do not start a new request.") Logger.debug("Was already busy refreshing token. Do not start a new request.")
return return
HttpRequestManager.getInstance().setDelayRequests(True)
self._currently_refreshing_token = True self._currently_refreshing_token = True
self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token, process_auth_data) self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token, process_auth_data)

View file

@ -7,6 +7,11 @@ from UM.Scene.Iterator import Iterator
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from functools import cmp_to_key from functools import cmp_to_key
from cura.HitChecker import HitChecker
from cura.PrintOrderManager import PrintOrderManager
from cura.Scene.CuraSceneNode import CuraSceneNode
class OneAtATimeIterator(Iterator.Iterator): class OneAtATimeIterator(Iterator.Iterator):
"""Iterator that returns a list of nodes in the order that they need to be printed """Iterator that returns a list of nodes in the order that they need to be printed
@ -16,8 +21,6 @@ class OneAtATimeIterator(Iterator.Iterator):
def __init__(self, scene_node) -> None: def __init__(self, scene_node) -> None:
super().__init__(scene_node) # Call super to make multiple inheritance work. super().__init__(scene_node) # Call super to make multiple inheritance work.
self._hit_map = [[]] # type: List[List[bool]] # For each node, which other nodes this hits. A grid of booleans on which nodes hit which.
self._original_node_list = [] # type: List[SceneNode] # The nodes that need to be checked for collisions.
def _fillStack(self) -> None: def _fillStack(self) -> None:
"""Fills the ``_node_stack`` with a list of scene nodes that need to be printed in order. """ """Fills the ``_node_stack`` with a list of scene nodes that need to be printed in order. """
@ -38,104 +41,50 @@ class OneAtATimeIterator(Iterator.Iterator):
self._node_stack = node_list[:] self._node_stack = node_list[:]
return return
# Copy the list hit_checker = HitChecker(node_list)
self._original_node_list = node_list[:]
# Initialise the hit map (pre-compute all hits between all objects) if PrintOrderManager.isUserDefinedPrintOrderEnabled():
self._hit_map = [[self._checkHit(i, j) for i in node_list] for j in node_list] self._node_stack = self._getNodesOrderedByUser(hit_checker, node_list)
else:
self._node_stack = self._getNodesOrderedAutomatically(hit_checker, node_list)
# Check if we have to files that block each other. If this is the case, there is no solution! # update print orders so that user can try to arrange the nodes automatically first
for a in range(0, len(node_list)): # and if result is not satisfactory he/she can switch to manual mode and change it
for b in range(0, len(node_list)): for index, node in enumerate(self._node_stack):
if a != b and self._hit_map[a][b] and self._hit_map[b][a]: node.printOrder = index + 1
return
@staticmethod
def _getNodesOrderedByUser(hit_checker: HitChecker, node_list: List[CuraSceneNode]) -> List[CuraSceneNode]:
nodes_ordered_by_user = sorted(node_list, key=lambda n: n.printOrder)
if hit_checker.canPrintNodesInProvidedOrder(nodes_ordered_by_user):
return nodes_ordered_by_user
return [] # No solution
@staticmethod
def _getNodesOrderedAutomatically(hit_checker: HitChecker, node_list: List[CuraSceneNode]) -> List[CuraSceneNode]:
# Check if we have two files that block each other. If this is the case, there is no solution!
if hit_checker.anyTwoNodesBlockEachOther(node_list):
return [] # No solution
# Sort the original list so that items that block the most other objects are at the beginning. # Sort the original list so that items that block the most other objects are at the beginning.
# This does not decrease the worst case running time, but should improve it in most cases. # This does not decrease the worst case running time, but should improve it in most cases.
sorted(node_list, key = cmp_to_key(self._calculateScore)) node_list = sorted(node_list, key = cmp_to_key(hit_checker.calculateScore))
todo_node_list = [_ObjectOrder([], node_list)] todo_node_list = [_ObjectOrder([], node_list)]
while len(todo_node_list) > 0: while len(todo_node_list) > 0:
current = todo_node_list.pop() current = todo_node_list.pop()
for node in current.todo: for node in current.todo:
# Check if the object can be placed with what we have and still allows for a solution in the future # Check if the object can be placed with what we have and still allows for a solution in the future
if not self._checkHitMultiple(node, current.order) and not self._checkBlockMultiple(node, current.todo): if hit_checker.canPrintAfter(node, current.order) and hit_checker.canPrintBefore(node, current.todo):
# We found a possible result. Create new todo & order list. # We found a possible result. Create new todo & order list.
new_todo_list = current.todo[:] new_todo_list = current.todo[:]
new_todo_list.remove(node) new_todo_list.remove(node)
new_order = current.order[:] + [node] new_order = current.order[:] + [node]
if len(new_todo_list) == 0: if len(new_todo_list) == 0:
# We have no more nodes to check, so quit looking. # We have no more nodes to check, so quit looking.
self._node_stack = new_order return new_order # Solution found!
return
todo_node_list.append(_ObjectOrder(new_order, new_todo_list)) todo_node_list.append(_ObjectOrder(new_order, new_todo_list))
self._node_stack = [] #No result found! return [] # No result found!
# Check if first object can be printed before the provided list (using the hit map)
def _checkHitMultiple(self, node: SceneNode, other_nodes: List[SceneNode]) -> bool:
node_index = self._original_node_list.index(node)
for other_node in other_nodes:
other_node_index = self._original_node_list.index(other_node)
if self._hit_map[node_index][other_node_index]:
return True
return False
def _checkBlockMultiple(self, node: SceneNode, other_nodes: List[SceneNode]) -> bool:
"""Check for a node whether it hits any of the other nodes.
:param node: The node to check whether it collides with the other nodes.
:param other_nodes: The nodes to check for collisions.
:return: returns collision between nodes
"""
node_index = self._original_node_list.index(node)
for other_node in other_nodes:
other_node_index = self._original_node_list.index(other_node)
if self._hit_map[other_node_index][node_index] and node_index != other_node_index:
return True
return False
def _calculateScore(self, a: SceneNode, b: SceneNode) -> int:
"""Calculate score simply sums the number of other objects it 'blocks'
:param a: node
:param b: node
:return: sum of the number of other objects
"""
score_a = sum(self._hit_map[self._original_node_list.index(a)])
score_b = sum(self._hit_map[self._original_node_list.index(b)])
return score_a - score_b
def _checkHit(self, a: SceneNode, b: SceneNode) -> bool:
"""Checks if a can be printed before b
:param a: node
:param b: node
:return: true if a can be printed before b
"""
if a == b:
return False
a_hit_hull = a.callDecoration("getConvexHullBoundary")
b_hit_hull = b.callDecoration("getConvexHullHeadFull")
overlap = a_hit_hull.intersectsPolygon(b_hit_hull)
if overlap:
return True
# Adhesion areas must never overlap, regardless of printing order
# This would cause over-extrusion
a_hit_hull = a.callDecoration("getAdhesionArea")
b_hit_hull = b.callDecoration("getAdhesionArea")
overlap = a_hit_hull.intersectsPolygon(b_hit_hull)
if overlap:
return True
else:
return False
class _ObjectOrder: class _ObjectOrder:

View file

@ -39,6 +39,11 @@ class PlatformPhysics:
Application.getInstance().getPreferences().addPreference("physics/automatic_push_free", False) Application.getInstance().getPreferences().addPreference("physics/automatic_push_free", False)
Application.getInstance().getPreferences().addPreference("physics/automatic_drop_down", True) Application.getInstance().getPreferences().addPreference("physics/automatic_drop_down", True)
self._app_all_model_drop = False
def setAppAllModelDropDown(self):
self._app_all_model_drop = True
self._onChangeTimerFinished()
def _onSceneChanged(self, source): def _onSceneChanged(self, source):
if not source.callDecoration("isSliceable"): if not source.callDecoration("isSliceable"):
@ -80,9 +85,9 @@ class PlatformPhysics:
# Move it downwards if bottom is above platform # Move it downwards if bottom is above platform
move_vector = Vector() move_vector = Vector()
if node.getSetting(SceneNodeSettings.AutoDropDown, app_automatic_drop_down) and not (node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent() != root) and node.isEnabled(): #If an object is grouped, don't move it down if (node.getSetting(SceneNodeSettings.AutoDropDown, app_automatic_drop_down) or self._app_all_model_drop) and not (node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent() != root) and node.isEnabled():
z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0 z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
move_vector = move_vector.set(y = -bbox.bottom + z_offset) move_vector = move_vector.set(y=-bbox.bottom + z_offset)
# If there is no convex hull for the node, start calculating it and continue. # If there is no convex hull for the node, start calculating it and continue.
if not node.getDecorator(ConvexHullDecorator) and not node.callDecoration("isNonPrintingMesh") and node.callDecoration("getLayerData") is None: if not node.getDecorator(ConvexHullDecorator) and not node.callDecoration("isNonPrintingMesh") and node.callDecoration("getLayerData") is None:
@ -168,6 +173,8 @@ class PlatformPhysics:
op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector) op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
op.push() op.push()
# setting this drop to model same as app_automatic_drop_down
self._app_all_model_drop = False
# After moving, we have to evaluate the boundary checks for nodes # After moving, we have to evaluate the boundary checks for nodes
build_volume.updateNodeBoundaryCheck() build_volume.updateNodeBoundaryCheck()

174
cura/PrintOrderManager.py Normal file
View file

@ -0,0 +1,174 @@
from typing import List, Callable, Optional, Any
from PyQt6.QtCore import pyqtProperty, pyqtSignal, QObject, pyqtSlot
from UM.Application import Application
from UM.Scene.Selection import Selection
from cura.Scene.CuraSceneNode import CuraSceneNode
class PrintOrderManager(QObject):
"""Allows to order the object list to set the print sequence manually"""
def __init__(self, get_nodes: Callable[[], List[CuraSceneNode]]) -> None:
super().__init__()
self._get_nodes = get_nodes
self._configureEvents()
_settingsChanged = pyqtSignal()
_uiActionsOutdated = pyqtSignal()
printOrderChanged = pyqtSignal()
@pyqtSlot()
def swapSelectedAndPreviousNodes(self) -> None:
selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
self._swapPrintOrders(selected_node, previous_node)
@pyqtSlot()
def swapSelectedAndNextNodes(self) -> None:
selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
self._swapPrintOrders(selected_node, next_node)
@pyqtProperty(str, notify=_uiActionsOutdated)
def previousNodeName(self) -> str:
selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
return self._getNodeName(previous_node)
@pyqtProperty(str, notify=_uiActionsOutdated)
def nextNodeName(self) -> str:
selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
return self._getNodeName(next_node)
@pyqtProperty(bool, notify=_uiActionsOutdated)
def shouldEnablePrintBeforeAction(self) -> bool:
selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
can_swap_with_previous_node = selected_node is not None and previous_node is not None
return can_swap_with_previous_node
@pyqtProperty(bool, notify=_uiActionsOutdated)
def shouldEnablePrintAfterAction(self) -> bool:
selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
can_swap_with_next_node = selected_node is not None and next_node is not None
return can_swap_with_next_node
@pyqtProperty(bool, notify=_settingsChanged)
def shouldShowEditPrintOrderActions(self) -> bool:
return PrintOrderManager.isUserDefinedPrintOrderEnabled()
@staticmethod
def isUserDefinedPrintOrderEnabled() -> bool:
stack = Application.getInstance().getGlobalContainerStack()
is_enabled = stack and \
stack.getProperty("print_sequence", "value") == "one_at_a_time" and \
stack.getProperty("user_defined_print_order_enabled", "value")
return bool(is_enabled)
@staticmethod
def initializePrintOrders(nodes: List[CuraSceneNode]) -> None:
"""Just created (loaded from file) nodes have print order 0.
This method initializes print orders with max value to put nodes at the end of object list"""
max_print_order = max(map(lambda n: n.printOrder, nodes), default=0)
for node in nodes:
if node.printOrder == 0:
max_print_order += 1
node.printOrder = max_print_order
@staticmethod
def updatePrintOrdersAfterGroupOperation(
all_nodes: List[CuraSceneNode],
group_node: CuraSceneNode,
grouped_nodes: List[CuraSceneNode]
) -> None:
group_node.printOrder = min(map(lambda n: n.printOrder, grouped_nodes))
all_nodes.append(group_node)
for node in grouped_nodes:
all_nodes.remove(node)
# reassign print orders so there won't be gaps like 1 2 5 6 7
sorted_nodes = sorted(all_nodes, key=lambda n: n.printOrder)
for i, node in enumerate(sorted_nodes):
node.printOrder = i + 1
@staticmethod
def updatePrintOrdersAfterUngroupOperation(
all_nodes: List[CuraSceneNode],
group_node: CuraSceneNode,
ungrouped_nodes: List[CuraSceneNode]
) -> None:
all_nodes.remove(group_node)
nodes_to_update_print_order = filter(lambda n: n.printOrder > group_node.printOrder, all_nodes)
for node in nodes_to_update_print_order:
node.printOrder += len(ungrouped_nodes) - 1
for i, child in enumerate(ungrouped_nodes):
child.printOrder = group_node.printOrder + i
all_nodes.append(child)
def _swapPrintOrders(self, node1: CuraSceneNode, node2: CuraSceneNode) -> None:
if node1 and node2:
node1.printOrder, node2.printOrder = node2.printOrder, node1.printOrder # swap print orders
self.printOrderChanged.emit() # update object list first
self._uiActionsOutdated.emit() # then update UI actions
def _getSelectedAndNeighborNodes(self
) -> (Optional[CuraSceneNode], Optional[CuraSceneNode], Optional[CuraSceneNode]):
nodes = self._get_nodes()
ordered_nodes = sorted(nodes, key=lambda n: n.printOrder)
for i, node in enumerate(ordered_nodes, 1):
node.printOrder = i
selected_node = PrintOrderManager._getSingleSelectedNode()
if selected_node and selected_node in ordered_nodes:
selected_node_index = ordered_nodes.index(selected_node)
else:
selected_node_index = None
if selected_node_index is not None and selected_node_index - 1 >= 0:
previous_node = ordered_nodes[selected_node_index - 1]
else:
previous_node = None
if selected_node_index is not None and selected_node_index + 1 < len(ordered_nodes):
next_node = ordered_nodes[selected_node_index + 1]
else:
next_node = None
return selected_node, previous_node, next_node
@staticmethod
def _getNodeName(node: CuraSceneNode, max_length: int = 30) -> str:
node_name = node.getName() if node else ""
truncated_node_name = node_name[:max_length]
return truncated_node_name
@staticmethod
def _getSingleSelectedNode() -> Optional[CuraSceneNode]:
if len(Selection.getAllSelectedObjects()) == 1:
selected_node = Selection.getSelectedObject(0)
return selected_node
return None
def _configureEvents(self) -> None:
Selection.selectionChanged.connect(self._onSelectionChanged)
self._global_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
def _onGlobalStackChanged(self) -> None:
if self._global_stack:
self._global_stack.propertyChanged.disconnect(self._onSettingsChanged)
self._global_stack.containersChanged.disconnect(self._onSettingsChanged)
self._global_stack = Application.getInstance().getGlobalContainerStack()
if self._global_stack:
self._global_stack.propertyChanged.connect(self._onSettingsChanged)
self._global_stack.containersChanged.connect(self._onSettingsChanged)
def _onSettingsChanged(self, *args: Any) -> None:
self._settingsChanged.emit()
def _onSelectionChanged(self) -> None:
self._uiActionsOutdated.emit()

View file

@ -11,6 +11,7 @@ from UM.Scene.SceneNode import SceneNode
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator # To cast the deepcopy of every decorator back to SceneNodeDecorator. from UM.Scene.SceneNodeDecorator import SceneNodeDecorator # To cast the deepcopy of every decorator back to SceneNodeDecorator.
import cura.CuraApplication # To get the build plate. import cura.CuraApplication # To get the build plate.
from UM.Scene.SceneNodeSettings import SceneNodeSettings
from cura.Settings.ExtruderStack import ExtruderStack # For typing. from cura.Settings.ExtruderStack import ExtruderStack # For typing.
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator # For per-object settings. from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator # For per-object settings.
@ -25,13 +26,26 @@ class CuraSceneNode(SceneNode):
if not no_setting_override: if not no_setting_override:
self.addDecorator(SettingOverrideDecorator()) # Now we always have a getActiveExtruderPosition, unless explicitly disabled self.addDecorator(SettingOverrideDecorator()) # Now we always have a getActiveExtruderPosition, unless explicitly disabled
self._outside_buildarea = False self._outside_buildarea = False
self._print_order = 0
def setOutsideBuildArea(self, new_value: bool) -> None: def setOutsideBuildArea(self, new_value: bool) -> None:
self._outside_buildarea = new_value self._outside_buildarea = new_value
@property
def printOrder(self):
return self._print_order
@printOrder.setter
def printOrder(self, new_value):
self._print_order = new_value
def isOutsideBuildArea(self) -> bool: def isOutsideBuildArea(self) -> bool:
return self._outside_buildarea or self.callDecoration("getBuildPlateNumber") < 0 return self._outside_buildarea or self.callDecoration("getBuildPlateNumber") < 0
@property
def isDropDownEnabled(self) ->bool:
return self.getSetting(SceneNodeSettings.AutoDropDown, Application.getInstance().getPreferences().getValue("physics/automatic_drop_down"))
def isVisible(self) -> bool: def isVisible(self) -> bool:
return super().isVisible() and self.callDecoration("getBuildPlateNumber") == cura.CuraApplication.CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate return super().isVisible() and self.callDecoration("getBuildPlateNumber") == cura.CuraApplication.CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
@ -157,3 +171,6 @@ class CuraSceneNode(SceneNode):
def transformChanged(self) -> None: def transformChanged(self) -> None:
self._transformChanged() self._transformChanged()
def __repr__(self) -> str:
return "{print_order}. {name}".format(print_order = self._print_order, name = self.getName())

View file

@ -5,16 +5,18 @@ import json
import os import os
from typing import List, Optional from typing import List, Optional
from PyQt6.QtCore import QUrl
from PyQt6.QtNetwork import QLocalServer, QLocalSocket from PyQt6.QtNetwork import QLocalServer, QLocalSocket
from UM.Qt.QtApplication import QtApplication #For typing. from UM.Qt.QtApplication import QtApplication # For typing.
from UM.Logger import Logger from UM.Logger import Logger
class SingleInstance: class SingleInstance:
def __init__(self, application: QtApplication, files_to_open: Optional[List[str]]) -> None: def __init__(self, application: QtApplication, files_to_open: Optional[List[str]], url_to_open: Optional[List[str]]) -> None:
self._application = application self._application = application
self._files_to_open = files_to_open self._files_to_open = files_to_open
self._url_to_open = url_to_open
self._single_instance_server = None self._single_instance_server = None
@ -33,7 +35,7 @@ class SingleInstance:
return False return False
# We only send the files that need to be opened. # We only send the files that need to be opened.
if not self._files_to_open: if not self._files_to_open and not self._url_to_open:
Logger.log("i", "No file need to be opened, do nothing.") Logger.log("i", "No file need to be opened, do nothing.")
return True return True
@ -55,8 +57,12 @@ class SingleInstance:
payload = {"command": "open", "filePath": os.path.abspath(filename)} payload = {"command": "open", "filePath": os.path.abspath(filename)}
single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding = "ascii")) single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding = "ascii"))
for url in self._url_to_open:
payload = {"command": "open-url", "urlPath": url.toString()}
single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding="ascii"))
payload = {"command": "close-connection"} payload = {"command": "close-connection"}
single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding = "ascii")) single_instance_socket.write(bytes(json.dumps(payload) + "\n", encoding="ascii"))
single_instance_socket.flush() single_instance_socket.flush()
single_instance_socket.waitForDisconnected() single_instance_socket.waitForDisconnected()
@ -72,7 +78,7 @@ class SingleInstance:
def _onClientConnected(self) -> None: def _onClientConnected(self) -> None:
Logger.log("i", "New connection received on our single-instance server") Logger.log("i", "New connection received on our single-instance server")
connection = None #type: Optional[QLocalSocket] connection = None # type: Optional[QLocalSocket]
if self._single_instance_server: if self._single_instance_server:
connection = self._single_instance_server.nextPendingConnection() connection = self._single_instance_server.nextPendingConnection()
@ -81,7 +87,7 @@ class SingleInstance:
def __readCommands(self, connection: QLocalSocket) -> None: def __readCommands(self, connection: QLocalSocket) -> None:
line = connection.readLine() line = connection.readLine()
while len(line) != 0: # There is also a .canReadLine() while len(line) != 0: # There is also a .canReadLine()
try: try:
payload = json.loads(str(line, encoding = "ascii").strip()) payload = json.loads(str(line, encoding = "ascii").strip())
command = payload["command"] command = payload["command"]
@ -94,13 +100,19 @@ class SingleInstance:
elif command == "open": elif command == "open":
self._application.callLater(lambda f = payload["filePath"]: self._application._openFile(f)) self._application.callLater(lambda f = payload["filePath"]: self._application._openFile(f))
#command: Load a url link in Cura
elif command == "open-url":
url = QUrl(payload["urlPath"])
self._application.callLater(lambda: self._application._openUrl(url))
# Command: Activate the window and bring it to the top. # Command: Activate the window and bring it to the top.
elif command == "focus": elif command == "focus":
# Operating systems these days prevent windows from moving around by themselves. # Operating systems these days prevent windows from moving around by themselves.
# 'alert' or flashing the icon in the taskbar is the best thing we do now. # 'alert' or flashing the icon in the taskbar is the best thing we do now.
main_window = self._application.getMainWindow() main_window = self._application.getMainWindow()
if main_window is not None: if main_window is not None:
self._application.callLater(lambda: main_window.alert(0)) # type: ignore # I don't know why MyPy complains here self._application.callLater(lambda: main_window.alert(0)) # type: ignore # I don't know why MyPy complains here
# Command: Close the socket connection. We're done. # Command: Close the socket connection. We're done.
elif command == "close-connection": elif command == "close-connection":

View file

@ -14,6 +14,9 @@ from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection from UM.Scene.Selection import Selection
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from cura.PrintOrderManager import PrintOrderManager
from cura.Scene.CuraSceneNode import CuraSceneNode
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
@ -76,6 +79,9 @@ class ObjectsModel(ListModel):
self._build_plate_number = nr self._build_plate_number = nr
self._update() self._update()
def getNodes(self) -> List[CuraSceneNode]:
return list(map(lambda n: n["node"], self.items))
def _updateSceneDelayed(self, source) -> None: def _updateSceneDelayed(self, source) -> None:
if not isinstance(source, Camera): if not isinstance(source, Camera):
self._update_timer.start() self._update_timer.start()
@ -175,6 +181,10 @@ class ObjectsModel(ListModel):
all_nodes = self._renameNodes(name_to_node_info_dict) all_nodes = self._renameNodes(name_to_node_info_dict)
user_defined_print_order_enabled = PrintOrderManager.isUserDefinedPrintOrderEnabled()
if user_defined_print_order_enabled:
PrintOrderManager.initializePrintOrders(all_nodes)
for node in all_nodes: for node in all_nodes:
if hasattr(node, "isOutsideBuildArea"): if hasattr(node, "isOutsideBuildArea"):
is_outside_build_area = node.isOutsideBuildArea() # type: ignore is_outside_build_area = node.isOutsideBuildArea() # type: ignore
@ -223,8 +233,13 @@ class ObjectsModel(ListModel):
# for anti overhang meshes and groups the extruder nr is irrelevant # for anti overhang meshes and groups the extruder nr is irrelevant
extruder_number = -1 extruder_number = -1
if not user_defined_print_order_enabled:
name = node.getName()
else:
name = "{print_order}. {name}".format(print_order = node.printOrder, name = node.getName())
nodes.append({ nodes.append({
"name": node.getName(), "name": name,
"selected": Selection.isSelected(node), "selected": Selection.isSelected(node),
"outside_build_area": is_outside_build_area, "outside_build_area": is_outside_build_area,
"buildplate_number": node_build_plate_number, "buildplate_number": node_build_plate_number,
@ -234,5 +249,5 @@ class ObjectsModel(ListModel):
"node": node "node": node
}) })
nodes = sorted(nodes, key=lambda n: n["name"]) nodes = sorted(nodes, key=lambda n: n["name"] if not user_defined_print_order_enabled else n["node"].printOrder)
self.setItems(nodes) self.setItems(nodes)

View file

@ -0,0 +1,46 @@
# Copyright (c) 2024 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt6.QtCore import Qt
from UM.Logger import Logger
from UM.Settings.SettingDefinition import SettingDefinition
from UM.Qt.ListModel import ListModel
class SpecificSettingsModel(ListModel):
CategoryRole = Qt.ItemDataRole.UserRole + 1
LabelRole = Qt.ItemDataRole.UserRole + 2
ValueRole = Qt.ItemDataRole.UserRole + 3
def __init__(self, parent = None):
super().__init__(parent = parent)
self.addRoleName(self.CategoryRole, "category")
self.addRoleName(self.LabelRole, "label")
self.addRoleName(self.ValueRole, "value")
self._i18n_catalog = None
self._update()
def addSettingsFromStack(self, stack, category, settings):
for setting, value in settings.items():
unit = stack.getProperty(setting, "unit")
setting_type = stack.getProperty(setting, "type")
if setting_type is not None:
# This is not very good looking, but will do for now
value = str(SettingDefinition.settingValueToString(setting_type, value)) + " " + str(unit)
else:
value = str(value)
self.appendItem({
"category": category,
"label": stack.getProperty(setting, "label"),
"value": value
})
def _update(self):
Logger.debug(f"Updating {self.__class__.__name__}")
self.setItems([])
return

View file

@ -16,6 +16,7 @@ from UM.Mesh.MeshReader import MeshReader
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
from UM.Scene.GroupDecorator import GroupDecorator from UM.Scene.GroupDecorator import GroupDecorator
from UM.Scene.SceneNode import SceneNode # For typing. from UM.Scene.SceneNode import SceneNode # For typing.
from UM.Scene.SceneNodeSettings import SceneNodeSettings
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.Machines.ContainerTree import ContainerTree from cura.Machines.ContainerTree import ContainerTree
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
@ -41,7 +42,7 @@ class ThreeMFReader(MeshReader):
MimeTypeDatabase.addMimeType( MimeTypeDatabase.addMimeType(
MimeType( MimeType(
name = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml", name="application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
comment="3MF", comment="3MF",
suffixes=["3mf"] suffixes=["3mf"]
) )
@ -177,6 +178,12 @@ class ThreeMFReader(MeshReader):
else: else:
Logger.log("w", "Unable to find extruder in position %s", setting_value) Logger.log("w", "Unable to find extruder in position %s", setting_value)
continue continue
if key == "print_order":
um_node.printOrder = int(setting_value)
continue
if key =="drop_to_buildplate":
um_node.setSetting(SceneNodeSettings.AutoDropDown, eval(setting_value))
continue
if key in known_setting_keys: if key in known_setting_keys:
setting_container.setProperty(key, "value", setting_value) setting_container.setProperty(key, "value", setting_value)
else: else:

View file

@ -5,6 +5,7 @@ from configparser import ConfigParser
import zipfile import zipfile
import os import os
import json import json
import re
from typing import cast, Dict, List, Optional, Tuple, Any, Set from typing import cast, Dict, List, Optional, Tuple, Any, Set
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
@ -57,6 +58,7 @@ _ignored_machine_network_metadata: Set[str] = {
"is_abstract_machine" "is_abstract_machine"
} }
USER_SETTINGS_PATH = "Cura/user-settings.json"
class ContainerInfo: class ContainerInfo:
def __init__(self, file_name: Optional[str], serialized: Optional[str], parser: Optional[ConfigParser]) -> None: def __init__(self, file_name: Optional[str], serialized: Optional[str], parser: Optional[ConfigParser]) -> None:
@ -115,6 +117,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._supported_extensions = [".3mf"] self._supported_extensions = [".3mf"]
self._dialog = WorkspaceDialog() self._dialog = WorkspaceDialog()
self._3mf_mesh_reader = None self._3mf_mesh_reader = None
self._is_ucp = None
self._container_registry = ContainerRegistry.getInstance() self._container_registry = ContainerRegistry.getInstance()
# suffixes registered with the MimeTypes don't start with a dot '.' # suffixes registered with the MimeTypes don't start with a dot '.'
@ -141,10 +144,16 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._old_new_materials: Dict[str, str] = {} self._old_new_materials: Dict[str, str] = {}
self._machine_info = None self._machine_info = None
self._user_settings: Dict[str, Dict[str, Any]] = {}
def _clearState(self): def _clearState(self):
self._id_mapping = {} self._id_mapping = {}
self._old_new_materials = {} self._old_new_materials = {}
self._machine_info = None self._machine_info = None
self._user_settings = {}
def clearOpenAsUcp(self):
self._is_ucp = None
def getNewId(self, old_id: str): def getNewId(self, old_id: str):
"""Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results. """Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results.
@ -200,6 +209,16 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
return global_stack_file_list[0], extruder_stack_file_list return global_stack_file_list[0], extruder_stack_file_list
def _isProjectUcp(self, file_name) -> bool:
if self._is_ucp == None:
archive = zipfile.ZipFile(file_name, "r")
cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")]
self._is_ucp =True if USER_SETTINGS_PATH in cura_file_names else False
def getIsProjectUcp(self) -> bool:
return self._is_ucp
def preRead(self, file_name, show_dialog=True, *args, **kwargs): def preRead(self, file_name, show_dialog=True, *args, **kwargs):
"""Read some info so we can make decisions """Read some info so we can make decisions
@ -208,7 +227,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
we don't want to show a dialog. we don't want to show a dialog.
""" """
self._clearState() self._clearState()
self._isProjectUcp(file_name)
self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name) self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name)
if self._3mf_mesh_reader and self._3mf_mesh_reader.preRead(file_name) == WorkspaceReader.PreReadResult.accepted: if self._3mf_mesh_reader and self._3mf_mesh_reader.preRead(file_name) == WorkspaceReader.PreReadResult.accepted:
pass pass
@ -228,11 +247,14 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._resolve_strategies = {k: None for k in resolve_strategy_keys} self._resolve_strategies = {k: None for k in resolve_strategy_keys}
containers_found_dict = {k: False for k in resolve_strategy_keys} containers_found_dict = {k: False for k in resolve_strategy_keys}
# Check whether the file is a UCP, which changes some import options
is_ucp = USER_SETTINGS_PATH in cura_file_names
# #
# Read definition containers # Read definition containers
# #
machine_definition_id = None machine_definition_id = None
updatable_machines = [] updatable_machines = None if self._is_ucp else []
machine_definition_container_count = 0 machine_definition_container_count = 0
extruder_definition_container_count = 0 extruder_definition_container_count = 0
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)] definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
@ -250,7 +272,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if definition_container_type == "machine": if definition_container_type == "machine":
machine_definition_id = container_id machine_definition_id = container_id
machine_definition_containers = self._container_registry.findDefinitionContainers(id = machine_definition_id) machine_definition_containers = self._container_registry.findDefinitionContainers(id = machine_definition_id)
if machine_definition_containers: if machine_definition_containers and updatable_machines is not None:
updatable_machines = [machine for machine in self._container_registry.findContainerStacks(type = "machine") if machine.definition == machine_definition_containers[0]] updatable_machines = [machine for machine in self._container_registry.findContainerStacks(type = "machine") if machine.definition == machine_definition_containers[0]]
machine_type = definition_container["name"] machine_type = definition_container["name"]
variant_type_name = definition_container.get("variants_name", variant_type_name) variant_type_name = definition_container.get("variants_name", variant_type_name)
@ -597,6 +619,39 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
package_metadata = self._parse_packages_metadata(archive) package_metadata = self._parse_packages_metadata(archive)
missing_package_metadata = self._filter_missing_package_metadata(package_metadata) missing_package_metadata = self._filter_missing_package_metadata(package_metadata)
# Load the user specifically exported settings
self._dialog.exportedSettingModel.clear()
self._dialog.setCurrentMachineName("")
if self._is_ucp:
try:
self._user_settings = json.loads(archive.open("Cura/user-settings.json").read().decode("utf-8"))
any_extruder_stack = ExtruderManager.getInstance().getExtruderStack(0)
actual_global_stack = CuraApplication.getInstance().getGlobalContainerStack()
self._dialog.setCurrentMachineName(actual_global_stack.id)
for stack_name, settings in self._user_settings.items():
if stack_name == 'global':
self._dialog.exportedSettingModel.addSettingsFromStack(actual_global_stack, i18n_catalog.i18nc("@label", "Global"), settings)
else:
extruder_match = re.fullmatch('extruder_([0-9]+)', stack_name)
if extruder_match is not None:
extruder_nr = int(extruder_match.group(1))
self._dialog.exportedSettingModel.addSettingsFromStack(any_extruder_stack,
i18n_catalog.i18nc("@label",
"Extruder {0}", extruder_nr + 1),
settings)
except KeyError as e:
# If there is no user settings file, it's not a UCP, so notify user of failure.
Logger.log("w", "File %s is not a valid UCP.", file_name)
message = Message(
i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
"Project file <filename>{0}</filename> is corrupt: <message>{1}</message>.",
file_name, str(e)),
title=i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
message_type=Message.MessageType.ERROR)
message.show()
return WorkspaceReader.PreReadResult.failed
# Show the dialog, informing the user what is about to happen. # Show the dialog, informing the user what is about to happen.
self._dialog.setMachineConflict(machine_conflict) self._dialog.setMachineConflict(machine_conflict)
self._dialog.setIsPrinterGroup(is_printer_group) self._dialog.setIsPrinterGroup(is_printer_group)
@ -617,12 +672,15 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._dialog.setVariantType(variant_type_name) self._dialog.setVariantType(variant_type_name)
self._dialog.setHasObjectsOnPlate(Application.getInstance().platformActivity) self._dialog.setHasObjectsOnPlate(Application.getInstance().platformActivity)
self._dialog.setMissingPackagesMetadata(missing_package_metadata) self._dialog.setMissingPackagesMetadata(missing_package_metadata)
self._dialog.setAllowCreatemachine(not self._is_ucp)
self._dialog.setIsUcp(self._is_ucp)
self._dialog.show() self._dialog.show()
# Choosing the initially selected printer in MachineSelector # Choosing the initially selected printer in MachineSelector
is_networked_machine = False is_networked_machine = False
is_abstract_machine = False is_abstract_machine = False
if global_stack and isinstance(global_stack, GlobalStack): if global_stack and isinstance(global_stack, GlobalStack) and not self._is_ucp:
# The machine included in the project file exists locally already, no need to change selected printers. # The machine included in the project file exists locally already, no need to change selected printers.
is_networked_machine = global_stack.hasNetworkedConnection() is_networked_machine = global_stack.hasNetworkedConnection()
is_abstract_machine = parseBool(existing_global_stack.getMetaDataEntry("is_abstract_machine", False)) is_abstract_machine = parseBool(existing_global_stack.getMetaDataEntry("is_abstract_machine", False))
@ -631,7 +689,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
elif self._dialog.updatableMachinesModel.count > 0: elif self._dialog.updatableMachinesModel.count > 0:
# The machine included in the project file does not exist. There is another machine of the same type. # The machine included in the project file does not exist. There is another machine of the same type.
# This will always default to an abstract machine first. # This will always default to an abstract machine first.
machine = self._dialog.updatableMachinesModel.getItem(0) machine = self._dialog.updatableMachinesModel.getItem(self._dialog.currentMachinePositionIndex)
machine_name = machine["name"] machine_name = machine["name"]
is_networked_machine = machine["isNetworked"] is_networked_machine = machine["isNetworked"]
is_abstract_machine = machine["isAbstractMachine"] is_abstract_machine = machine["isAbstractMachine"]
@ -648,6 +706,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._dialog.setIsNetworkedMachine(is_networked_machine) self._dialog.setIsNetworkedMachine(is_networked_machine)
self._dialog.setIsAbstractMachine(is_abstract_machine) self._dialog.setIsAbstractMachine(is_abstract_machine)
self._dialog.setMachineName(machine_name) self._dialog.setMachineName(machine_name)
self._dialog.updateCompatibleMachine()
# Block until the dialog is closed. # Block until the dialog is closed.
self._dialog.waitForClose() self._dialog.waitForClose()
@ -669,7 +728,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if key not in containers_found_dict or strategy is not None: if key not in containers_found_dict or strategy is not None:
continue continue
self._resolve_strategies[key] = "override" if containers_found_dict[key] else "new" self._resolve_strategies[key] = "override" if containers_found_dict[key] else "new"
return WorkspaceReader.PreReadResult.accepted return WorkspaceReader.PreReadResult.accepted
@call_on_qt_thread @call_on_qt_thread
@ -690,16 +748,16 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
except EnvironmentError as e: except EnvironmentError as e:
message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!", message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
"Project file <filename>{0}</filename> is suddenly inaccessible: <message>{1}</message>.", file_name, str(e)), "Project file <filename>{0}</filename> is suddenly inaccessible: <message>{1}</message>.", file_name, str(e)),
title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"), title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
message_type = Message.MessageType.ERROR) message_type = Message.MessageType.ERROR)
message.show() message.show()
self.setWorkspaceName("") self.setWorkspaceName("")
return [], {} return [], {}
except zipfile.BadZipFile as e: except zipfile.BadZipFile as e:
message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!", message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
"Project file <filename>{0}</filename> is corrupt: <message>{1}</message>.", file_name, str(e)), "Project file <filename>{0}</filename> is corrupt: <message>{1}</message>.", file_name, str(e)),
title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"), title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
message_type = Message.MessageType.ERROR) message_type = Message.MessageType.ERROR)
message.show() message.show()
self.setWorkspaceName("") self.setWorkspaceName("")
return [], {} return [], {}
@ -761,9 +819,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# Find the machine which will be overridden # Find the machine which will be overridden
global_stacks = self._container_registry.findContainerStacks(id = self._dialog.getMachineToOverride(), type = "machine") global_stacks = self._container_registry.findContainerStacks(id = self._dialog.getMachineToOverride(), type = "machine")
if not global_stacks: if not global_stacks:
message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tag <filename>!", message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tag <filename>!",
"Project file <filename>{0}</filename> is made using profiles that are unknown to this version of UltiMaker Cura.", file_name), "Project file <filename>{0}</filename> is made using profiles that are unknown to this version of UltiMaker Cura.", file_name),
message_type = Message.MessageType.ERROR) message_type = Message.MessageType.ERROR)
message.show() message.show()
self.setWorkspaceName("") self.setWorkspaceName("")
return [], {} return [], {}
@ -777,84 +835,90 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
for stack in extruder_stacks: for stack in extruder_stacks:
stack.setNextStack(global_stack, connect_signals = False) stack.setNextStack(global_stack, connect_signals = False)
Logger.log("d", "Workspace loading is checking definitions...") if not self._is_ucp:
# Get all the definition files & check if they exist. If not, add them. Logger.log("d", "Workspace loading is checking definitions...")
definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)] # Get all the definition files & check if they exist. If not, add them.
for definition_container_file in definition_container_files: definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
container_id = self._stripFileToId(definition_container_file) for definition_container_file in definition_container_files:
container_id = self._stripFileToId(definition_container_file)
definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id) definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id)
if not definitions: if not definitions:
definition_container = DefinitionContainer(container_id) definition_container = DefinitionContainer(container_id)
try:
definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"),
file_name = definition_container_file)
except ContainerFormatError:
# We cannot just skip the definition file because everything else later will just break if the
# machine definition cannot be found.
Logger.logException("e", "Failed to deserialize definition file %s in project file %s",
definition_container_file, file_name)
definition_container = self._container_registry.findDefinitionContainers(id = "fdmprinter")[0] #Fall back to defaults.
self._container_registry.addContainer(definition_container)
Job.yieldThread()
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
Logger.log("d", "Workspace loading is checking materials...")
# Get all the material files and check if they exist. If not, add them.
xml_material_profile = self._getXmlProfileClass()
if self._material_container_suffix is None:
self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0]
if xml_material_profile:
material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
for material_container_file in material_container_files:
to_deserialize_material = False
container_id = self._stripFileToId(material_container_file)
need_new_name = False
materials = self._container_registry.findInstanceContainers(id = container_id)
if not materials:
# No material found, deserialize this material later and add it
to_deserialize_material = True
else:
material_container = materials[0]
old_material_root_id = material_container.getMetaDataEntry("base_file")
if old_material_root_id is not None and not self._container_registry.isReadOnly(old_material_root_id): # Only create new materials if they are not read only.
to_deserialize_material = True
if self._resolve_strategies["material"] == "override":
# Remove the old materials and then deserialize the one from the project
root_material_id = material_container.getMetaDataEntry("base_file")
application.getContainerRegistry().removeContainer(root_material_id)
elif self._resolve_strategies["material"] == "new":
# Note that we *must* deserialize it with a new ID, as multiple containers will be
# auto created & added.
container_id = self.getNewId(container_id)
self._old_new_materials[old_material_root_id] = container_id
need_new_name = True
if to_deserialize_material:
material_container = xml_material_profile(container_id)
try: try:
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"), definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"),
file_name = container_id + "." + self._material_container_suffix) file_name = definition_container_file)
except ContainerFormatError: except ContainerFormatError:
Logger.logException("e", "Failed to deserialize material file %s in project file %s", # We cannot just skip the definition file because everything else later will just break if the
material_container_file, file_name) # machine definition cannot be found.
continue Logger.logException("e", "Failed to deserialize definition file %s in project file %s",
if need_new_name: definition_container_file, file_name)
new_name = ContainerRegistry.getInstance().uniqueName(material_container.getName()) definition_container = self._container_registry.findDefinitionContainers(id = "fdmprinter")[0] #Fall back to defaults.
material_container.setName(new_name) self._container_registry.addContainer(definition_container)
material_container.setDirty(True)
self._container_registry.addContainer(material_container)
Job.yieldThread() Job.yieldThread()
QCoreApplication.processEvents() # Ensure that the GUI does not freeze. QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
if global_stack: Logger.log("d", "Workspace loading is checking materials...")
# Handle quality changes if any # Get all the material files and check if they exist. If not, add them.
self._processQualityChanges(global_stack) xml_material_profile = self._getXmlProfileClass()
if self._material_container_suffix is None:
self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0]
if xml_material_profile:
material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
for material_container_file in material_container_files:
to_deserialize_material = False
container_id = self._stripFileToId(material_container_file)
need_new_name = False
materials = self._container_registry.findInstanceContainers(id = container_id)
if not materials:
# No material found, deserialize this material later and add it
to_deserialize_material = True
else:
material_container = materials[0]
old_material_root_id = material_container.getMetaDataEntry("base_file")
if old_material_root_id is not None and not self._container_registry.isReadOnly(old_material_root_id): # Only create new materials if they are not read only.
to_deserialize_material = True
if self._resolve_strategies["material"] == "override":
# Remove the old materials and then deserialize the one from the project
root_material_id = material_container.getMetaDataEntry("base_file")
application.getContainerRegistry().removeContainer(root_material_id)
elif self._resolve_strategies["material"] == "new":
# Note that we *must* deserialize it with a new ID, as multiple containers will be
# auto created & added.
container_id = self.getNewId(container_id)
self._old_new_materials[old_material_root_id] = container_id
need_new_name = True
if to_deserialize_material:
material_container = xml_material_profile(container_id)
try:
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"),
file_name = container_id + "." + self._material_container_suffix)
except ContainerFormatError:
Logger.logException("e", "Failed to deserialize material file %s in project file %s",
material_container_file, file_name)
continue
if need_new_name:
new_name = ContainerRegistry.getInstance().uniqueName(material_container.getName())
material_container.setName(new_name)
material_container.setDirty(True)
self._container_registry.addContainer(material_container)
Job.yieldThread()
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
if global_stack:
if not self._is_ucp:
# Handle quality changes if any
self._processQualityChanges(global_stack)
# Prepare the machine
self._applyChangesToMachine(global_stack, extruder_stack_dict)
else:
# Just clear the settings now, so that we can change the active machine without conflicts
self._clearMachineSettings(global_stack, {})
# Prepare the machine
self._applyChangesToMachine(global_stack, extruder_stack_dict)
Logger.log("d", "Workspace loading is notifying rest of the code of changes...") Logger.log("d", "Workspace loading is notifying rest of the code of changes...")
# Actually change the active machine. # Actually change the active machine.
@ -864,8 +928,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# function is running on the main thread (Qt thread), although those "changed" signals have been emitted, but # function is running on the main thread (Qt thread), although those "changed" signals have been emitted, but
# they won't take effect until this function is done. # they won't take effect until this function is done.
# To solve this, we schedule _updateActiveMachine() for later so it will have the latest data. # To solve this, we schedule _updateActiveMachine() for later so it will have the latest data.
self._updateActiveMachine(global_stack) self._updateActiveMachine(global_stack)
if self._is_ucp:
# Now we have switched, apply the user settings
self._applyUserSettings(global_stack, extruder_stack_dict, self._user_settings)
# Load all the nodes / mesh data of the workspace # Load all the nodes / mesh data of the workspace
nodes = self._3mf_mesh_reader.read(file_name) nodes = self._3mf_mesh_reader.read(file_name)
if nodes is None: if nodes is None:
@ -874,6 +943,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
base_file_name = os.path.basename(file_name) base_file_name = os.path.basename(file_name)
self.setWorkspaceName(base_file_name) self.setWorkspaceName(base_file_name)
self._is_ucp = None
return nodes, self._loadMetadata(file_name) return nodes, self._loadMetadata(file_name)
@staticmethod @staticmethod
@ -1177,21 +1247,44 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
material_node = machine_node.variants[extruder_stack.variant.getName()].materials[root_material_id] material_node = machine_node.variants[extruder_stack.variant.getName()].materials[root_material_id]
extruder_stack.material = material_node.container extruder_stack.material = material_node.container
def _applyChangesToMachine(self, global_stack, extruder_stack_dict): def _clearMachineSettings(self, global_stack, extruder_stack_dict):
# Clear all first
self._clearStack(global_stack) self._clearStack(global_stack)
for extruder_stack in extruder_stack_dict.values(): for extruder_stack in extruder_stack_dict.values():
self._clearStack(extruder_stack) self._clearStack(extruder_stack)
self._quality_changes_to_apply = None
self._quality_type_to_apply = None
self._intent_category_to_apply = None
self._user_settings_to_apply = None
def _applyUserSettings(self, global_stack, extruder_stack_dict, user_settings):
for stack_name, settings in user_settings.items():
if stack_name == 'global':
ThreeMFWorkspaceReader._applyUserSettingsOnStack(global_stack, settings)
else:
extruder_match = re.fullmatch('extruder_([0-9]+)', stack_name)
if extruder_match is not None:
extruder_nr = extruder_match.group(1)
if extruder_nr in extruder_stack_dict:
ThreeMFWorkspaceReader._applyUserSettingsOnStack(extruder_stack_dict[extruder_nr], settings)
@staticmethod
def _applyUserSettingsOnStack(stack, user_settings):
user_settings_container = stack.userChanges
for setting_to_import, setting_value in user_settings.items():
user_settings_container.setProperty(setting_to_import, 'value', setting_value)
def _applyChangesToMachine(self, global_stack, extruder_stack_dict):
# Clear all first
self._clearMachineSettings(global_stack, extruder_stack_dict)
self._applyDefinitionChanges(global_stack, extruder_stack_dict) self._applyDefinitionChanges(global_stack, extruder_stack_dict)
self._applyUserChanges(global_stack, extruder_stack_dict) self._applyUserChanges(global_stack, extruder_stack_dict)
self._applyVariants(global_stack, extruder_stack_dict) self._applyVariants(global_stack, extruder_stack_dict)
self._applyMaterials(global_stack, extruder_stack_dict) self._applyMaterials(global_stack, extruder_stack_dict)
# prepare the quality to select # prepare the quality to select
self._quality_changes_to_apply = None
self._quality_type_to_apply = None
self._intent_category_to_apply = None
if self._machine_info.quality_changes_info is not None: if self._machine_info.quality_changes_info is not None:
self._quality_changes_to_apply = self._machine_info.quality_changes_info.name self._quality_changes_to_apply = self._machine_info.quality_changes_info.name
else: else:
@ -1229,39 +1322,40 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
machine_manager.setActiveMachine(global_stack.getId()) machine_manager.setActiveMachine(global_stack.getId())
# Set metadata fields that are missing from the global stack # Set metadata fields that are missing from the global stack
for key, value in self._machine_info.metadata_dict.items(): if not self._is_ucp:
if key not in global_stack.getMetaData() and key not in _ignored_machine_network_metadata: for key, value in self._machine_info.metadata_dict.items():
global_stack.setMetaDataEntry(key, value) if key not in global_stack.getMetaData() and key not in _ignored_machine_network_metadata:
global_stack.setMetaDataEntry(key, value)
if self._quality_changes_to_apply: if self._quality_changes_to_apply !=None:
quality_changes_group_list = container_tree.getCurrentQualityChangesGroups() quality_changes_group_list = container_tree.getCurrentQualityChangesGroups()
quality_changes_group = next((qcg for qcg in quality_changes_group_list if qcg.name == self._quality_changes_to_apply), None) quality_changes_group = next((qcg for qcg in quality_changes_group_list if qcg.name == self._quality_changes_to_apply), None)
if not quality_changes_group: if not quality_changes_group:
Logger.log("e", "Could not find quality_changes [%s]", self._quality_changes_to_apply) Logger.log("e", "Could not find quality_changes [%s]", self._quality_changes_to_apply)
return return
machine_manager.setQualityChangesGroup(quality_changes_group, no_dialog = True) machine_manager.setQualityChangesGroup(quality_changes_group, no_dialog = True)
else:
self._quality_type_to_apply = self._quality_type_to_apply.lower() if self._quality_type_to_apply else None
quality_group_dict = container_tree.getCurrentQualityGroups()
if self._quality_type_to_apply in quality_group_dict:
quality_group = quality_group_dict[self._quality_type_to_apply]
else: else:
Logger.log("i", "Could not find quality type [%s], switch to default", self._quality_type_to_apply) self._quality_type_to_apply = self._quality_type_to_apply.lower() if self._quality_type_to_apply else None
preferred_quality_type = global_stack.getMetaDataEntry("preferred_quality_type") quality_group_dict = container_tree.getCurrentQualityGroups()
quality_group = quality_group_dict.get(preferred_quality_type) if self._quality_type_to_apply in quality_group_dict:
if quality_group is None: quality_group = quality_group_dict[self._quality_type_to_apply]
Logger.log("e", "Could not get preferred quality type [%s]", preferred_quality_type)
if quality_group is not None:
machine_manager.setQualityGroup(quality_group, no_dialog = True)
# Also apply intent if available
available_intent_category_list = IntentManager.getInstance().currentAvailableIntentCategories()
if self._intent_category_to_apply is not None and self._intent_category_to_apply in available_intent_category_list:
machine_manager.setIntentByCategory(self._intent_category_to_apply)
else: else:
# if no intent is provided, reset to the default (balanced) intent Logger.log("i", "Could not find quality type [%s], switch to default", self._quality_type_to_apply)
machine_manager.resetIntents() preferred_quality_type = global_stack.getMetaDataEntry("preferred_quality_type")
quality_group = quality_group_dict.get(preferred_quality_type)
if quality_group is None:
Logger.log("e", "Could not get preferred quality type [%s]", preferred_quality_type)
if quality_group is not None:
machine_manager.setQualityGroup(quality_group, no_dialog = True)
# Also apply intent if available
available_intent_category_list = IntentManager.getInstance().currentAvailableIntentCategories()
if self._intent_category_to_apply is not None and self._intent_category_to_apply in available_intent_category_list:
machine_manager.setIntentByCategory(self._intent_category_to_apply)
else:
# if no intent is provided, reset to the default (balanced) intent
machine_manager.resetIntents()
# Notify everything/one that is to notify about changes. # Notify everything/one that is to notify about changes.
global_stack.containersChanged.emit(global_stack.getTop()) global_stack.containersChanged.emit(global_stack.getTop())

View file

@ -22,6 +22,8 @@ import time
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from .SpecificSettingsModel import SpecificSettingsModel
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
@ -61,16 +63,22 @@ class WorkspaceDialog(QObject):
self._machine_name = "" self._machine_name = ""
self._machine_type = "" self._machine_type = ""
self._variant_type = "" self._variant_type = ""
self._current_machine_name = ""
self._material_labels = [] self._material_labels = []
self._extruders = [] self._extruders = []
self._objects_on_plate = False self._objects_on_plate = False
self._is_printer_group = False self._is_printer_group = False
self._updatable_machines_model = MachineListModel(self, listenToChanges=False) self._updatable_machines_model = MachineListModel(self, listenToChanges = False, showCloudPrinters = True)
self._missing_package_metadata: List[Dict[str, str]] = [] self._missing_package_metadata: List[Dict[str, str]] = []
self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry() self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry()
self._install_missing_package_dialog: Optional[QObject] = None self._install_missing_package_dialog: Optional[QObject] = None
self._is_abstract_machine = False self._is_abstract_machine = False
self._is_networked_machine = False self._is_networked_machine = False
self._is_compatible_machine = False
self._allow_create_machine = True
self._exported_settings_model = SpecificSettingsModel()
self._current_machine_pos_index = 0
self._is_ucp = False
machineConflictChanged = pyqtSignal() machineConflictChanged = pyqtSignal()
qualityChangesConflictChanged = pyqtSignal() qualityChangesConflictChanged = pyqtSignal()
@ -94,6 +102,8 @@ class WorkspaceDialog(QObject):
extrudersChanged = pyqtSignal() extrudersChanged = pyqtSignal()
isPrinterGroupChanged = pyqtSignal() isPrinterGroupChanged = pyqtSignal()
missingPackagesChanged = pyqtSignal() missingPackagesChanged = pyqtSignal()
isCompatibleMachineChanged = pyqtSignal()
isUcpChanged = pyqtSignal()
@pyqtProperty(bool, notify = isPrinterGroupChanged) @pyqtProperty(bool, notify = isPrinterGroupChanged)
def isPrinterGroup(self) -> bool: def isPrinterGroup(self) -> bool:
@ -166,8 +176,30 @@ class WorkspaceDialog(QObject):
self._machine_name = machine_name self._machine_name = machine_name
self.machineNameChanged.emit() self.machineNameChanged.emit()
def setCurrentMachineName(self, machine: str) -> None:
self._current_machine_name = machine
@pyqtProperty(str, notify = machineNameChanged)
def currentMachineName(self) -> str:
return self._current_machine_name
@staticmethod
def getIndexOfCurrentMachine(list_of_dicts, key, value, defaultIndex):
for i, d in enumerate(list_of_dicts):
if d.get(key) == value: # found the dictionary
return i
return defaultIndex
@pyqtProperty(int, notify = machineNameChanged)
def currentMachinePositionIndex(self):
return self._current_machine_pos_index
@pyqtProperty(QObject, notify = updatableMachinesChanged) @pyqtProperty(QObject, notify = updatableMachinesChanged)
def updatableMachinesModel(self) -> MachineListModel: def updatableMachinesModel(self) -> MachineListModel:
if self._current_machine_name != "":
self._current_machine_pos_index = self.getIndexOfCurrentMachine(self._updatable_machines_model.getItems(), "id", self._current_machine_name, defaultIndex = 0)
else:
self._current_machine_pos_index = 0
return cast(MachineListModel, self._updatable_machines_model) return cast(MachineListModel, self._updatable_machines_model)
def setUpdatableMachines(self, updatable_machines: List[GlobalStack]) -> None: def setUpdatableMachines(self, updatable_machines: List[GlobalStack]) -> None:
@ -292,6 +324,41 @@ class WorkspaceDialog(QObject):
@pyqtSlot(str) @pyqtSlot(str)
def setMachineToOverride(self, machine_name: str) -> None: def setMachineToOverride(self, machine_name: str) -> None:
self._override_machine = machine_name self._override_machine = machine_name
self.updateCompatibleMachine()
def updateCompatibleMachine(self):
registry = ContainerRegistry.getInstance()
containers_expected = registry.findDefinitionContainers(name=self._machine_type)
containers_selected = registry.findContainerStacks(id=self._override_machine)
if len(containers_expected) == 1 and len(containers_selected) == 1:
new_compatible_machine = (containers_expected[0] == containers_selected[0].definition)
if new_compatible_machine != self._is_compatible_machine:
self._is_compatible_machine = new_compatible_machine
self.isCompatibleMachineChanged.emit()
@pyqtProperty(bool, notify = isCompatibleMachineChanged)
def isCompatibleMachine(self) -> bool:
return self._is_compatible_machine
def setIsUcp(self, isUcp: bool) -> None:
if isUcp != self._is_ucp:
self._is_ucp = isUcp
self.isUcpChanged.emit()
@pyqtProperty(bool, notify=isUcpChanged)
def isUcp(self):
return self._is_ucp
def setAllowCreatemachine(self, allow_create_machine):
self._allow_create_machine = allow_create_machine
@pyqtProperty(bool, constant = True)
def allowCreateMachine(self):
return self._allow_create_machine
@pyqtProperty(QObject)
def exportedSettingModel(self):
return self._exported_settings_model
@pyqtSlot() @pyqtSlot()
def closeBackend(self) -> None: def closeBackend(self) -> None:

View file

@ -6,13 +6,13 @@ import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import UM 1.5 as UM import UM 1.6 as UM
import Cura 1.1 as Cura import Cura 1.1 as Cura
UM.Dialog UM.Dialog
{ {
id: workspaceDialog id: workspaceDialog
title: catalog.i18nc("@title:window", "Open Project") title: manager.isUcp? catalog.i18nc("@title:window", "Open Universal Cura Project (UCP)"): catalog.i18nc("@title:window", "Open Project")
margin: UM.Theme.getSize("default_margin").width margin: UM.Theme.getSize("default_margin").width
minimumWidth: UM.Theme.getSize("modal_window_minimum").width minimumWidth: UM.Theme.getSize("modal_window_minimum").width
@ -28,7 +28,7 @@ UM.Dialog
UM.Label UM.Label
{ {
id: titleLabel id: titleLabel
text: catalog.i18nc("@action:title", "Summary - Cura Project") text: manager.isUcp? catalog.i18nc("@action:title", "Summary - Open Universal Cura Project (UCP)"): catalog.i18nc("@action:title", "Summary - Cura Project")
font: UM.Theme.getFont("large") font: UM.Theme.getFont("large")
anchors.top: parent.top anchors.top: parent.top
anchors.left: parent.left anchors.left: parent.left
@ -96,7 +96,7 @@ UM.Dialog
WorkspaceRow WorkspaceRow
{ {
leftLabelText: catalog.i18nc("@action:label", manager.isPrinterGroup ? "Printer Group" : "Printer Name") leftLabelText: catalog.i18nc("@action:label", manager.isPrinterGroup ? "Printer Group" : "Printer Name")
rightLabelText: manager.machineName == catalog.i18nc("@button", "Create new") ? "" : manager.machineName rightLabelText: manager.isUcp? manager.machineType: manager.machineName == catalog.i18nc("@button", "Create new") ? "" : manager.machineName
} }
} }
@ -120,13 +120,17 @@ UM.Dialog
minDropDownWidth: machineSelector.width minDropDownWidth: machineSelector.width
buttons: [ Component
{
id: componentNewPrinter
Cura.SecondaryButton Cura.SecondaryButton
{ {
id: createNewPrinter id: createNewPrinter
text: catalog.i18nc("@button", "Create new") text: catalog.i18nc("@button", "Create new")
fixedWidthMode: true fixedWidthMode: true
width: parent.width - leftPadding * 1.5 width: parent.width - leftPadding * 1.5
visible: manager.allowCreateMachine
onClicked: onClicked:
{ {
toggleContent() toggleContent()
@ -136,7 +140,9 @@ UM.Dialog
manager.setIsNetworkedMachine(false) manager.setIsNetworkedMachine(false)
} }
} }
] }
buttons: manager.allowCreateMachine ? [componentNewPrinter.createObject()] : []
onSelectPrinter: function(machine) onSelectPrinter: function(machine)
{ {
@ -153,7 +159,7 @@ UM.Dialog
WorkspaceSection WorkspaceSection
{ {
id: profileSection id: profileSection
title: catalog.i18nc("@action:label", "Profile settings") title: manager.isUcp? catalog.i18nc("@action:label", "Suggested Profile settings"):catalog.i18nc("@action:label", "Profile settings")
iconSource: UM.Theme.getIcon("Sliders") iconSource: UM.Theme.getIcon("Sliders")
content: Column content: Column
{ {
@ -165,26 +171,79 @@ UM.Dialog
{ {
leftLabelText: catalog.i18nc("@action:label", "Name") leftLabelText: catalog.i18nc("@action:label", "Name")
rightLabelText: manager.qualityName rightLabelText: manager.qualityName
visible: manager.isCompatibleMachine
} }
WorkspaceRow WorkspaceRow
{ {
leftLabelText: catalog.i18nc("@action:label", "Intent") leftLabelText: catalog.i18nc("@action:label", "Intent")
rightLabelText: manager.intentName rightLabelText: manager.intentName
visible: manager.isCompatibleMachine
} }
WorkspaceRow WorkspaceRow
{ {
leftLabelText: catalog.i18nc("@action:label", "Not in profile") leftLabelText: catalog.i18nc("@action:label", "Not in profile")
rightLabelText: catalog.i18ncp("@action:label", "%1 override", "%1 overrides", manager.numUserSettings).arg(manager.numUserSettings) rightLabelText: catalog.i18ncp("@action:label", "%1 override", "%1 overrides", manager.numUserSettings).arg(manager.numUserSettings)
visible: manager.numUserSettings != 0 visible: manager.numUserSettings != 0 && manager.isCompatibleMachine
} }
WorkspaceRow WorkspaceRow
{ {
leftLabelText: catalog.i18nc("@action:label", "Derivative from") leftLabelText: catalog.i18nc("@action:label", "Derivative from")
rightLabelText: catalog.i18ncp("@action:label", "%1, %2 override", "%1, %2 overrides", manager.numSettingsOverridenByQualityChanges).arg(manager.qualityType).arg(manager.numSettingsOverridenByQualityChanges) rightLabelText: catalog.i18ncp("@action:label", "%1, %2 override", "%1, %2 overrides", manager.numSettingsOverridenByQualityChanges).arg(manager.qualityType).arg(manager.numSettingsOverridenByQualityChanges)
visible: manager.numSettingsOverridenByQualityChanges != 0 visible: manager.numSettingsOverridenByQualityChanges != 0 && manager.isCompatibleMachine
}
}
}
WorkspaceSection
{
id: ucpProfileSection
visible: manager.isUcp
title: catalog.i18nc("@action:label", "Settings Loaded from UCP file")
iconSource: UM.Theme.getIcon("Settings")
content: Column
{
id: ucpProfileSettingsValuesTable
spacing: UM.Theme.getSize("default_margin").height
leftPadding: UM.Theme.getSize("medium_button_icon").width + UM.Theme.getSize("default_margin").width
WorkspaceRow
{
leftLabelText: catalog.i18nc("@action:label", "Settings Loaded from UCP file")
rightLabelText: catalog.i18ncp("@action:label", "%1 override", "%1 overrides", manager.exportedSettingModel.rowCount()).arg(manager.exportedSettingModel.rowCount())
buttonText: tableViewSpecificSettings.shouldBeVisible ? catalog.i18nc("@action:button", "Hide settings") : catalog.i18nc("@action:button", "Show settings")
onButtonClicked: tableViewSpecificSettings.shouldBeVisible = !tableViewSpecificSettings.shouldBeVisible
}
Cura.TableView
{
id: tableViewSpecificSettings
width: parent.width - parent.leftPadding - UM.Theme.getSize("default_margin").width
height: UM.Theme.getSize("card").height
visible: shouldBeVisible && manager.isUcp
property bool shouldBeVisible: true
columnHeaders:
[
catalog.i18nc("@title:column", "Applies on"),
catalog.i18nc("@title:column", "Setting"),
catalog.i18nc("@title:column", "Value")
]
model: UM.TableModel
{
id: tableModel
headers: ["category", "label", "value"]
rows: manager.exportedSettingModel.items
}
}
property var modelRows: manager.exportedSettingModel.items
onModelRowsChanged:
{
tableModel.clear()
tableModel.rows = modelRows
} }
} }
@ -194,7 +253,7 @@ UM.Dialog
id: qualityChangesResolveComboBox id: qualityChangesResolveComboBox
model: resolveStrategiesModel model: resolveStrategiesModel
textRole: "label" textRole: "label"
visible: manager.qualityChangesConflict visible: manager.qualityChangesConflict && !manager.isUcp
contentLeftPadding: UM.Theme.getSize("default_margin").width + UM.Theme.getSize("narrow_margin").width contentLeftPadding: UM.Theme.getSize("default_margin").width + UM.Theme.getSize("narrow_margin").width
textFont: UM.Theme.getFont("medium") textFont: UM.Theme.getFont("medium")
@ -223,7 +282,7 @@ UM.Dialog
WorkspaceSection WorkspaceSection
{ {
id: materialSection id: materialSection
title: catalog.i18nc("@action:label", "Material settings") title: manager.isUcp? catalog.i18nc("@action:label", "Suggested Material settings"): catalog.i18nc("@action:label", "Material settings")
iconSource: UM.Theme.getIcon("Spool") iconSource: UM.Theme.getIcon("Spool")
content: Column content: Column
{ {
@ -248,7 +307,7 @@ UM.Dialog
id: materialResolveComboBox id: materialResolveComboBox
model: resolveStrategiesModel model: resolveStrategiesModel
textRole: "label" textRole: "label"
visible: manager.materialConflict visible: manager.materialConflict && !manager.isUcp
contentLeftPadding: UM.Theme.getSize("default_margin").width + UM.Theme.getSize("narrow_margin").width contentLeftPadding: UM.Theme.getSize("default_margin").width + UM.Theme.getSize("narrow_margin").width
textFont: UM.Theme.getFont("medium") textFont: UM.Theme.getFont("medium")
@ -279,6 +338,7 @@ UM.Dialog
id: visibilitySection id: visibilitySection
title: catalog.i18nc("@action:label", "Setting visibility") title: catalog.i18nc("@action:label", "Setting visibility")
iconSource: UM.Theme.getIcon("Eye") iconSource: UM.Theme.getIcon("Eye")
visible : !manager.isUcp
content: Column content: Column
{ {
spacing: UM.Theme.getSize("default_margin").height spacing: UM.Theme.getSize("default_margin").height
@ -416,12 +476,13 @@ UM.Dialog
{ {
if (visible) if (visible)
{ {
// Force relead the comboboxes // Force reload the comboboxes
// Since this dialog is only created once the first time you open it, these comboxes need to be reloaded // Since this dialog is only created once the first time you open it, these comboxes need to be reloaded
// each time it is shown after the first time so that the indexes will update correctly. // each time it is shown after the first time so that the indexes will update correctly.
materialSection.reloadValues() materialSection.reloadValues()
profileSection.reloadValues() profileSection.reloadValues()
printerSection.reloadValues() printerSection.reloadValues()
ucpProfileSection.reloadValues()
} }
} }
} }

View file

@ -9,26 +9,38 @@ import QtQuick.Window 2.2
import UM 1.5 as UM import UM 1.5 as UM
import Cura 1.1 as Cura import Cura 1.1 as Cura
Row RowLayout
{ {
id: root
property alias leftLabelText: leftLabel.text property alias leftLabelText: leftLabel.text
property alias rightLabelText: rightLabel.text property alias rightLabelText: rightLabel.text
property alias buttonText: button.text
signal buttonClicked
width: parent.width width: parent.width
height: visible ? childrenRect.height : 0
UM.Label UM.Label
{ {
id: leftLabel id: leftLabel
text: catalog.i18nc("@action:label", "Type") text: catalog.i18nc("@action:label", "Type")
width: Math.round(parent.width / 4) Layout.preferredWidth: Math.round(parent.width / 4)
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }
UM.Label UM.Label
{ {
id: rightLabel id: rightLabel
text: manager.machineType text: manager.machineType
width: Math.round(parent.width / 3)
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }
Cura.TertiaryButton
{
id: button
visible: !text.isEmpty
Layout.maximumHeight: leftLabel.implicitHeight
Layout.fillWidth: true
onClicked: root.buttonClicked()
}
} }

View file

@ -5,7 +5,7 @@ import QtQuick 2.10
import QtQuick.Controls 2.3 import QtQuick.Controls 2.3
import UM 1.5 as UM import UM 1.8 as UM
Item Item
@ -80,34 +80,14 @@ Item
sourceComponent: combobox sourceComponent: combobox
} }
MouseArea UM.HelpIcon
{ {
id: helpIconMouseArea
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: comboboxLabel.verticalCenter anchors.verticalCenter: comboboxLabel.verticalCenter
width: childrenRect.width color: UM.Theme.getColor("small_button_text")
height: childrenRect.height icon: UM.Theme.getIcon("Information")
hoverEnabled: true text: comboboxTooltipText
visible: comboboxTooltipText != ""
UM.ColorImage
{
width: UM.Theme.getSize("section_icon").width
height: width
visible: comboboxTooltipText != ""
source: UM.Theme.getIcon("Help")
color: UM.Theme.getColor("text")
UM.ToolTip
{
text: comboboxTooltipText
visible: helpIconMouseArea.containsMouse
targetPoint: Qt.point(parent.x + Math.round(parent.width / 2), parent.y)
x: 0
y: parent.y + parent.height + UM.Theme.getSize("default_margin").height
width: UM.Theme.getSize("tooltip").width
}
}
} }
} }

View file

@ -0,0 +1,38 @@
# Copyright (c) 2024 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal
class SettingExport(QObject):
def __init__(self, id, name, value, selectable):
super().__init__()
self.id = id
self._name = name
self._value = value
self._selected = selectable
self._selectable = selectable
@pyqtProperty(str, constant=True)
def name(self):
return self._name
@pyqtProperty(str, constant=True)
def value(self):
return self._value
selectedChanged = pyqtSignal(bool)
def setSelected(self, selected):
if selected != self._selected:
self._selected = selected
self.selectedChanged.emit(self._selected)
@pyqtProperty(bool, fset = setSelected, notify = selectedChanged)
def selected(self):
return self._selected
@pyqtProperty(bool, constant=True)
def selectable(self):
return self._selectable

View file

@ -0,0 +1,39 @@
// Copyright (c) 2024 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
import UM 1.8 as UM
import Cura 1.1 as Cura
RowLayout
{
id: settingSelection
UM.CheckBox
{
text: modelData.name
Layout.preferredWidth: UM.Theme.getSize("setting").width
checked: modelData.selected
onClicked: modelData.selected = checked
tooltip: modelData.selectable ? "" :catalog.i18nc("@tooltip", "This setting may not perform well while exporting to UCP. Users are asked to add it at their own risk.")
}
UM.Label
{
text: modelData.value
}
UM.HelpIcon
{
UM.I18nCatalog { id: catalog; name: "cura" }
text: catalog.i18nc("@tooltip",
"This setting may not perform well while exporting to UCP, Users are asked to add it at their own risk.")
visible: !modelData.selectable
}
}

View file

@ -0,0 +1,49 @@
# Copyright (c) 2024 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from enum import IntEnum
from PyQt6.QtCore import QObject, pyqtProperty, pyqtEnum
class SettingsExportGroup(QObject):
@pyqtEnum
class Category(IntEnum):
Global = 0
Extruder = 1
Model = 2
def __init__(self, stack, name, category, settings, category_details = '', extruder_index = 0, extruder_color = ''):
super().__init__()
self.stack = stack
self._name = name
self._settings = settings
self._category = category
self._category_details = category_details
self._extruder_index = extruder_index
self._extruder_color = extruder_color
@pyqtProperty(str, constant=True)
def name(self):
return self._name
@pyqtProperty(list, constant=True)
def settings(self):
return self._settings
@pyqtProperty(int, constant=True)
def category(self):
return self._category
@pyqtProperty(str, constant=True)
def category_details(self):
return self._category_details
@pyqtProperty(int, constant=True)
def extruder_index(self):
return self._extruder_index
@pyqtProperty(str, constant=True)
def extruder_color(self):
return self._extruder_color

View file

@ -0,0 +1,135 @@
# Copyright (c) 2024 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from dataclasses import asdict
from typing import Optional, cast, List, Dict, Pattern, Set
from PyQt6.QtCore import QObject, pyqtProperty
from UM.Settings.SettingDefinition import SettingDefinition
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.SettingFunction import SettingFunction
from cura.CuraApplication import CuraApplication
from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.GlobalStack import GlobalStack
from .SettingsExportGroup import SettingsExportGroup
from .SettingExport import SettingExport
class SettingsExportModel(QObject):
EXPORTABLE_SETTINGS = {'infill_sparse_density',
'adhesion_type',
'support_enable',
'infill_pattern',
'support_type',
'support_structure',
'support_angle',
'support_infill_rate',
'ironing_enabled',
'fill_outline_gaps',
'coasting_enable',
'skin_monotonic',
'z_seam_position',
'infill_before_walls',
'ironing_only_highest_layer',
'xy_offset',
'adaptive_layer_height_enabled',
'brim_gap',
'support_offset',
'brim_outside_only',
'magic_spiralize',
'slicing_tolerance',
'outer_inset_first',
'magic_fuzzy_skin_outside_only',
'conical_overhang_enabled',
'min_infill_area',
'small_hole_max_size',
'magic_mesh_surface_mode',
'carve_multiple_volumes',
'meshfix_union_all_remove_holes',
'support_tree_rest_preference',
'small_feature_max_length',
'draft_shield_enabled',
'brim_smart_ordering',
'ooze_shield_enabled',
'bottom_skin_preshrink',
'skin_edge_support_thickness',
'alternate_carve_order',
'top_skin_preshrink',
'interlocking_enable'}
PER_MODEL_EXPORTABLE_SETTINGS_KEYS = {"anti_overhang_mesh",
"infill_mesh",
"cutting_mesh",
"support_mesh"}
def __init__(self, parent=None):
super().__init__(parent)
self._settings_groups = []
application = CuraApplication.getInstance()
self._appendGlobalSettings(application)
self._appendExtruderSettings(application)
self._appendModelSettings(application)
def _appendGlobalSettings(self, application):
global_stack = application.getGlobalContainerStack()
self._settings_groups.append(SettingsExportGroup(
global_stack, "Global settings", SettingsExportGroup.Category.Global, self._exportSettings(global_stack)))
def _appendExtruderSettings(self, application):
extruders_stacks = ExtruderManager.getInstance().getUsedExtruderStacks()
for extruder_stack in extruders_stacks:
color = extruder_stack.material.getMetaDataEntry("color_code") if extruder_stack.material else ""
self._settings_groups.append(SettingsExportGroup(
extruder_stack, "Extruder settings", SettingsExportGroup.Category.Extruder,
self._exportSettings(extruder_stack), extruder_index=extruder_stack.position, extruder_color=color))
def _appendModelSettings(self, application):
scene = application.getController().getScene()
for scene_node in scene.getRoot().getChildren():
self._appendNodeSettings(scene_node, "Model settings", SettingsExportGroup.Category.Model)
def _appendNodeSettings(self, node, title_prefix, category):
stack = node.callDecoration("getStack")
if stack:
self._settings_groups.append(SettingsExportGroup(
stack, f"{title_prefix}", category, self._exportSettings(stack), node.getName()))
for child in node.getChildren():
self._appendNodeSettings(child, f"Children of {node.getName()}", SettingsExportGroup.Category.Model)
@pyqtProperty(list, constant=True)
def settingsGroups(self) -> List[SettingsExportGroup]:
return self._settings_groups
@staticmethod
def _exportSettings(settings_stack):
user_settings_container = settings_stack.userChanges
user_keys = user_settings_container.getAllKeys()
exportable_settings = SettingsExportModel.EXPORTABLE_SETTINGS
settings_export = []
# Check whether any of the user keys exist in PER_MODEL_EXPORTABLE_SETTINGS_KEYS
is_exportable = any(key in SettingsExportModel.PER_MODEL_EXPORTABLE_SETTINGS_KEYS for key in user_keys)
for setting_to_export in user_keys:
label = settings_stack.getProperty(setting_to_export, "label")
value = settings_stack.getProperty(setting_to_export, "value")
unit = settings_stack.getProperty(setting_to_export, "unit")
setting_type = settings_stack.getProperty(setting_to_export, "type")
if setting_type is not None:
value = f"{str(SettingDefinition.settingValueToString(setting_type, value))} {unit}"
else:
value = str(value)
settings_export.append(SettingExport(setting_to_export,
label,
value,
is_exportable or setting_to_export in exportable_settings))
return settings_export

View file

@ -0,0 +1,87 @@
// Copyright (c) 2024 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
import UM 1.5 as UM
import Cura 1.1 as Cura
import ThreeMFWriter 1.0 as ThreeMFWriter
ColumnLayout
{
id: settingsGroup
spacing: UM.Theme.getSize("narrow_margin").width
RowLayout
{
id: settingsGroupTitleRow
spacing: UM.Theme.getSize("default_margin").width
Item
{
id: icon
height: UM.Theme.getSize("medium_button_icon").height
width: height
UM.ColorImage
{
id: settingsMainImage
anchors.fill: parent
source:
{
switch(modelData.category)
{
case ThreeMFWriter.SettingsExportGroup.Global:
return UM.Theme.getIcon("Sliders")
case ThreeMFWriter.SettingsExportGroup.Model:
return UM.Theme.getIcon("View3D")
default:
return ""
}
}
color: UM.Theme.getColor("text")
}
Cura.ExtruderIcon
{
id: settingsExtruderIcon
anchors.fill: parent
visible: modelData.category === ThreeMFWriter.SettingsExportGroup.Extruder
text: (modelData.extruder_index + 1).toString()
font: UM.Theme.getFont("tiny_emphasis")
materialColor: modelData.extruder_color
}
}
UM.Label
{
id: settingsTitle
text: modelData.name + (modelData.category_details ? ' (%1)'.arg(modelData.category_details) : '')
font: UM.Theme.getFont("default_bold")
}
}
ListView
{
id: settingsExportList
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
spacing: 0
model: modelData.settings
visible: modelData.settings.length > 0
delegate: SettingSelection { }
}
UM.Label
{
UM.I18nCatalog { id: catalog; name: "cura" }
text: catalog.i18nc("@label", "No specific value has been set")
visible: modelData.settings.length === 0
}
}

View file

@ -1,9 +1,13 @@
# Copyright (c) 2020 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
import configparser import configparser
from io import StringIO from io import StringIO
from threading import Lock
import zipfile import zipfile
from typing import Dict, Any
from UM.Application import Application from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
@ -13,15 +17,23 @@ from UM.Workspace.WorkspaceWriter import WorkspaceWriter
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
from cura.Utils.Threading import call_on_qt_thread from .ThreeMFWriter import ThreeMFWriter
from .SettingsExportModel import SettingsExportModel
from .SettingsExportGroup import SettingsExportGroup
USER_SETTINGS_PATH = "Cura/user-settings.json"
class ThreeMFWorkspaceWriter(WorkspaceWriter): class ThreeMFWorkspaceWriter(WorkspaceWriter):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._ucp_model: Optional[SettingsExportModel] = None
@call_on_qt_thread def setExportModel(self, model: SettingsExportModel) -> None:
def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode): if self._ucp_model != model:
self._ucp_model = model
def _write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode):
application = Application.getInstance() application = Application.getInstance()
machine_manager = application.getMachineManager() machine_manager = application.getMachineManager()
@ -34,20 +46,20 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
global_stack = machine_manager.activeMachine global_stack = machine_manager.activeMachine
if global_stack is None: if global_stack is None:
self.setInformation(catalog.i18nc("@error", "There is no workspace yet to write. Please add a printer first.")) self.setInformation(
catalog.i18nc("@error", "There is no workspace yet to write. Please add a printer first."))
Logger.error("Tried to write a 3MF workspace before there was a global stack.") Logger.error("Tried to write a 3MF workspace before there was a global stack.")
return False return False
# Indicate that the 3mf mesh writer should not close the archive just yet (we still need to add stuff to it). # Indicate that the 3mf mesh writer should not close the archive just yet (we still need to add stuff to it).
mesh_writer.setStoreArchive(True) mesh_writer.setStoreArchive(True)
if not mesh_writer.write(stream, nodes, mode): if not mesh_writer.write(stream, nodes, mode, self._ucp_model):
self.setInformation(mesh_writer.getInformation()) self.setInformation(mesh_writer.getInformation())
return False return False
archive = mesh_writer.getArchive() archive = mesh_writer.getArchive()
if archive is None: # This happens if there was no mesh data to write. if archive is None: # This happens if there was no mesh data to write.
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED) archive = zipfile.ZipFile(stream, "w", compression=zipfile.ZIP_DEFLATED)
try: try:
# Add global container stack data to the archive. # Add global container stack data to the archive.
@ -62,15 +74,21 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
self._writeContainerToArchive(extruder_stack, archive) self._writeContainerToArchive(extruder_stack, archive)
for container in extruder_stack.getContainers(): for container in extruder_stack.getContainers():
self._writeContainerToArchive(container, archive) self._writeContainerToArchive(container, archive)
# Write user settings data
if self._ucp_model is not None:
user_settings_data = self._getUserSettings(self._ucp_model)
ThreeMFWriter._storeMetadataJson(user_settings_data, archive, USER_SETTINGS_PATH)
except PermissionError: except PermissionError:
self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here.")) self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here."))
Logger.error("No permission to write workspace to this stream.") Logger.error("No permission to write workspace to this stream.")
return False return False
# Write preferences to archive # Write preferences to archive
original_preferences = Application.getInstance().getPreferences() #Copy only the preferences that we use to the workspace. original_preferences = Application.getInstance().getPreferences() # Copy only the preferences that we use to the workspace.
temp_preferences = Preferences() temp_preferences = Preferences()
for preference in {"general/visible_settings", "cura/active_mode", "cura/categories_expanded", "metadata/setting_version"}: for preference in {"general/visible_settings", "cura/active_mode", "cura/categories_expanded",
"metadata/setting_version"}:
temp_preferences.addPreference(preference, None) temp_preferences.addPreference(preference, None)
temp_preferences.setValue(preference, original_preferences.getValue(preference)) temp_preferences.setValue(preference, original_preferences.getValue(preference))
preferences_string = StringIO() preferences_string = StringIO()
@ -81,7 +99,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
# Save Cura version # Save Cura version
version_file = zipfile.ZipInfo("Cura/version.ini") version_file = zipfile.ZipInfo("Cura/version.ini")
version_config_parser = configparser.ConfigParser(interpolation = None) version_config_parser = configparser.ConfigParser(interpolation=None)
version_config_parser.add_section("versions") version_config_parser.add_section("versions")
version_config_parser.set("versions", "cura_version", application.getVersion()) version_config_parser.set("versions", "cura_version", application.getVersion())
version_config_parser.set("versions", "build_type", application.getBuildType()) version_config_parser.set("versions", "build_type", application.getBuildType())
@ -101,11 +119,17 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
return False return False
except EnvironmentError as e: except EnvironmentError as e:
self.setInformation(catalog.i18nc("@error:zip", str(e))) self.setInformation(catalog.i18nc("@error:zip", str(e)))
Logger.error("EnvironmentError when writing workspace to this stream: {err}".format(err = str(e))) Logger.error("EnvironmentError when writing workspace to this stream: {err}".format(err=str(e)))
return False return False
mesh_writer.setStoreArchive(False) mesh_writer.setStoreArchive(False)
return True return True
def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode):
success = self._write(stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode)
self._ucp_model = None
return success
@staticmethod @staticmethod
def _writePluginMetadataToArchive(archive: zipfile.ZipFile) -> None: def _writePluginMetadataToArchive(archive: zipfile.ZipFile) -> None:
file_name_template = "%s/plugin_metadata.json" file_name_template = "%s/plugin_metadata.json"
@ -165,4 +189,27 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
archive.writestr(file_in_archive, serialized_data) archive.writestr(file_in_archive, serialized_data)
except (FileNotFoundError, EnvironmentError): except (FileNotFoundError, EnvironmentError):
Logger.error("File became inaccessible while writing to it: {archive_filename}".format(archive_filename = archive.fp.name)) Logger.error("File became inaccessible while writing to it: {archive_filename}".format(archive_filename = archive.fp.name))
return return
@staticmethod
def _getUserSettings(model: SettingsExportModel) -> Dict[str, Dict[str, Any]]:
user_settings = {}
for group in model.settingsGroups:
category = ''
if group.category == SettingsExportGroup.Category.Global:
category = 'global'
elif group.category == SettingsExportGroup.Category.Extruder:
category = f"extruder_{group.extruder_index}"
if len(category) > 0:
settings_values = {}
stack = group.stack
for setting in group.settings:
if setting.selected:
settings_values[setting.id] = stack.getProperty(setting.id, "value")
user_settings[category] = settings_values
return user_settings

View file

@ -10,6 +10,9 @@ from UM.Math.Vector import Vector
from UM.Logger import Logger from UM.Logger import Logger
from UM.Math.Matrix import Matrix from UM.Math.Matrix import Matrix
from UM.Application import Application from UM.Application import Application
from UM.OutputDevice import OutputDeviceError
from UM.Message import Message
from UM.Resources import Resources
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
@ -17,12 +20,14 @@ from cura.CuraApplication import CuraApplication
from cura.CuraPackageManager import CuraPackageManager from cura.CuraPackageManager import CuraPackageManager
from cura.Settings import CuraContainerStack from cura.Settings import CuraContainerStack
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.Snapshot import Snapshot from cura.Snapshot import Snapshot
from PyQt6.QtCore import QBuffer from PyQt6.QtCore import Qt, QBuffer
from PyQt6.QtGui import QImage, QPainter
import pySavitar as Savitar import pySavitar as Savitar
from .UCPDialog import UCPDialog
import numpy import numpy
import datetime import datetime
@ -37,6 +42,9 @@ except ImportError:
import zipfile import zipfile
import UM.Application import UM.Application
from .SettingsExportModel import SettingsExportModel
from .SettingsExportGroup import SettingsExportGroup
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
@ -84,7 +92,9 @@ class ThreeMFWriter(MeshWriter):
self._store_archive = store_archive self._store_archive = store_archive
@staticmethod @staticmethod
def _convertUMNodeToSavitarNode(um_node, transformation=Matrix()): def _convertUMNodeToSavitarNode(um_node,
transformation = Matrix(),
exported_settings: Optional[Dict[str, Set[str]]] = None):
"""Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode """Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
:returns: Uranium Scene node. :returns: Uranium Scene node.
@ -126,13 +136,26 @@ class ThreeMFWriter(MeshWriter):
if stack is not None: if stack is not None:
changed_setting_keys = stack.getTop().getAllKeys() changed_setting_keys = stack.getTop().getAllKeys()
# Ensure that we save the extruder used for this object in a multi-extrusion setup if exported_settings is None:
if stack.getProperty("machine_extruder_count", "value") > 1: # Ensure that we save the extruder used for this object in a multi-extrusion setup
changed_setting_keys.add("extruder_nr") if stack.getProperty("machine_extruder_count", "value") > 1:
changed_setting_keys.add("extruder_nr")
# Get values for all changed settings & save them. # Get values for all changed settings & save them.
for key in changed_setting_keys: for key in changed_setting_keys:
savitar_node.setSetting("cura:" + key, str(stack.getProperty(key, "value"))) savitar_node.setSetting("cura:" + key, str(stack.getProperty(key, "value")))
else:
# We want to export only the specified settings
if um_node.getName() in exported_settings:
model_exported_settings = exported_settings[um_node.getName()]
# Get values for all exported settings & save them.
for key in model_exported_settings:
savitar_node.setSetting("cura:" + key, str(stack.getProperty(key, "value")))
if isinstance(um_node, CuraSceneNode):
savitar_node.setSetting("cura:print_order", str(um_node.printOrder))
savitar_node.setSetting("cura:drop_to_buildplate", str(um_node.isDropDownEnabled))
# Store the metadata. # Store the metadata.
for key, value in um_node.metadata.items(): for key, value in um_node.metadata.items():
@ -142,7 +165,8 @@ class ThreeMFWriter(MeshWriter):
# only save the nodes on the active build plate # only save the nodes on the active build plate
if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr: if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
continue continue
savitar_child_node = ThreeMFWriter._convertUMNodeToSavitarNode(child_node) savitar_child_node = ThreeMFWriter._convertUMNodeToSavitarNode(child_node,
exported_settings = exported_settings)
if savitar_child_node is not None: if savitar_child_node is not None:
savitar_node.addChild(savitar_child_node) savitar_node.addChild(savitar_child_node)
@ -151,7 +175,24 @@ class ThreeMFWriter(MeshWriter):
def getArchive(self): def getArchive(self):
return self._archive return self._archive
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode) -> bool: def _addLogoToThumbnail(self, primary_image, logo_name):
# Load the icon png image
icon_image = QImage(Resources.getPath(Resources.Images, logo_name))
# Resize icon_image to be 1/4 of primary_image size
new_width = int(primary_image.width() / 4)
new_height = int(primary_image.height() / 4)
icon_image = icon_image.scaled(new_width, new_height, Qt.AspectRatioMode.KeepAspectRatio)
# Create a QPainter to draw on the image
painter = QPainter(primary_image)
# Draw the icon in the top-left corner (adjust coordinates as needed)
icon_position = (10, 10)
painter.drawImage(icon_position[0], icon_position[1], icon_image)
painter.end()
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode, export_settings_model = None) -> 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)
try: try:
@ -175,6 +216,10 @@ class ThreeMFWriter(MeshWriter):
# Attempt to add a thumbnail # Attempt to add a thumbnail
snapshot = self._createSnapshot() snapshot = self._createSnapshot()
if snapshot: if snapshot:
if export_settings_model != None:
self._addLogoToThumbnail(snapshot, "cura-share.png")
elif export_settings_model == None and self._store_archive:
self._addLogoToThumbnail(snapshot, "cura-icon.png")
thumbnail_buffer = QBuffer() thumbnail_buffer = QBuffer()
thumbnail_buffer.open(QBuffer.OpenModeFlag.ReadWrite) thumbnail_buffer.open(QBuffer.OpenModeFlag.ReadWrite)
snapshot.save(thumbnail_buffer, "PNG") snapshot.save(thumbnail_buffer, "PNG")
@ -229,14 +274,20 @@ class ThreeMFWriter(MeshWriter):
transformation_matrix.preMultiply(translation_matrix) transformation_matrix.preMultiply(translation_matrix)
root_node = UM.Application.Application.getInstance().getController().getScene().getRoot() root_node = UM.Application.Application.getInstance().getController().getScene().getRoot()
exported_model_settings = ThreeMFWriter._extractModelExportedSettings(export_settings_model) if export_settings_model != None else None
for node in nodes: for node in nodes:
if node == root_node: if node == root_node:
for root_child in node.getChildren(): for root_child in node.getChildren():
savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(root_child, transformation_matrix) savitar_node = ThreeMFWriter._convertUMNodeToSavitarNode(root_child,
transformation_matrix,
exported_model_settings)
if savitar_node: if savitar_node:
savitar_scene.addSceneNode(savitar_node) savitar_scene.addSceneNode(savitar_node)
else: else:
savitar_node = self._convertUMNodeToSavitarNode(node, transformation_matrix) savitar_node = self._convertUMNodeToSavitarNode(node,
transformation_matrix,
exported_model_settings)
if savitar_node: if savitar_node:
savitar_scene.addSceneNode(savitar_node) savitar_scene.addSceneNode(savitar_node)
@ -392,3 +443,24 @@ class ThreeMFWriter(MeshWriter):
parser = Savitar.ThreeMFParser() parser = Savitar.ThreeMFParser()
scene_string = parser.sceneToString(savitar_scene) scene_string = parser.sceneToString(savitar_scene)
return scene_string return scene_string
@staticmethod
def _extractModelExportedSettings(model: Optional[SettingsExportModel]) -> Dict[str, Set[str]]:
extra_settings = {}
if model is not None:
for group in model.settingsGroups:
if group.category == SettingsExportGroup.Category.Model:
exported_model_settings = set()
for exported_setting in group.settings:
if exported_setting.selected:
exported_model_settings.add(exported_setting.id)
extra_settings[group.category_details] = exported_model_settings
return extra_settings
def exportUcp(self):
self._config_dialog = UCPDialog()
self._config_dialog.show()

View file

@ -0,0 +1,114 @@
# Copyright (c) 2024 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
from PyQt6.QtCore import pyqtSignal, QObject
import UM
from UM.FlameProfiler import pyqtSlot
from UM.OutputDevice import OutputDeviceError
from UM.Workspace.WorkspaceWriter import WorkspaceWriter
from UM.i18n import i18nCatalog
from UM.Logger import Logger
from UM.Message import Message
from cura.CuraApplication import CuraApplication
from .SettingsExportModel import SettingsExportModel
i18n_catalog = i18nCatalog("cura")
class UCPDialog(QObject):
finished = pyqtSignal(bool)
def __init__(self, parent = None) -> None:
super().__init__(parent)
plugin_path = os.path.dirname(__file__)
dialog_path = os.path.join(plugin_path, 'UCPDialog.qml')
self._model = SettingsExportModel()
self._view = CuraApplication.getInstance().createQmlComponent(
dialog_path,
{
"manager": self,
"settingsExportModel": self._model
}
)
self._view.accepted.connect(self._onAccepted)
self._view.rejected.connect(self._onRejected)
self._finished = False
self._accepted = False
def show(self) -> None:
self._finished = False
self._accepted = False
self._view.show()
def getModel(self) -> SettingsExportModel:
return self._model
@pyqtSlot()
def notifyClosed(self):
self._onFinished()
def save3mf(self):
application = CuraApplication.getInstance()
workspace_handler = application.getInstance().getWorkspaceFileHandler()
# Set the model to the workspace writer
mesh_writer = workspace_handler.getWriter("3MFWriter")
mesh_writer.setExportModel(self._model)
# Open file dialog and write the file
device = application.getOutputDeviceManager().getOutputDevice("local_file")
nodes = [application.getController().getScene().getRoot()]
device.writeError.connect(lambda: self._onRejected())
device.writeSuccess.connect(lambda: self._onSuccess())
device.writeFinished.connect(lambda: self._onFinished())
file_name = CuraApplication.getInstance().getPrintInformation().baseName
try:
device.requestWrite(
nodes,
file_name,
["application/vnd.ms-package.3dmanufacturing-3dmodel+xml"],
workspace_handler,
preferred_mimetype_list="application/vnd.ms-package.3dmanufacturing-3dmodel+xml"
)
except OutputDeviceError.UserCanceledError:
self._onRejected()
except Exception as e:
message = Message(
i18n_catalog.i18nc("@info:error", "Unable to write to file: {0}", file_name),
title=i18n_catalog.i18nc("@info:title", "Error"),
message_type=Message.MessageType.ERROR
)
message.show()
Logger.logException("e", "Unable to write to file %s: %s", file_name, e)
self._onRejected()
def _onAccepted(self):
self.save3mf()
def _onRejected(self):
self._onFinished()
def _onSuccess(self):
self._accepted = True
self._onFinished()
def _onFinished(self):
# Make sure we don't send the finished signal twice, whatever happens
if self._finished:
return
self._finished = True
# Reset the model to the workspace writer
mesh_writer = CuraApplication.getInstance().getInstance().getWorkspaceFileHandler().getWriter("3MFWriter")
mesh_writer.setExportModel(None)
self.finished.emit(self._accepted)

View file

@ -0,0 +1,99 @@
// Copyright (c) 2024 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import QtQuick.Window 2.2
import UM 1.5 as UM
import Cura 1.1 as Cura
UM.Dialog
{
id: exportDialog
title: catalog.i18nc("@title:window", "Export Universal Cura Project")
margin: UM.Theme.getSize("default_margin").width
minimumWidth: UM.Theme.getSize("modal_window_minimum").width
minimumHeight: UM.Theme.getSize("modal_window_minimum").height
backgroundColor: UM.Theme.getColor("detail_background")
headerComponent: Rectangle
{
height: childrenRect.height + 2 * UM.Theme.getSize("default_margin").height
color: UM.Theme.getColor("main_background")
ColumnLayout
{
id: headerColumn
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.rightMargin: anchors.leftMargin
UM.Label
{
id: titleLabel
text: catalog.i18nc("@action:title", "Summary - Universal Cura Project")
font: UM.Theme.getFont("large")
}
UM.Label
{
id: descriptionLabel
text: catalog.i18nc("@action:description", "When exporting a Universal Cura Project, all the models present on the build plate will be included with their current position, orientation and scale. You can also select which per-extruder or per-model settings should be included to ensure a proper printing of the batch, even on different printers.")
font: UM.Theme.getFont("default")
wrapMode: Text.Wrap
Layout.maximumWidth: headerColumn.width
}
}
}
Rectangle
{
anchors.fill: parent
color: UM.Theme.getColor("main_background")
UM.I18nCatalog { id: catalog; name: "cura" }
ListView
{
id: settingsExportList
anchors.fill: parent
anchors.margins: UM.Theme.getSize("default_margin").width
spacing: UM.Theme.getSize("thick_margin").height
model: settingsExportModel.settingsGroups
clip: true
ScrollBar.vertical: UM.ScrollBar { id: verticalScrollBar }
delegate: SettingsSelectionGroup { Layout.margins: 0 }
}
}
rightButtons:
[
Cura.TertiaryButton
{
text: catalog.i18nc("@action:button", "Cancel")
onClicked: reject()
},
Cura.PrimaryButton
{
text: catalog.i18nc("@action:button", "Save project")
onClicked: accept()
}
]
buttonSpacing: UM.Theme.getSize("wide_margin").width
onClosing:
{
manager.notifyClosed()
}
}

View file

@ -2,9 +2,12 @@
# Uranium is released under the terms of the LGPLv3 or higher. # Uranium is released under the terms of the LGPLv3 or higher.
import sys import sys
from PyQt6.QtQml import qmlRegisterType
from UM.Logger import Logger from UM.Logger import Logger
try: try:
from . import ThreeMFWriter from . import ThreeMFWriter
from .SettingsExportGroup import SettingsExportGroup
threemf_writer_was_imported = True threemf_writer_was_imported = True
except ImportError: except ImportError:
Logger.log("w", "Could not import ThreeMFWriter; libSavitar may be missing") Logger.log("w", "Could not import ThreeMFWriter; libSavitar may be missing")
@ -23,20 +26,24 @@ def getMetaData():
if threemf_writer_was_imported: if threemf_writer_was_imported:
metaData["mesh_writer"] = { metaData["mesh_writer"] = {
"output": [{ "output": [
"extension": "3mf", {
"description": i18n_catalog.i18nc("@item:inlistbox", "3MF file"), "extension": "3mf",
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml", "description": i18n_catalog.i18nc("@item:inlistbox", "3MF file"),
"mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode "mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
}] "mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode
}
]
} }
metaData["workspace_writer"] = { metaData["workspace_writer"] = {
"output": [{ "output": [
"extension": workspace_extension, {
"description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"), "extension": workspace_extension,
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml", "description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"),
"mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode "mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
}] "mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode
}
]
} }
return metaData return metaData
@ -44,6 +51,8 @@ def getMetaData():
def register(app): def register(app):
if "3MFWriter.ThreeMFWriter" in sys.modules: if "3MFWriter.ThreeMFWriter" in sys.modules:
qmlRegisterType(SettingsExportGroup, "ThreeMFWriter", 1, 0, "SettingsExportGroup")
return {"mesh_writer": ThreeMFWriter.ThreeMFWriter(), return {"mesh_writer": ThreeMFWriter.ThreeMFWriter(),
"workspace_writer": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter()} "workspace_writer": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter()}
else: else:

View file

@ -2,7 +2,7 @@
"name": "3MF Writer", "name": "3MF Writer",
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides support for writing 3MF files.", "description": "Provides support for writing 3MF and UCP files.",
"api": 8, "api": 8,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -76,6 +76,7 @@ class CuraEngineBackend(QObject, Backend):
self._default_engine_location = executable_name self._default_engine_location = executable_name
search_path = [ search_path = [
os.path.abspath(os.path.join(os.path.dirname(sys.executable), "..", "Resources")),
os.path.abspath(os.path.dirname(sys.executable)), os.path.abspath(os.path.dirname(sys.executable)),
os.path.abspath(os.path.join(os.path.dirname(sys.executable), "bin")), os.path.abspath(os.path.join(os.path.dirname(sys.executable), "bin")),
os.path.abspath(os.path.join(os.path.dirname(sys.executable), "..")), os.path.abspath(os.path.join(os.path.dirname(sys.executable), "..")),
@ -180,7 +181,10 @@ class CuraEngineBackend(QObject, Backend):
application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged) application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
self._slicing_error_message = Message( self._slicing_error_message = Message(
text = catalog.i18nc("@message", "Slicing failed with an unexpected error. Please consider reporting a bug on our issue tracker."), text = catalog.i18nc("@message", "Oops! We encountered an unexpected error during your slicing process. "
"Rest assured, we've automatically received the crash logs for analysis, "
"if you have not disabled data sharing in your preferences. To assist us "
"further, consider sharing your project details on our issue tracker."),
title = catalog.i18nc("@message:title", "Slicing failed"), title = catalog.i18nc("@message:title", "Slicing failed"),
message_type = Message.MessageType.ERROR message_type = Message.MessageType.ERROR
) )

View file

@ -9,6 +9,7 @@
# When setting an accel limit on multi-extruder printers ALL extruders are effected. # When setting an accel limit on multi-extruder printers ALL extruders are effected.
# This post does not distinguish between Print Accel and Travel Accel. The limit is the limit for all regardless. Example: Skin Accel = 1000 and Outer Wall accel = 500. If the limit is set to 300 then both Skin and Outer Wall will be Accel = 300. # This post does not distinguish between Print Accel and Travel Accel. The limit is the limit for all regardless. Example: Skin Accel = 1000 and Outer Wall accel = 500. If the limit is set to 300 then both Skin and Outer Wall will be Accel = 300.
# 9/15/2023 added support for RepRap M566 command for Jerk in mm/min # 9/15/2023 added support for RepRap M566 command for Jerk in mm/min
# 2/4/2024 Added a block so the script doesn't run unless Accel Control is enabled in Cura. This should keep users from increasing the Accel Limits.
from ..Script import Script from ..Script import Script
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
@ -45,6 +46,10 @@ class LimitXYAccelJerk(Script):
# Warn the user if the printer is multi-extruder------------------ # Warn the user if the printer is multi-extruder------------------
if ext_count > 1: if ext_count > 1:
Message(text = "<NOTICE> 'Limit the X-Y Accel/Jerk': The post processor treats all extruders the same. If you have multiple extruders they will all be subject to the same Accel and Jerk limits imposed. If you have different Travel and Print Accel they will also be subject to the same limits. If that is not acceptable then you should not use this Post Processor.").show() Message(text = "<NOTICE> 'Limit the X-Y Accel/Jerk': The post processor treats all extruders the same. If you have multiple extruders they will all be subject to the same Accel and Jerk limits imposed. If you have different Travel and Print Accel they will also be subject to the same limits. If that is not acceptable then you should not use this Post Processor.").show()
# Warn the user if Accel Control is not enabled in Cura. This keeps the script from being able to increase the Accel limits-----------
if not bool(extruder[0].getProperty("acceleration_enabled", "value")):
Message(title = "[Limit the X-Y Accel/Jerk]", text = "You must have 'Enable Acceleration Control' checked in Cura or the script will exit.").show()
def getSettingDataString(self): def getSettingDataString(self):
return """{ return """{
@ -169,6 +174,13 @@ class LimitXYAccelJerk(Script):
extruder = mycura.extruderList extruder = mycura.extruderList
machine_name = str(mycura.getProperty("machine_name", "value")) machine_name = str(mycura.getProperty("machine_name", "value"))
print_sequence = str(mycura.getProperty("print_sequence", "value")) print_sequence = str(mycura.getProperty("print_sequence", "value"))
acceleration_enabled = bool(extruder[0].getProperty("acceleration_enabled", "value"))
# Exit if acceleration control is not enabled----------------
if not acceleration_enabled:
Message(title = "[Limit the X-Y Accel/Jerk]", text = "DID NOT RUN. You must have 'Enable Acceleration Control' checked in Cura.").show()
data[0] += "; [LimitXYAccelJerk] DID NOT RUN because 'Enable Acceleration Control' is not checked in Cura.\n"
return data
# Exit if 'one_at_a_time' is enabled------------------------- # Exit if 'one_at_a_time' is enabled-------------------------
if print_sequence == "one_at_a_time": if print_sequence == "one_at_a_time":
@ -183,12 +195,8 @@ class LimitXYAccelJerk(Script):
return data return data
type_of_change = str(self.getSettingValueByKey("type_of_change")) type_of_change = str(self.getSettingValueByKey("type_of_change"))
accel_print_enabled = bool(extruder[0].getProperty("acceleration_enabled", "value"))
accel_travel_enabled = bool(extruder[0].getProperty("acceleration_travel_enabled", "value"))
accel_print = extruder[0].getProperty("acceleration_print", "value") accel_print = extruder[0].getProperty("acceleration_print", "value")
accel_travel = extruder[0].getProperty("acceleration_travel", "value") accel_travel = extruder[0].getProperty("acceleration_travel", "value")
jerk_print_enabled = str(extruder[0].getProperty("jerk_enabled", "value"))
jerk_travel_enabled = str(extruder[0].getProperty("jerk_travel_enabled", "value"))
jerk_print_old = extruder[0].getProperty("jerk_print", "value") jerk_print_old = extruder[0].getProperty("jerk_print", "value")
jerk_travel_old = extruder[0].getProperty("jerk_travel", "value") jerk_travel_old = extruder[0].getProperty("jerk_travel", "value")
if int(accel_print) >= int(accel_travel): if int(accel_print) >= int(accel_travel):

View file

@ -264,6 +264,7 @@ class SliceInfo(QObject, Extension):
# Prime tower settings # Prime tower settings
print_settings["prime_tower_enable"] = global_stack.getProperty("prime_tower_enable", "value") print_settings["prime_tower_enable"] = global_stack.getProperty("prime_tower_enable", "value")
print_settings["prime_tower_mode"] = global_stack.getProperty("prime_tower_mode", "value")
# Infill settings # Infill settings
print_settings["infill_sparse_density"] = global_stack.getProperty("infill_sparse_density", "value") print_settings["infill_sparse_density"] = global_stack.getProperty("infill_sparse_density", "value")

View file

@ -579,8 +579,9 @@ class XmlMaterialProfile(InstanceContainer):
meta_data[tag_name] = entry.text meta_data[tag_name] = entry.text
if tag_name in self.__material_metadata_setting_map: for tag_name, value in meta_data.items():
common_setting_values[self.__material_metadata_setting_map[tag_name]] = entry.text if tag_name in self.__material_metadata_setting_map:
common_setting_values[self.__material_metadata_setting_map[tag_name]] = value
if "description" not in meta_data: if "description" not in meta_data:
meta_data["description"] = "" meta_data["description"] = ""
@ -1222,7 +1223,9 @@ class XmlMaterialProfile(InstanceContainer):
"diameter": "material_diameter" "diameter": "material_diameter"
} }
__material_metadata_setting_map = { __material_metadata_setting_map = {
"GUID": "material_guid" "GUID": "material_guid",
"material": "material_type",
"brand": "material_brand",
} }
# Map of recognised namespaces with a proper prefix. # Map of recognised namespaces with a proper prefix.

View file

@ -1,5 +1,5 @@
pytest pytest
pyinstaller==5.8.0 pyinstaller==6.3.0
pyinstaller-hooks-contrib pyinstaller-hooks-contrib
pyyaml pyyaml
sip==6.5.1 sip==6.5.1

View file

@ -42,7 +42,7 @@
"machine_max_jerk_xy": { "value": 10 }, "machine_max_jerk_xy": { "value": 10 },
"machine_max_jerk_z": { "value": 2 }, "machine_max_jerk_z": { "value": 2 },
"machine_name": { "default_value": "Anycubic Kobra 2" }, "machine_name": { "default_value": "Anycubic Kobra 2" },
"machine_start_gcode": { "default_value": "G21 ;metric values\nG90 ; use absolute coordinates\nM82 ; use absolute distances for extrusion\nM104 S[first_layer_temperature] ; set extruder temp\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S[first_layer_temperature] ; wait for extruder temp\nG28 ; home all axes\nM300 S1318 P266\nG1 Z5 F5000 ; lift nozzle\nG1 X5 Y0 F3000\nG1 Z0.3 ; set nozzle height\nG92 E0\nG1 X50 Y0 E20 F500 ; Extrude 20mm of filament in a 5cm line \nG92 E0 ; zero the extruded length again \nG1 E-4.5 F4800 ; Retract a little \nG92 E0\nG1 X120 F4000 ; Quickly wipe away from the filament line\nM117 ; Printing\u2026\nG5" }, "machine_start_gcode": { "default_value": "G21 ;metric values\nG90 ; use absolute coordinates\nM82 ; use absolute distances for extrusion\nM104 S{material_print_temperature_layer_0} ; set extruder temp\nM140 S{material_bed_temperature_layer_0} ; set bed temp\nM190 S{material_bed_temperature_layer_0} ; wait for bed temp\nM109 S{material_print_temperature_layer_0} ; wait for extruder temp\nG28 ; home all axes\nM300 S1318 P266\nG1 Z5 F5000 ; lift nozzle\nG1 X5 Y0 F3000\nG1 Z0.3 ; set nozzle height\nG92 E0\nG1 X50 Y0 E20 F500 ; Extrude 20mm of filament in a 5cm line \nG92 E0 ; zero the extruded length again \nG1 E-4.5 F4800 ; Retract a little \nG92 E0\nG1 X120 F4000 ; Quickly wipe away from the filament line\nM117 ; Printing\u2026\nG5" },
"machine_width": { "default_value": 220 }, "machine_width": { "default_value": 220 },
"material_bed_temperature": { "maximum_value_warning": 110 }, "material_bed_temperature": { "maximum_value_warning": 110 },
"material_bed_temperature_layer_0": { "maximum_value_warning": 110 }, "material_bed_temperature_layer_0": { "maximum_value_warning": 110 },

View file

@ -23,7 +23,7 @@
"machine_heated_bed": { "default_value": true }, "machine_heated_bed": { "default_value": true },
"machine_height": { "default_value": 250 }, "machine_height": { "default_value": 250 },
"machine_name": { "default_value": "Anycubic Kobra Go" }, "machine_name": { "default_value": "Anycubic Kobra Go" },
"machine_start_gcode": { "default_value": "M140 S[first_layer_bed_temperature]; Heat bed\nM104 S[first_layer_temperature\n ]; Heat extruder\nG21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nG28 ; Home all axes\nG92 E0 ; Reset Extruder\nM420 S1 ; Enable Bed Levelling Mesh\nM190 S[first_layer_bed_temperature\n ]; Wait for bed to get up to temperature\nM109 S[first_layer_temperature\n ]; Wait for extruder to get up to temperature\nG1 Z2.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\nG1 X2 Y20 Z0.3 F5000.0 ; Move to start position\nG1 X2 Y200.0 Z0.3 F1500.0 E15 ; Draw the first line\nG1 X2.4 Y200.0 Z0.3 F5000.0 ; Move to side a little\nG1 X2.4 Y20 Z0.3 F1500.0 E30 ; Draw the second line\nG92 E0 ; Reset Extruder\nG1 Z2.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\nG1 F2400 E-1\nG1 X5 Y20 Z0.3 F5000.0 ; Move over to prevent blob squish" }, "machine_start_gcode": { "default_value": "M140 S{material_bed_temperature_layer_0}; Heat bed\nM104 S{material_print_temperature_layer_0}; Heat extruder\nG21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nG28 ; Home all axes\nG92 E0 ; Reset Extruder\nM420 S1 ; Enable Bed Levelling Mesh\nM190 S{material_bed_temperature_layer_0}; Wait for bed to get up to temperature\nM109 S{material_print_temperature_layer_0}; Wait for extruder to get up to temperature\nG1 Z2.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\nG1 X2 Y20 Z0.3 F5000.0 ; Move to start position\nG1 X2 Y200.0 Z0.3 F1500.0 E15 ; Draw the first line\nG1 X2.4 Y200.0 Z0.3 F5000.0 ; Move to side a little\nG1 X2.4 Y20 Z0.3 F1500.0 E30 ; Draw the second line\nG92 E0 ; Reset Extruder\nG1 Z2.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\nG1 F2400 E-1\nG1 X5 Y20 Z0.3 F5000.0 ; Move over to prevent blob squish" },
"machine_width": { "default_value": 222 } "machine_width": { "default_value": 222 }
} }
} }

View file

@ -0,0 +1,31 @@
{
"version": 2,
"name": "Creality Ender-3 v2 Neo",
"inherits": "creality_base",
"metadata":
{
"visible": true,
"platform": "creality_ender3.3mf",
"quality_definition": "creality_base"
},
"overrides":
{
"gantry_height": { "value": 25 },
"machine_depth": { "default_value": 230 },
"machine_head_with_fans_polygon":
{
"default_value": [
[-26, 34],
[-26, -32],
[32, -32],
[32, 34]
]
},
"machine_height": { "default_value": 250 },
"machine_name": { "default_value": "Creality Ender-3 v2 Neo" },
"machine_start_gcode": { "default_value": "G92 E0 ;Reset Extruder\nG28 ;Home\nG1 Z2.0 F3000 ;Move Z Axis up\nG1 X10.1 Y20 Z0.28 F5000.0 ;Move to start position\nG1 X10.1 Y200.0 Z0.28 F1500.0 E15 ;Draw the first line\nG1 X10.4 Y200.0 Z0.28 F5000.0 ;Move to side a little\nG1 X10.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line\nG92 E0 ;Reset Extruder\nG1 Z2.0 F3000 ;Move Z Axis up" },
"machine_width": { "default_value": 230 },
"retraction_amount": { "value": 4 },
"retraction_speed": { "value": 25 }
}
}

View file

@ -45,7 +45,7 @@
"machine_max_feedrate_y": { "value": 500 }, "machine_max_feedrate_y": { "value": 500 },
"machine_max_feedrate_z": { "value": 30 }, "machine_max_feedrate_z": { "value": 30 },
"machine_name": { "default_value": "Creality Ender-3 V3 SE" }, "machine_name": { "default_value": "Creality Ender-3 V3 SE" },
"machine_start_gcode": { "default_value": "M220 S100 ;Reset Feedrate\nM221 S100 ;Reset Flowrate\n\nG28 ;Home\n\nM420 S1; Enable mesh leveling\n\nG92 E0 ;Reset Extruder\nG1 Z2.0 F3000 ;Move Z Axis up\nG1 X10.1 Y20 Z0.28 F5000.0 ;Move to start position\nM109 S[material_print_temperature_layer_0]\nG1 X10.1 Y145.0 Z0.28 F1500.0 E15 ;Draw the first line\nG1 X10.4 Y145.0 Z0.28 F5000.0 ;Move to side a little\nG1 X10.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line\nG92 E0 ;Reset Extruder\nG1 E-1.0000 F1800 ;Retract a bit\nG1 Z2.0 F3000 ;Move Z Axis up\nG1 E0.0000 F1800 \n" }, "machine_start_gcode": { "default_value": "M220 S100 ;Reset Feedrate\nM221 S100 ;Reset Flowrate\n\nG28 ;Home\n\nM420 S1; Use saved mesh leveling data\n\nG92 E0 ;Reset Extruder\nG1 Z2.0 F3000 ;Move Z Axis up\nG1 X10.1 Y20 Z0.28 F5000.0 ;Move to start position\nM190 S{material_bed_temperature_layer_0} ; Set bed temperature and wait\nM109 S{material_print_temperature_layer_0} ; Set hotend temperature and wait\nG1 X10.1 Y145.0 Z0.28 F1500.0 E15 ;Draw the first line\nG1 X10.4 Y145.0 Z0.28 F5000.0 ;Move to side a little\nG1 X10.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line\nG92 E0 ;Reset Extruder\nG1 E-1.0000 F1800 ;Retract a bit\nG1 Z2.0 F3000 ;Move Z Axis up\nG1 E0.0000 F1800 \n" },
"machine_width": { "default_value": 220 }, "machine_width": { "default_value": 220 },
"retraction_amount": { "value": 0.8 }, "retraction_amount": { "value": 0.8 },
"retraction_speed": { "default_value": 40 }, "retraction_speed": { "default_value": 40 },

View file

@ -23,7 +23,7 @@
}, },
"machine_height": { "default_value": 400 }, "machine_height": { "default_value": 400 },
"machine_name": { "default_value": "Creality Ender-5 Plus" }, "machine_name": { "default_value": "Creality Ender-5 Plus" },
"machine_start_gcode": { "default_value": "M201 X500.00 Y500.00 Z100.00 E5000.00 ;Setup machine max acceleration\nM203 X500.00 Y500.00 Z10.00 E50.00 ;Setup machine max feedrate\nM204 P500.00 R1000.00 T500.00 ;Setup Print/Retract/Travel acceleration\nM205 X8.00 Y8.00 Z0.40 E5.00 ;Setup Jerk\nM220 S100 ;Reset Feedrate\nM221 S100 ;Reset Flowrate\n\nG28 ;Home\nM420 S1 Z2 ;Enable ABL using saved Mesh and Fade Height\n\nG92 E0 ;Reset Extruder\nG1 Z2.0 F3000 ;Move Z Axis up\nG1 X10.1 Y20 Z0.28 F5000.0 ;Move to start position\nG1 X10.1 Y200.0 Z0.28 F1500.0 E15 ;Draw the first line\nG1 X10.4 Y200.0 Z0.28 F5000.0 ;Move to side a little\nG1 X10.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line\nG92 E0 ;Reset Extruder\nG1 Z2.0 F3000 ;Move Z Axis up\n" }, "machine_start_gcode": { "default_value": "M201 X500.00 Y500.00 Z100.00 E5000.00 ;Setup machine max acceleration\nM203 X500.00 Y500.00 Z10.00 E50.00 ;Setup machine max feedrate\nM204 P500.00 R1000.00 T500.00 ;Setup Print/Retract/Travel acceleration\nM205 X8.00 Y8.00 Z0.40 E5.00 ;Setup Jerk\nM220 S100 ;Reset Feedrate\nM221 S100 ;Reset Flowrate\n\nG28 ;Home\nG29 ;Auto bed level\n\nG92 E0 ;Reset Extruder\nG1 Z2.0 F3000 ;Move Z Axis up\nG1 X10.1 Y20 Z0.28 F5000.0 ;Move to start position\nG1 X10.1 Y200.0 Z0.28 F1500.0 E15 ;Draw the first line\nG1 X10.4 Y200.0 Z0.28 F5000.0 ;Move to side a little\nG1 X10.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line\nG92 E0 ;Reset Extruder\nG1 Z2.0 F3000 ;Move Z Axis up\n" },
"machine_width": { "default_value": 350 }, "machine_width": { "default_value": 350 },
"speed_print": { "value": 80.0 } "speed_print": { "value": 80.0 }
} }

View file

@ -0,0 +1,32 @@
{
"version": 2,
"name": "Dagoma Sigma Pro 500Z",
"inherits": "dagoma_delta",
"metadata":
{
"visible": true,
"author": "Dagoma",
"manufacturer": "Dagoma",
"file_formats": "text/x-gcode",
"platform": "dagoma_sigma_pro.obj",
"first_start_actions": [ "MachineSettingsAction" ],
"has_machine_quality": true,
"has_variants": true,
"machine_extruder_trains": { "0": "dagoma_sigma_pro_extruder" },
"platform_texture": "dagoma_sigma_pro.png",
"preferred_quality_type": "h0.2",
"preferred_variant_name": "Brass 0.4mm",
"quality_definition": "dagoma_sigma_pro",
"variants_name": "Nozzle"
},
"overrides":
{
"machine_depth": { "default_value": 200 },
"machine_end_gcode": { "default_value": ";End Gcode for {machine_name}\n;Author: Dagoma\nM104 S0\nM107 ;stop fan\nM140 S0 ;heated bed heater off (if you have it)\nG92 E0\nG1 E-55 F4600\nG27\nG90 ; Absolute positioning\nT0" },
"machine_heated_bed": { "default_value": true },
"machine_height": { "default_value": 501 },
"machine_name": { "default_value": "Dagoma Sigma Pro 500Z" },
"machine_start_gcode": { "default_value": ";Start Gcode for {machine_name}\n;Author: Dagoma\n;Sliced: {date} {time}\n;Estimated print time: {print_time}\n;Print speed: {speed_print}mm/s\n;Layer height: {layer_height}mm\n;Wall thickness: {wall_thickness}mm\n;Infill density: {infill_sparse_density}%\n;Infill pattern: {infill_pattern}\n;Support: {support_enable}\n;Print temperature: {material_print_temperature}\u00b0C\n;Flow: {material_flow}%\n;Retraction amount: {retraction_amount}mm\n;Retraction speed: {retraction_retract_speed}mm/s\nG90 ;absolute positioning\nM190 S{material_bed_temperature_layer_0};\nM109 S140;\nG1 F200 E-1.0\nM106 S255 ;Activating layers fans\nG28 ;Homing\nG29 ;Calibration\nM107 ;Off Ventilateur\nM109 S{material_print_temperature_layer_0} ;Temperature for the first layer only\nG92 E0 ;Zero the extruded length again\nG1 X0 Y-105 Z1 F3000\nG1 F{speed_travel}\nM117 Printing...\n" },
"machine_width": { "default_value": 200 }
}
}

View file

@ -0,0 +1,37 @@
{
"version": 2,
"name": "Dagoma Sigma Pro 500Z Dual",
"inherits": "dagoma_delta",
"metadata":
{
"visible": true,
"author": "Dagoma",
"manufacturer": "Dagoma",
"file_formats": "text/x-gcode",
"platform": "dagoma_sigma_pro.obj",
"first_start_actions": [ "MachineSettingsAction" ],
"has_machine_quality": true,
"has_variants": true,
"machine_extruder_trains":
{
"0": "dagoma_sigma_pro_dual_extruder_right",
"1": "dagoma_sigma_pro_dual_extruder_left"
},
"platform_texture": "dagoma_sigma_pro.png",
"preferred_quality_type": "h0.2",
"preferred_variant_name": "Brass 0.4mm",
"quality_definition": "dagoma_sigma_pro_dual",
"variants_name": "Nozzle"
},
"overrides":
{
"machine_depth": { "default_value": 200 },
"machine_end_gcode": { "default_value": ";End Gcode for {machine_name}\n;Author: Dagoma\nM104 S0\nM107 ;stop fan\nM140 S0 ;heated bed heater off (if you have it)\nG92 E0\nG1 E-55 F4600\nG27\nG90 ; Absolute positioning\nT0" },
"machine_extruder_count": { "default_value": 2 },
"machine_heated_bed": { "default_value": true },
"machine_height": { "default_value": 501 },
"machine_name": { "default_value": "Dagoma Sigma Pro 500Z Dual" },
"machine_start_gcode": { "default_value": ";Start Gcode for {machine_name}\n;Author: Dagoma\n;Sliced: {date} {time}\n;Estimated print time: {print_time}\n;Print speed: {speed_print}mm/s\n;Layer height: {layer_height}mm\n;Wall thickness: {wall_thickness}mm\n;Infill density: {infill_sparse_density}%\n;Infill pattern: {infill_pattern}\n;Support: {support_enable}\n;Print temperature: {material_print_temperature}\u00b0C\n;Flow: {material_flow}%\n;Retraction amount: {retraction_amount}mm\n;Retraction speed: {retraction_retract_speed}mm/s\nG90 ;absolute positioning\nM190 S{material_bed_temperature_layer_0};\nM109 S140;\nG1 F200 E-1.0\nM106 S255 ;Activating layers fans\nG28 ;Homing\nG29 ;Calibration\nM107 ;Off Ventilateur\nM109 S{material_print_temperature_layer_0} ;Temperature for the first layer only\nG92 E0 ;Zero the extruded length again\nG1 X0 Y-105 Z1 F3000\nG1 F{speed_travel}\nM117 Printing...\n" },
"machine_width": { "default_value": 200 }
}
}

View file

@ -31,7 +31,7 @@
"material_final_print_temperature": { "value": "material_print_temperature" }, "material_final_print_temperature": { "value": "material_print_temperature" },
"material_initial_print_temperature": { "value": "material_print_temperature" }, "material_initial_print_temperature": { "value": "material_print_temperature" },
"material_standby_temperature": { "value": "material_print_temperature" }, "material_standby_temperature": { "value": "material_print_temperature" },
"prime_tower_enable": { "value": "1" }, "prime_tower_enable": { "value": "True" },
"prime_tower_min_volume": { "value": "50" }, "prime_tower_min_volume": { "value": "50" },
"switch_extruder_retraction_amount": { "value": "0" } "switch_extruder_retraction_amount": { "value": "0" }
} }

View file

@ -76,6 +76,22 @@
"type": "str", "type": "str",
"enabled": false "enabled": false
}, },
"material_type":
{
"label": "Material Type",
"description": "The type of material used.",
"default_value": "",
"type": "str",
"enabled": false
},
"material_brand":
{
"label": "Material Brand",
"description": "The brand of material used.",
"default_value": "",
"type": "str",
"enabled": false
},
"material_diameter": "material_diameter":
{ {
"label": "Diameter", "label": "Diameter",
@ -963,7 +979,7 @@
"description": "Width of a single prime tower line.", "description": "Width of a single prime tower line.",
"type": "float", "type": "float",
"unit": "mm", "unit": "mm",
"enabled": "resolveOrValue('prime_tower_enable')", "enabled": "prime_tower_enable",
"default_value": 0.4, "default_value": 0.4,
"value": "line_width", "value": "line_width",
"minimum_value": "0.001", "minimum_value": "0.001",
@ -1668,7 +1684,7 @@
"value": "skin_line_width * 2", "value": "skin_line_width * 2",
"default_value": 1, "default_value": 1,
"minimum_value": "0", "minimum_value": "0",
"maximum_value_warning": "skin_line_width * 3", "maximum_value_warning": "skin_line_width * 10",
"type": "float", "type": "float",
"enabled": "(top_layers > 0 or bottom_layers > 0) and top_bottom_pattern != 'concentric'", "enabled": "(top_layers > 0 or bottom_layers > 0) and top_bottom_pattern != 'concentric'",
"limit_to_extruder": "top_bottom_extruder_nr", "limit_to_extruder": "top_bottom_extruder_nr",
@ -3347,7 +3363,7 @@
"description": "The speed at which the prime tower is printed. Printing the prime tower slower can make it more stable when the adhesion between the different filaments is suboptimal.", "description": "The speed at which the prime tower is printed. Printing the prime tower slower can make it more stable when the adhesion between the different filaments is suboptimal.",
"type": "float", "type": "float",
"unit": "mm/s", "unit": "mm/s",
"enabled": "resolveOrValue('prime_tower_enable')", "enabled": "prime_tower_enable",
"default_value": 60, "default_value": 60,
"value": "speed_print", "value": "speed_print",
"minimum_value": "0.1", "minimum_value": "0.1",
@ -3725,7 +3741,7 @@
"maximum_value_warning": "10000", "maximum_value_warning": "10000",
"default_value": 3000, "default_value": 3000,
"value": "acceleration_print", "value": "acceleration_print",
"enabled": "resolveOrValue('prime_tower_enable') and resolveOrValue('acceleration_enabled')", "enabled": "prime_tower_enable and resolveOrValue('acceleration_enabled')",
"settable_per_mesh": false "settable_per_mesh": false
} }
} }
@ -4044,7 +4060,7 @@
"maximum_value_warning": "50", "maximum_value_warning": "50",
"default_value": 20, "default_value": 20,
"value": "jerk_print", "value": "jerk_print",
"enabled": "resolveOrValue('prime_tower_enable') and resolveOrValue('jerk_enabled')", "enabled": "prime_tower_enable and resolveOrValue('jerk_enabled')",
"settable_per_mesh": false "settable_per_mesh": false
} }
} }
@ -5135,7 +5151,7 @@
"unit": "mm", "unit": "mm",
"type": "float", "type": "float",
"minimum_value": "0", "minimum_value": "0",
"maximum_value_warning": "machine_nozzle_size", "maximum_value_warning": "5*layer_height",
"default_value": 0.1, "default_value": 0.1,
"limit_to_extruder": "support_interface_extruder_nr if support_interface_enable else support_infill_extruder_nr", "limit_to_extruder": "support_interface_extruder_nr if support_interface_enable else support_infill_extruder_nr",
"enabled": "support_enable or support_meshes_present", "enabled": "support_enable or support_meshes_present",
@ -6789,13 +6805,29 @@
"settable_per_mesh": false, "settable_per_mesh": false,
"settable_per_extruder": false "settable_per_extruder": false
}, },
"prime_tower_mode":
{
"label": "Prime Tower Type",
"description": "<html>How to generate the prime tower:<ul><li><b>Normal:</b> create a bucket in which secondary materials are primed</li><li><b>Interleaved:</b> create a prime tower as sparse as possible. This will save time and filament, but is only possible if the used materials adhere to each other.</li></ul></html>",
"type": "enum",
"value": "'interleaved' if (all(material_type_var == extruderValues('material_type')[0] for material_type_var in extruderValues('material_type')) and all(material_brand_var == extruderValues('material_brand')[0] for material_brand_var in extruderValues('material_brand'))) else 'normal'",
"options":
{
"normal": "Normal",
"interleaved": "Interleaved"
},
"default_value": "normal",
"enabled": "prime_tower_enable",
"settable_per_mesh": false,
"settable_per_extruder": false
},
"prime_tower_size": "prime_tower_size":
{ {
"label": "Prime Tower Size", "label": "Prime Tower Size",
"description": "The width of the prime tower.", "description": "The width of the prime tower.",
"type": "float", "type": "float",
"unit": "mm", "unit": "mm",
"enabled": "resolveOrValue('prime_tower_enable')", "enabled": "prime_tower_enable",
"default_value": 20, "default_value": 20,
"resolve": "max(extruderValues('prime_tower_size'))", "resolve": "max(extruderValues('prime_tower_size'))",
"minimum_value": "0", "minimum_value": "0",
@ -6814,7 +6846,20 @@
"default_value": 6, "default_value": 6,
"minimum_value": "0", "minimum_value": "0",
"maximum_value_warning": "(resolveOrValue('prime_tower_size') * 0.5) ** 2 * 3.14159 * resolveOrValue('layer_height') - sum(extruderValues('prime_tower_min_volume')) + prime_tower_min_volume", "maximum_value_warning": "(resolveOrValue('prime_tower_size') * 0.5) ** 2 * 3.14159 * resolveOrValue('layer_height') - sum(extruderValues('prime_tower_min_volume')) + prime_tower_min_volume",
"enabled": "resolveOrValue('prime_tower_enable')", "enabled": "prime_tower_enable",
"settable_per_mesh": false,
"settable_per_extruder": true
},
"prime_tower_max_bridging_distance":
{
"label": "Prime Tower Maximum Bridging Distance",
"description": "The maximum length of the branches which may be printed over the air.",
"unit": "mm",
"type": "float",
"default_value": 5,
"minimum_value": "line_width",
"maximum_value_warning": "10.0",
"enabled": "prime_tower_enable",
"settable_per_mesh": false, "settable_per_mesh": false,
"settable_per_extruder": true "settable_per_extruder": true
}, },
@ -6824,9 +6869,9 @@
"description": "The x coordinate of the position of the prime tower.", "description": "The x coordinate of the position of the prime tower.",
"type": "float", "type": "float",
"unit": "mm", "unit": "mm",
"enabled": "resolveOrValue('prime_tower_enable')", "enabled": "prime_tower_enable",
"default_value": 200, "default_value": 200,
"value": "(resolveOrValue('machine_width') / 2 + resolveOrValue('prime_tower_size') / 2) if resolveOrValue('machine_shape') == 'elliptic' else (resolveOrValue('machine_width') - (resolveOrValue('prime_tower_base_size') if (resolveOrValue('adhesion_type') == 'raft' or resolveOrValue('prime_tower_brim_enable')) else 0) - max(max(extruderValues('travel_avoid_distance')) + max(extruderValues('machine_nozzle_offset_x')) + max(extruderValues('support_offset')) + (extruderValue(skirt_brim_extruder_nr, 'skirt_brim_line_width') * extruderValue(skirt_brim_extruder_nr, 'skirt_line_count') * extruderValue(skirt_brim_extruder_nr, 'initial_layer_line_width_factor') / 100 + extruderValue(skirt_brim_extruder_nr, 'skirt_gap') if resolveOrValue('adhesion_type') == 'skirt' else 0) + (resolveOrValue('draft_shield_dist') if resolveOrValue('draft_shield_enabled') else 0), max(map(abs, extruderValues('machine_nozzle_offset_x'))), 1)) - (resolveOrValue('machine_width') / 2 if resolveOrValue('machine_center_is_zero') else 0)", "value": "((resolveOrValue('machine_width') / 2 + resolveOrValue('prime_tower_size') / 2) if resolveOrValue('machine_shape') == 'elliptic' else (resolveOrValue('machine_width') - (resolveOrValue('prime_tower_base_size') if (resolveOrValue('adhesion_type') == 'raft' or resolveOrValue('prime_tower_brim_enable')) else 0) - max(max(extruderValues('travel_avoid_distance')) + max(extruderValues('machine_nozzle_offset_x')) + max(extruderValues('support_offset')) + (extruderValue(skirt_brim_extruder_nr, 'skirt_brim_line_width') * extruderValue(skirt_brim_extruder_nr, 'skirt_line_count') * extruderValue(skirt_brim_extruder_nr, 'initial_layer_line_width_factor') / 100 + extruderValue(skirt_brim_extruder_nr, 'skirt_gap') if resolveOrValue('adhesion_type') == 'skirt' else 0) + (resolveOrValue('draft_shield_dist') if resolveOrValue('draft_shield_enabled') else 0), max(map(abs, extruderValues('machine_nozzle_offset_x'))), 1))) - (resolveOrValue('machine_width') / 2 if resolveOrValue('machine_center_is_zero') else 0)",
"maximum_value": "(machine_width / 2 if machine_center_is_zero else machine_width) - (resolveOrValue('prime_tower_base_size') if (resolveOrValue('adhesion_type') == 'raft' or resolveOrValue('prime_tower_brim_enable')) else 0)", "maximum_value": "(machine_width / 2 if machine_center_is_zero else machine_width) - (resolveOrValue('prime_tower_base_size') if (resolveOrValue('adhesion_type') == 'raft' or resolveOrValue('prime_tower_brim_enable')) else 0)",
"minimum_value": "resolveOrValue('prime_tower_size') + (resolveOrValue('prime_tower_base_size') if (resolveOrValue('adhesion_type') == 'raft' or resolveOrValue('prime_tower_brim_enable')) else 0) - (machine_width / 2 if machine_center_is_zero else 0)", "minimum_value": "resolveOrValue('prime_tower_size') + (resolveOrValue('prime_tower_base_size') if (resolveOrValue('adhesion_type') == 'raft' or resolveOrValue('prime_tower_brim_enable')) else 0) - (machine_width / 2 if machine_center_is_zero else 0)",
"settable_per_mesh": false, "settable_per_mesh": false,
@ -6838,7 +6883,7 @@
"description": "The y coordinate of the position of the prime tower.", "description": "The y coordinate of the position of the prime tower.",
"type": "float", "type": "float",
"unit": "mm", "unit": "mm",
"enabled": "resolveOrValue('prime_tower_enable')", "enabled": "prime_tower_enable",
"default_value": 200, "default_value": 200,
"value": "machine_depth - prime_tower_size - (resolveOrValue('prime_tower_base_size') if (resolveOrValue('adhesion_type') == 'raft' or resolveOrValue('prime_tower_brim_enable')) else 0) - max(max(extruderValues('travel_avoid_distance')) + max(extruderValues('machine_nozzle_offset_y')) + max(extruderValues('support_offset')) + (extruderValue(skirt_brim_extruder_nr, 'skirt_brim_line_width') * extruderValue(skirt_brim_extruder_nr, 'skirt_line_count') * extruderValue(skirt_brim_extruder_nr, 'initial_layer_line_width_factor') / 100 + extruderValue(skirt_brim_extruder_nr, 'skirt_gap') if resolveOrValue('adhesion_type') == 'skirt' else 0) + (resolveOrValue('draft_shield_dist') if resolveOrValue('draft_shield_enabled') else 0), max(map(abs, extruderValues('machine_nozzle_offset_y'))), 1) - (resolveOrValue('machine_depth') / 2 if resolveOrValue('machine_center_is_zero') else 0)", "value": "machine_depth - prime_tower_size - (resolveOrValue('prime_tower_base_size') if (resolveOrValue('adhesion_type') == 'raft' or resolveOrValue('prime_tower_brim_enable')) else 0) - max(max(extruderValues('travel_avoid_distance')) + max(extruderValues('machine_nozzle_offset_y')) + max(extruderValues('support_offset')) + (extruderValue(skirt_brim_extruder_nr, 'skirt_brim_line_width') * extruderValue(skirt_brim_extruder_nr, 'skirt_line_count') * extruderValue(skirt_brim_extruder_nr, 'initial_layer_line_width_factor') / 100 + extruderValue(skirt_brim_extruder_nr, 'skirt_gap') if resolveOrValue('adhesion_type') == 'skirt' else 0) + (resolveOrValue('draft_shield_dist') if resolveOrValue('draft_shield_enabled') else 0), max(map(abs, extruderValues('machine_nozzle_offset_y'))), 1) - (resolveOrValue('machine_depth') / 2 if resolveOrValue('machine_center_is_zero') else 0)",
"maximum_value": "(machine_depth / 2 - resolveOrValue('prime_tower_size') if machine_center_is_zero else machine_depth - resolveOrValue('prime_tower_size')) - (resolveOrValue('prime_tower_base_size') if (resolveOrValue('adhesion_type') == 'raft' or resolveOrValue('prime_tower_brim_enable')) else 0)", "maximum_value": "(machine_depth / 2 - resolveOrValue('prime_tower_size') if machine_center_is_zero else machine_depth - resolveOrValue('prime_tower_size')) - (resolveOrValue('prime_tower_base_size') if (resolveOrValue('adhesion_type') == 'raft' or resolveOrValue('prime_tower_brim_enable')) else 0)",
@ -6851,7 +6896,7 @@
"label": "Wipe Inactive Nozzle on Prime Tower", "label": "Wipe Inactive Nozzle on Prime Tower",
"description": "After printing the prime tower with one nozzle, wipe the oozed material from the other nozzle off on the prime tower.", "description": "After printing the prime tower with one nozzle, wipe the oozed material from the other nozzle off on the prime tower.",
"type": "bool", "type": "bool",
"enabled": "resolveOrValue('prime_tower_enable')", "enabled": "prime_tower_enable",
"default_value": true, "default_value": true,
"settable_per_mesh": false, "settable_per_mesh": false,
"settable_per_extruder": true "settable_per_extruder": true
@ -6862,7 +6907,7 @@
"label": "Prime Tower Base", "label": "Prime Tower Base",
"description": "By enabling this setting, your prime-tower will get a brim, even if the model doesn't. If you want a sturdier base for a high tower, you can increase the base height.", "description": "By enabling this setting, your prime-tower will get a brim, even if the model doesn't. If you want a sturdier base for a high tower, you can increase the base height.",
"type": "bool", "type": "bool",
"enabled": "resolveOrValue('prime_tower_enable') and resolveOrValue('adhesion_type') != 'raft'", "enabled": "prime_tower_enable and resolveOrValue('adhesion_type') != 'raft'",
"default_value": false, "default_value": false,
"settable_per_mesh": false, "settable_per_mesh": false,
"settable_per_extruder": false "settable_per_extruder": false
@ -6874,7 +6919,7 @@
"description": "The width of the prime tower brim/base. A larger base enhances adhesion to the build plate, but also reduces the effective print area.", "description": "The width of the prime tower brim/base. A larger base enhances adhesion to the build plate, but also reduces the effective print area.",
"type": "float", "type": "float",
"unit": "mm", "unit": "mm",
"enabled": "resolveOrValue('prime_tower_enable') and (resolveOrValue('prime_tower_brim_enable') or resolveOrValue('adhesion_type') == 'raft')", "enabled": "prime_tower_enable and (resolveOrValue('prime_tower_brim_enable') or resolveOrValue('adhesion_type') == 'raft')",
"default_value": 1.2, "default_value": 1.2,
"minimum_value": "0", "minimum_value": "0",
"maximum_value": "min(0.5 * machine_width, 0.5 * machine_depth)", "maximum_value": "min(0.5 * machine_width, 0.5 * machine_depth)",
@ -6888,7 +6933,7 @@
"description": "The height of the prime tower base. Increasing this value will result in a more sturdy prime tower because the base will be wider. If this setting is too low, the prime tower will not have a sturdy base.", "description": "The height of the prime tower base. Increasing this value will result in a more sturdy prime tower because the base will be wider. If this setting is too low, the prime tower will not have a sturdy base.",
"type": "float", "type": "float",
"unit": "mm", "unit": "mm",
"enabled": "resolveOrValue('prime_tower_enable') and (resolveOrValue('prime_tower_brim_enable') or resolveOrValue('adhesion_type') == 'raft')", "enabled": "prime_tower_enable and (resolveOrValue('prime_tower_brim_enable') or resolveOrValue('adhesion_type') == 'raft')",
"default_value": 0, "default_value": 0,
"minimum_value": "0", "minimum_value": "0",
"maximum_value": "machine_height", "maximum_value": "machine_height",
@ -6900,7 +6945,7 @@
"label": "Prime Tower Base Slope", "label": "Prime Tower Base Slope",
"description": "The magnitude factor used for the slope of the prime tower base. If you increase this value, the base will become slimmer. If you decrease it, the base will become thicker.", "description": "The magnitude factor used for the slope of the prime tower base. If you increase this value, the base will become slimmer. If you decrease it, the base will become thicker.",
"type": "float", "type": "float",
"enabled": "resolveOrValue('prime_tower_enable') and (resolveOrValue('prime_tower_brim_enable') or resolveOrValue('adhesion_type') == 'raft')", "enabled": "prime_tower_enable and (resolveOrValue('prime_tower_brim_enable') or resolveOrValue('adhesion_type') == 'raft')",
"default_value": 4, "default_value": 4,
"minimum_value": "0", "minimum_value": "0",
"maximum_value": "10", "maximum_value": "10",
@ -6918,7 +6963,7 @@
"minimum_value": "0", "minimum_value": "0",
"minimum_value_warning": "raft_base_line_width", "minimum_value_warning": "raft_base_line_width",
"maximum_value_warning": "100", "maximum_value_warning": "100",
"enabled": "resolveOrValue('prime_tower_enable') and resolveOrValue('adhesion_type') == 'raft'", "enabled": "prime_tower_enable and resolveOrValue('adhesion_type') == 'raft'",
"settable_per_mesh": false, "settable_per_mesh": false,
"settable_per_extruder": true, "settable_per_extruder": true,
"limit_to_extruder": "raft_base_extruder_nr" "limit_to_extruder": "raft_base_extruder_nr"
@ -7246,6 +7291,16 @@
"settable_per_extruder": false, "settable_per_extruder": false,
"settable_per_meshgroup": false "settable_per_meshgroup": false
}, },
"user_defined_print_order_enabled":
{
"label": "Set Print Sequence Manually",
"description": "Allows to order the object list to set the print sequence manually. First object from the list will be printed first.",
"type": "bool",
"default_value": false,
"settable_per_mesh": false,
"settable_per_extruder": false,
"enabled": "print_sequence == 'one_at_a_time'"
},
"infill_mesh": "infill_mesh":
{ {
"label": "Infill Mesh", "label": "Infill Mesh",

View file

@ -11,6 +11,7 @@
"exclude_materials": [], "exclude_materials": [],
"first_start_actions": [ "MachineSettingsAction" ], "first_start_actions": [ "MachineSettingsAction" ],
"has_materials": true, "has_materials": true,
"machine_extruder_trains": { "0": "ratrig_base_extruder_0" },
"preferred_material": "generic_pla", "preferred_material": "generic_pla",
"preferred_quality_type": "standard", "preferred_quality_type": "standard",
"quality_definition": "ratrig_base", "quality_definition": "ratrig_base",

View file

@ -8,7 +8,7 @@
"platform": "ratrig_vcore3_200.stl", "platform": "ratrig_vcore3_200.stl",
"platform_offset": [ "platform_offset": [
0, 0,
5, 0,
0 0
], ],
"weight": 16 "weight": 16

View file

@ -27,7 +27,6 @@
"cool_fan_full_at_height": { "value": "layer_height_0 + 2 * layer_height" }, "cool_fan_full_at_height": { "value": "layer_height_0 + 2 * layer_height" },
"cool_min_layer_time": { "value": 2 }, "cool_min_layer_time": { "value": 2 },
"fill_outline_gaps": { "value": false }, "fill_outline_gaps": { "value": false },
"filter_out_tiny_gaps": { "value": false },
"gantry_height": { "value": 30 }, "gantry_height": { "value": 30 },
"infill_before_walls": { "value": false }, "infill_before_walls": { "value": false },
"infill_overlap": { "value": 30 }, "infill_overlap": { "value": 30 },

View file

@ -11,7 +11,7 @@
"machine_extruder_trains": { "0": "ratrig_base_extruder_0" }, "machine_extruder_trains": { "0": "ratrig_base_extruder_0" },
"platform_offset": [ "platform_offset": [
0, 0,
5, 0,
0 0
], ],
"preferred_variant_name": "0.4mm Nozzle", "preferred_variant_name": "0.4mm Nozzle",
@ -34,7 +34,6 @@
"cool_fan_full_at_height": { "value": "layer_height_0 + 2 * layer_height" }, "cool_fan_full_at_height": { "value": "layer_height_0 + 2 * layer_height" },
"cool_min_layer_time": { "value": 2 }, "cool_min_layer_time": { "value": 2 },
"fill_outline_gaps": { "value": false }, "fill_outline_gaps": { "value": false },
"filter_out_tiny_gaps": { "value": false },
"gantry_height": { "value": 30 }, "gantry_height": { "value": 30 },
"infill_before_walls": { "value": false }, "infill_before_walls": { "value": false },
"infill_overlap": { "value": 30 }, "infill_overlap": { "value": 30 },

View file

@ -8,8 +8,8 @@
"author": "Ultimaker", "author": "Ultimaker",
"manufacturer": "Ultimaker B.V.", "manufacturer": "Ultimaker B.V.",
"exclude_materials": [ "exclude_materials": [
"generic_hips", "structur3d_",
"structur3d_dap100silicone" "generic_hips"
] ]
}, },
"overrides": "overrides":
@ -96,16 +96,14 @@
"raft_interface_thickness": { "value": "(raft_base_thickness + raft_surface_thickness) / 2" }, "raft_interface_thickness": { "value": "(raft_base_thickness + raft_surface_thickness) / 2" },
"raft_speed": { "value": 15 }, "raft_speed": { "value": 15 },
"raft_surface_fan_speed": { "value": "cool_fan_speed_min" }, "raft_surface_fan_speed": { "value": "cool_fan_speed_min" },
"raft_surface_monotonic": { "value": true },
"raft_surface_speed": { "value": "speed_topbottom" }, "raft_surface_speed": { "value": "speed_topbottom" },
"relative_extrusion": "relative_extrusion": { "enabled": false },
{
"enabled": false,
"value": false
},
"retraction_combing": { "value": "'no_outer_surfaces'" }, "retraction_combing": { "value": "'no_outer_surfaces'" },
"retraction_combing_max_distance": { "value": 15 }, "retraction_combing_max_distance": { "value": 15 },
"retraction_count_max": { "value": 25 }, "retraction_count_max": { "value": 25 },
"retraction_extrusion_window": { "value": 1 }, "retraction_extrusion_window": { "value": 1 },
"retraction_min_travel": { "value": 5 },
"roofing_layer_count": { "value": "1" }, "roofing_layer_count": { "value": "1" },
"roofing_material_flow": { "value": "material_flow" }, "roofing_material_flow": { "value": "material_flow" },
"skin_angles": { "value": "[] if infill_pattern not in ['cross', 'cross_3d'] else [20, 110]" }, "skin_angles": { "value": "[] if infill_pattern not in ['cross', 'cross_3d'] else [20, 110]" },

View file

@ -10,23 +10,23 @@
"file_formats": "text/x-gcode", "file_formats": "text/x-gcode",
"platform": "ultimaker2_platform.obj", "platform": "ultimaker2_platform.obj",
"exclude_materials": [ "exclude_materials": [
"generic_bam",
"generic_cffcpe",
"generic_cffpa",
"generic_flexible",
"generic_gffcpe",
"generic_gffpa",
"generic_hips", "generic_hips",
"generic_petcf",
"generic_petg", "generic_petg",
"structur3d_dap100silicone", "generic_pva",
"ultimaker_petg_red", "generic_tough_pla",
"ultimaker_petg_blue", "structur3d_",
"ultimaker_petg_grey", "ultimaker_bam",
"ultimaker_petg_black", "ultimaker_petcf",
"ultimaker_petg_green", "ultimaker_petg",
"ultimaker_petg_white", "ultimaker_pva",
"ultimaker_petg_orange", "ultimaker_tough_pla"
"ultimaker_petg_silver",
"ultimaker_petg_yellow",
"ultimaker_petg_transparent",
"ultimaker_petg_red_translucent",
"ultimaker_petg_blue_translucent",
"ultimaker_petg_green_translucent",
"ultimaker_petg_yellow_fluorescent"
], ],
"firmware_file": "MarlinUltimaker2.hex", "firmware_file": "MarlinUltimaker2.hex",
"has_machine_quality": true, "has_machine_quality": true,

View file

@ -9,43 +9,23 @@
"file_formats": "text/x-gcode", "file_formats": "text/x-gcode",
"platform": "ultimaker2_platform.obj", "platform": "ultimaker2_platform.obj",
"exclude_materials": [ "exclude_materials": [
"generic_hips",
"generic_petg",
"generic_bam", "generic_bam",
"ultimaker_bam",
"generic_pva",
"ultimaker_pva",
"generic_tough_pla",
"ultimaker_tough_pla_black",
"ultimaker_tough_pla_green",
"ultimaker_tough_pla_red",
"ultimaker_tough_pla_white",
"ultimaker_tough_pla_blue",
"ultimaker_tough_pla_gray",
"ultimaker_tough_pla_yellow",
"generic_cffcpe", "generic_cffcpe",
"generic_cffpa", "generic_cffpa",
"generic_flexible",
"generic_gffcpe", "generic_gffcpe",
"generic_gffpa", "generic_gffpa",
"generic_hips",
"generic_petcf", "generic_petcf",
"structur3d_dap100silicone", "generic_petg",
"ultimaker_petg_red", "generic_pva",
"ultimaker_petg_blue", "generic_tough_pla",
"ultimaker_petg_grey", "structur3d_",
"ultimaker_petg_black", "ultimaker_bam",
"ultimaker_petg_green", "ultimaker_petcf",
"ultimaker_petg_white", "ultimaker_petg",
"ultimaker_petg_orange", "ultimaker_pva",
"ultimaker_petg_silver", "ultimaker_tough_pla"
"ultimaker_petg_yellow",
"ultimaker_petg_transparent",
"ultimaker_petg_red_translucent",
"ultimaker_petg_blue_translucent",
"ultimaker_petg_green_translucent",
"ultimaker_petg_yellow_fluorescent",
"ultimaker_petcf_black",
"ultimaker_petcf_blue",
"ultimaker_petcf_gray"
], ],
"firmware_file": "MarlinUltimaker2plus.hex", "firmware_file": "MarlinUltimaker2plus.hex",
"first_start_actions": [], "first_start_actions": [],

View file

@ -9,20 +9,18 @@
"file_formats": "application/x-ufp;text/x-gcode", "file_formats": "application/x-ufp;text/x-gcode",
"platform": "ultimaker2_plus_connect_platform.obj", "platform": "ultimaker2_plus_connect_platform.obj",
"exclude_materials": [ "exclude_materials": [
"generic_hips",
"generic_bam", "generic_bam",
"ultimaker_bam",
"generic_pva",
"ultimaker_pva",
"ultimaker_petcf_black",
"ultimaker_petcf_blue",
"ultimaker_petcf_gray",
"generic_petcf",
"generic_cffcpe", "generic_cffcpe",
"generic_cffpa", "generic_cffpa",
"generic_flexible",
"generic_gffcpe", "generic_gffcpe",
"generic_gffpa", "generic_gffpa",
"structur3d_dap100silicone" "generic_hips",
"generic_pva",
"structur3d_",
"ultimaker_bam",
"ultimaker_petcf",
"ultimaker_pva"
], ],
"first_start_actions": [], "first_start_actions": [],
"has_machine_materials": true, "has_machine_materials": true,

View file

@ -13,16 +13,15 @@
9066 9066
], ],
"exclude_materials": [ "exclude_materials": [
"generic_hips",
"generic_cffcpe", "generic_cffcpe",
"generic_cffpa", "generic_cffpa",
"generic_flexible",
"generic_gffcpe", "generic_gffcpe",
"generic_gffpa", "generic_gffpa",
"generic_hips",
"generic_petcf", "generic_petcf",
"ultimaker_petcf_black", "structur3d_",
"ultimaker_petcf_blue", "ultimaker_petcf"
"ultimaker_petcf_gray",
"structur3d_dap100silicone"
], ],
"firmware_update_info": "firmware_update_info":
{ {
@ -156,7 +155,6 @@
"retraction_hop": { "value": "2" }, "retraction_hop": { "value": "2" },
"retraction_hop_enabled": { "value": "extruders_enabled_count > 1" }, "retraction_hop_enabled": { "value": "extruders_enabled_count > 1" },
"retraction_hop_only_when_collides": { "value": "True" }, "retraction_hop_only_when_collides": { "value": "True" },
"retraction_min_travel": { "value": "5" },
"retraction_prime_speed": { "value": "15" }, "retraction_prime_speed": { "value": "15" },
"skin_overlap": { "value": "10" }, "skin_overlap": { "value": "10" },
"speed_prime_tower": { "value": "speed_topbottom" }, "speed_prime_tower": { "value": "speed_topbottom" },

View file

@ -28,7 +28,6 @@
"generic_hips_175", "generic_hips_175",
"generic_pc_175", "generic_pc_175",
"ultimaker_rapidrinse_175", "ultimaker_rapidrinse_175",
"ultimaker_sr30_175",
"generic_tpu_175", "generic_tpu_175",
"goofoo_", "goofoo_",
"ideagen3D_", "ideagen3D_",
@ -373,19 +372,17 @@
"retraction_hop": { "value": 0.4 }, "retraction_hop": { "value": 0.4 },
"retraction_hop_enabled": { "value": true }, "retraction_hop_enabled": { "value": true },
"retraction_hop_only_when_collides": { "value": false }, "retraction_hop_only_when_collides": { "value": false },
"retraction_min_travel": { "value": "line_width * 4" },
"retraction_prime_speed": { "value": "retraction_speed" }, "retraction_prime_speed": { "value": "retraction_speed" },
"retraction_speed": { "value": 5 }, "retraction_speed": { "value": 5 },
"roofing_layer_count": { "value": 2 }, "roofing_layer_count": { "value": 2 },
"roofing_material_flow": { "value": "material_flow" },
"roofing_monotonic": { "value": true }, "roofing_monotonic": { "value": true },
"skin_material_flow": { "value": "0.95*material_flow" }, "skin_material_flow": { "value": "0.95*material_flow" },
"skin_monotonic": { "value": true },
"skin_outline_count": { "value": 0 }, "skin_outline_count": { "value": 0 },
"skin_overlap": { "value": 0 }, "skin_overlap": { "value": 0 },
"skin_preshrink": { "value": 0 }, "skin_preshrink": { "value": 0 },
"skirt_brim_material_flow": { "value": "material_flow" }, "skirt_brim_material_flow": { "value": "material_flow" },
"skirt_brim_minimal_length": { "value": 500 }, "skirt_brim_minimal_length": { "value": 500 },
"small_skin_width": { "value": 4 },
"speed_equalize_flow_width_factor": { "value": 0 }, "speed_equalize_flow_width_factor": { "value": 0 },
"speed_prime_tower": { "value": "speed_topbottom" }, "speed_prime_tower": { "value": "speed_topbottom" },
"speed_print": { "value": 50 }, "speed_print": { "value": 50 },
@ -426,7 +423,7 @@
"travel_avoid_other_parts": { "value": false }, "travel_avoid_other_parts": { "value": false },
"wall_0_inset": { "value": 0 }, "wall_0_inset": { "value": 0 },
"wall_0_material_flow": { "value": "material_flow" }, "wall_0_material_flow": { "value": "material_flow" },
"wall_0_wipe_dist": { "value": 0 }, "wall_0_wipe_dist": { "value": 0.8 },
"wall_material_flow": { "value": "material_flow" }, "wall_material_flow": { "value": "material_flow" },
"wall_x_material_flow": { "value": "material_flow" }, "wall_x_material_flow": { "value": "material_flow" },
"xy_offset": { "value": 0 }, "xy_offset": { "value": 0 },

View file

@ -35,7 +35,6 @@
"generic_nylon_175", "generic_nylon_175",
"generic_hips_175", "generic_hips_175",
"generic_pc_175", "generic_pc_175",
"ultimaker_sr30_175",
"generic_tpu_175", "generic_tpu_175",
"goofoo_", "goofoo_",
"ideagen3D_", "ideagen3D_",

View file

@ -10,43 +10,22 @@
"file_formats": "text/x-gcode", "file_formats": "text/x-gcode",
"platform": "ultimaker_platform.3mf", "platform": "ultimaker_platform.3mf",
"exclude_materials": [ "exclude_materials": [
"generic_hips",
"generic_petg",
"generic_bam",
"ultimaker_bam",
"generic_pva",
"ultimaker_pva",
"generic_tough_pla",
"ultimaker_tough_pla_black",
"ultimaker_tough_pla_green",
"ultimaker_tough_pla_red",
"ultimaker_tough_pla_white",
"ultimaker_tough_pla_blue",
"ultimaker_tough_pla_gray",
"ultimaker_tough_pla_yellow",
"generic_cffcpe", "generic_cffcpe",
"generic_cffpa", "generic_cffpa",
"generic_flexible",
"generic_gffcpe", "generic_gffcpe",
"generic_gffpa", "generic_gffpa",
"generic_hips",
"generic_petcf", "generic_petcf",
"structur3d_dap100silicone", "generic_petg",
"ultimaker_petg_blue", "generic_pva",
"ultimaker_petg_grey", "generic_tough_pla",
"ultimaker_petg_black", "structur3d_",
"ultimaker_petg_green", "ultimaker_bam",
"ultimaker_petg_white", "ultimaker_petcf",
"ultimaker_petg_orange", "ultimaker_petg",
"ultimaker_petg_silver", "ultimaker_pva",
"ultimaker_petg_yellow", "ultimaker_tough_pla"
"ultimaker_petg_transparent",
"ultimaker_petg_red_translucent",
"ultimaker_petg_blue_translucent",
"ultimaker_petg_green_translucent",
"ultimaker_petg_yellow_fluorescent",
"ultimaker_petg_red",
"ultimaker_petcf_black",
"ultimaker_petcf_blue",
"ultimaker_petcf_gray"
], ],
"firmware_file": "MarlinUltimaker-{baudrate}.hex", "firmware_file": "MarlinUltimaker-{baudrate}.hex",
"firmware_hbk_file": "MarlinUltimaker-HBK-{baudrate}.hex", "firmware_hbk_file": "MarlinUltimaker-HBK-{baudrate}.hex",

View file

@ -10,43 +10,22 @@
"file_formats": "text/x-gcode", "file_formats": "text/x-gcode",
"platform": "ultimaker_platform.3mf", "platform": "ultimaker_platform.3mf",
"exclude_materials": [ "exclude_materials": [
"generic_hips",
"generic_petg",
"generic_bam",
"ultimaker_bam",
"generic_pva",
"ultimaker_pva",
"generic_tough_pla",
"ultimaker_tough_pla_black",
"ultimaker_tough_pla_green",
"ultimaker_tough_pla_red",
"ultimaker_tough_pla_white",
"ultimaker_tough_pla_blue",
"ultimaker_tough_pla_gray",
"ultimaker_tough_pla_yellow",
"generic_cffcpe", "generic_cffcpe",
"generic_cffpa", "generic_cffpa",
"generic_flexible",
"generic_gffcpe", "generic_gffcpe",
"generic_gffpa", "generic_gffpa",
"generic_hips",
"generic_petcf", "generic_petcf",
"structur3d_dap100silicone", "generic_petg",
"ultimaker_petg_blue", "generic_pva",
"ultimaker_petg_grey", "generic_tough_pla",
"ultimaker_petg_black", "structur3d_",
"ultimaker_petg_green", "ultimaker_bam",
"ultimaker_petg_white", "ultimaker_petcf",
"ultimaker_petg_orange", "ultimaker_petg",
"ultimaker_petg_silver", "ultimaker_pva",
"ultimaker_petg_yellow", "ultimaker_tough_pla"
"ultimaker_petg_transparent",
"ultimaker_petg_red_translucent",
"ultimaker_petg_blue_translucent",
"ultimaker_petg_green_translucent",
"ultimaker_petg_yellow_fluorescent",
"ultimaker_petg_red",
"ultimaker_petcf_black",
"ultimaker_petcf_blue",
"ultimaker_petcf_gray"
], ],
"firmware_file": "MarlinUltimaker-{baudrate}-dual.hex", "firmware_file": "MarlinUltimaker-{baudrate}-dual.hex",
"firmware_hbk_file": "MarlinUltimaker-HBK-{baudrate}-dual.hex", "firmware_hbk_file": "MarlinUltimaker-HBK-{baudrate}-dual.hex",

View file

@ -16,7 +16,8 @@
], ],
"exclude_materials": [ "exclude_materials": [
"generic_hips", "generic_hips",
"structur3d_dap100silicone" "generic_flexible",
"structur3d_"
], ],
"firmware_update_info": "firmware_update_info":
{ {
@ -108,7 +109,6 @@
"retraction_hop": { "value": "2" }, "retraction_hop": { "value": "2" },
"retraction_hop_enabled": { "value": "extruders_enabled_count > 1" }, "retraction_hop_enabled": { "value": "extruders_enabled_count > 1" },
"retraction_hop_only_when_collides": { "value": "True" }, "retraction_hop_only_when_collides": { "value": "True" },
"retraction_min_travel": { "value": "5" },
"retraction_prime_speed": { "value": "15" }, "retraction_prime_speed": { "value": "15" },
"retraction_speed": { "value": "45" }, "retraction_speed": { "value": "45" },
"speed_prime_tower": { "value": "speed_topbottom" }, "speed_prime_tower": { "value": "speed_topbottom" },

View file

@ -15,6 +15,11 @@
214476, 214476,
214477 214477
], ],
"exclude_materials": [
"generic_hips",
"generic_flexible",
"structur3d_"
],
"firmware_update_info": "firmware_update_info":
{ {
"check_urls": [ "https://software.ultimaker.com/releases/firmware/9051/stable/um-update.swu.version" ], "check_urls": [ "https://software.ultimaker.com/releases/firmware/9051/stable/um-update.swu.version" ],
@ -110,7 +115,6 @@
"retraction_hop": { "value": "2" }, "retraction_hop": { "value": "2" },
"retraction_hop_enabled": { "value": "extruders_enabled_count > 1" }, "retraction_hop_enabled": { "value": "extruders_enabled_count > 1" },
"retraction_hop_only_when_collides": { "value": "True" }, "retraction_hop_only_when_collides": { "value": "True" },
"retraction_min_travel": { "value": "5" },
"retraction_prime_speed": { "value": "15" }, "retraction_prime_speed": { "value": "15" },
"retraction_speed": { "value": "45" }, "retraction_speed": { "value": "45" },
"speed_prime_tower": { "value": "speed_topbottom" }, "speed_prime_tower": { "value": "speed_topbottom" },

View file

@ -44,6 +44,8 @@
}, },
"overrides": "overrides":
{ {
"machine_name": { "default_value": "Ultimaker S7" } "default_material_print_temperature": { "maximum_value_warning": "320" },
"machine_name": { "default_value": "Ultimaker S7" },
"material_print_temperature_layer_0": { "maximum_value_warning": "320" }
} }
} }

View file

@ -0,0 +1,18 @@
{
"version": 2,
"name": "Left Extruder",
"inherits": "fdmextruder",
"metadata":
{
"machine": "dagoma_sigma_pro_dual",
"position": "1"
},
"overrides":
{
"extruder_nr": { "default_value": 1 },
"machine_extruder_end_code": { "default_value": ";END T1\nG92 E0\nM83\nG1 E-55 F4700\nM82\nG92 E0\n" },
"machine_extruder_start_code": { "default_value": ";START T1\n;No temperature change\nG1 X0 Y77.5 F8000\nG92 E0\nM83\nG1 E50 F1200\nM82\nG92 E0\n" },
"machine_nozzle_size": { "default_value": 0.4 },
"material_diameter": { "default_value": 1.75 }
}
}

View file

@ -0,0 +1,18 @@
{
"version": 2,
"name": "Right Extruder",
"inherits": "fdmextruder",
"metadata":
{
"machine": "dagoma_sigma_pro_dual",
"position": "0"
},
"overrides":
{
"extruder_nr": { "default_value": 0 },
"machine_extruder_end_code": { "default_value": ";END T0\nG92 E0\nM83\nG1 E-55 F4700\nM82\nG92 E0\n" },
"machine_extruder_start_code": { "default_value": ";START T0\n;No temperature change\nG1 X0 Y77.5 F8000\nG92 E0\nM83\nG1 E50 F1200\nM82\nG92 E0\n" },
"machine_nozzle_size": { "default_value": 0.4 },
"material_diameter": { "default_value": 1.75 }
}
}

View file

@ -0,0 +1,18 @@
{
"version": 2,
"name": "Extruder",
"inherits": "fdmextruder",
"metadata":
{
"machine": "dagoma_sigma_pro",
"position": "0"
},
"overrides":
{
"extruder_nr": { "default_value": 0 },
"machine_extruder_end_code": { "default_value": ";END T0\nG92 E0\nM83\nG1 E-55 F4700\nM82\nG92 E0\n" },
"machine_extruder_start_code": { "default_value": ";START T0\n;No temperature change\nG1 X0 Y77.5 F8000\nG92 E0\nM83\nG1 E50 F1200\nM82\nG92 E0\n" },
"machine_nozzle_size": { "default_value": 0.4 },
"material_diameter": { "default_value": 1.75 }
}
}

View file

@ -0,0 +1,16 @@
{
"version": 2,
"name": "Extruder 1",
"inherits": "fdmextruder",
"metadata":
{
"machine": "geeetech_A20",
"position": "0"
},
"overrides":
{
"extruder_nr": { "default_value": 0 },
"machine_nozzle_size": { "default_value": 0.4 },
"material_diameter": { "default_value": 1.75 }
}
}

View file

@ -4946,6 +4946,14 @@ msgctxt "@action:inmenu menubar:edit"
msgid "Ungroup Models" msgid "Ungroup Models"
msgstr "Rozdělit modely" msgstr "Rozdělit modely"
msgctxt "@action:inmenu menubar:edit"
msgid "Print Before"
msgstr "Tisknout před"
msgctxt "@action:inmenu menubar:edit"
msgid "Print After"
msgstr "Tisknout po"
msgctxt "@button" msgctxt "@button"
msgid "Uninstall" msgid "Uninstall"
msgstr "Odinstalovat" msgstr "Odinstalovat"

View file

@ -2583,6 +2583,14 @@ msgctxt "print_sequence label"
msgid "Print Sequence" msgid "Print Sequence"
msgstr "Tisková sekvence" msgstr "Tisková sekvence"
msgctxt "user_defined_print_order_enabled label"
msgid "Set Print Sequence Manually"
msgstr "Nastavit tiskovou sekvenci ručně"
msgctxt "user_defined_print_order_enabled description"
msgid "Allows to order the object list to set the print sequence manually. First object from the list will be printed first."
msgstr "Umožňuje řadit seznam objektů pro ruční nastavení tiskové sekvence. První objekt ze seznamu bude vytisknut jako první."
msgctxt "speed_print label" msgctxt "speed_print label"
msgid "Print Speed" msgid "Print Speed"
msgstr "Rychlost tisku" msgstr "Rychlost tisku"

View file

@ -4565,6 +4565,14 @@ msgctxt "@action:inmenu menubar:edit"
msgid "Ungroup Models" msgid "Ungroup Models"
msgstr "" msgstr ""
msgctxt "@action:inmenu menubar:edit"
msgid "Print Before"
msgstr ""
msgctxt "@action:inmenu menubar:edit"
msgid "Print After"
msgstr ""
msgctxt "@button" msgctxt "@button"
msgid "Uninstall" msgid "Uninstall"
msgstr "" msgstr ""

View file

@ -4930,6 +4930,14 @@ msgctxt "@action:inmenu menubar:edit"
msgid "Ungroup Models" msgid "Ungroup Models"
msgstr "Gruppierung für Modelle aufheben" msgstr "Gruppierung für Modelle aufheben"
msgctxt "@action:inmenu menubar:edit"
msgid "Print Before"
msgstr "Vor dem Drucken"
msgctxt "@action:inmenu menubar:edit"
msgid "Print After"
msgstr "Nach dem Drucken"
msgctxt "@button" msgctxt "@button"
msgid "Uninstall" msgid "Uninstall"
msgstr "Deinstallieren" msgstr "Deinstallieren"

View file

@ -2580,6 +2580,14 @@ msgctxt "print_sequence label"
msgid "Print Sequence" msgid "Print Sequence"
msgstr "Druckreihenfolge" msgstr "Druckreihenfolge"
msgctxt "user_defined_print_order_enabled label"
msgid "Set Print Sequence Manually"
msgstr "Druckreihenfolge manuell einstellen"
msgctxt "user_defined_print_order_enabled description"
msgid "Allows to order the object list to set the print sequence manually. First object from the list will be printed first."
msgstr "Ermöglicht das Ordnen der Objektliste, um die Druckreihenfolge manuell festzulegen. Das erste Objekt aus der Liste wird zuerst gedruckt."
msgctxt "speed_print label" msgctxt "speed_print label"
msgid "Print Speed" msgid "Print Speed"
msgstr "Druckgeschwindigkeit" msgstr "Druckgeschwindigkeit"

View file

@ -4931,6 +4931,14 @@ msgctxt "@action:inmenu menubar:edit"
msgid "Ungroup Models" msgid "Ungroup Models"
msgstr "Desagrupar modelos" msgstr "Desagrupar modelos"
msgctxt "@action:inmenu menubar:edit"
msgid "Print Before"
msgstr "Imprimir antes"
msgctxt "@action:inmenu menubar:edit"
msgid "Print After"
msgstr "Imprimir después"
msgctxt "@button" msgctxt "@button"
msgid "Uninstall" msgid "Uninstall"
msgstr "Desinstalar" msgstr "Desinstalar"

View file

@ -2580,6 +2580,14 @@ msgctxt "print_sequence label"
msgid "Print Sequence" msgid "Print Sequence"
msgstr "Secuencia de impresión" msgstr "Secuencia de impresión"
msgctxt "user_defined_print_order_enabled label"
msgid "Set Print Sequence Manually"
msgstr "Establecer secuencia de impresión manualmente"
msgctxt "user_defined_print_order_enabled description"
msgid "Allows to order the object list to set the print sequence manually. First object from the list will be printed first."
msgstr "Permite ordenar la lista de objetos para establecer la secuencia de impresión manualmente. El primer objeto de la lista se imprimirá primero."
msgctxt "speed_print label" msgctxt "speed_print label"
msgid "Print Speed" msgid "Print Speed"
msgstr "Velocidad de impresión" msgstr "Velocidad de impresión"

View file

@ -4588,6 +4588,14 @@ msgctxt "print_sequence option one_at_a_time"
msgid "One at a Time" msgid "One at a Time"
msgstr "" msgstr ""
msgctxt "user_defined_print_order_enabled label"
msgid "Set Print Sequence Manually"
msgstr ""
msgctxt "user_defined_print_order_enabled description"
msgid "Allows to order the object list to set the print sequence manually. First object from the list will be printed first."
msgstr ""
msgctxt "infill_mesh label" msgctxt "infill_mesh label"
msgid "Infill Mesh" msgid "Infill Mesh"
msgstr "" msgstr ""

View file

@ -4899,6 +4899,14 @@ msgctxt "@action:inmenu menubar:edit"
msgid "Ungroup Models" msgid "Ungroup Models"
msgstr "Poista mallien ryhmitys" msgstr "Poista mallien ryhmitys"
msgctxt "@action:inmenu menubar:edit"
msgid "Print Before"
msgstr "Tulosta ennen"
msgctxt "@action:inmenu menubar:edit"
msgid "Print After"
msgstr "Tulosta jälkeen"
msgctxt "@button" msgctxt "@button"
msgid "Uninstall" msgid "Uninstall"
msgstr "" msgstr ""

View file

@ -2578,6 +2578,14 @@ msgctxt "print_sequence label"
msgid "Print Sequence" msgid "Print Sequence"
msgstr "Tulostusjärjestys" msgstr "Tulostusjärjestys"
msgctxt "user_defined_print_order_enabled label"
msgid "Set Print Sequence Manually"
msgstr "Aseta tulostusjärjestys manuaalisesti"
msgctxt "user_defined_print_order_enabled description"
msgid "Allows to order the object list to set the print sequence manually. First object from the list will be printed first."
msgstr "Mahdollistaa kohteiden järjestämisen tulostusjärjestyksen manuaaliseen asettamiseen. Listan ensimmäinen kohde tulostetaan ensin."
msgctxt "speed_print label" msgctxt "speed_print label"
msgid "Print Speed" msgid "Print Speed"
msgstr "Tulostusnopeus" msgstr "Tulostusnopeus"

View file

@ -4928,6 +4928,14 @@ msgctxt "@action:inmenu menubar:edit"
msgid "Ungroup Models" msgid "Ungroup Models"
msgstr "Dégrouper les modèles" msgstr "Dégrouper les modèles"
msgctxt "@action:inmenu menubar:edit"
msgid "Print Before"
msgstr "Imprimer avant"
msgctxt "@action:inmenu menubar:edit"
msgid "Print After"
msgstr "Imprimer après"
msgctxt "@button" msgctxt "@button"
msgid "Uninstall" msgid "Uninstall"
msgstr "Désinstaller" msgstr "Désinstaller"

View file

@ -2580,6 +2580,14 @@ msgctxt "print_sequence label"
msgid "Print Sequence" msgid "Print Sequence"
msgstr "Séquence d'impression" msgstr "Séquence d'impression"
msgctxt "user_defined_print_order_enabled label"
msgid "Set Print Sequence Manually"
msgstr "Définir la séquence d'impression manuellement"
msgctxt "user_defined_print_order_enabled description"
msgid "Allows to order the object list to set the print sequence manually. First object from the list will be printed first."
msgstr "Permet de classer la liste des objets pour définir manuellement la séquence d'impression. Le premier objet de la liste sera imprimé en premier."
msgctxt "speed_print label" msgctxt "speed_print label"
msgid "Print Speed" msgid "Print Speed"
msgstr "Vitesse dimpression" msgstr "Vitesse dimpression"

View file

@ -4913,6 +4913,14 @@ msgctxt "@action:inmenu menubar:edit"
msgid "Ungroup Models" msgid "Ungroup Models"
msgstr "Csoport bontása" msgstr "Csoport bontása"
msgctxt "@action:inmenu menubar:edit"
msgid "Print Before"
msgstr "Nyomtatás előtt"
msgctxt "@action:inmenu menubar:edit"
msgid "Print After"
msgstr "Nyomtatás után"
msgctxt "@button" msgctxt "@button"
msgid "Uninstall" msgid "Uninstall"
msgstr "" msgstr ""

View file

@ -2585,6 +2585,14 @@ msgctxt "print_sequence label"
msgid "Print Sequence" msgid "Print Sequence"
msgstr "Nyomtatási sorrend" msgstr "Nyomtatási sorrend"
msgctxt "user_defined_print_order_enabled label"
msgid "Set Print Sequence Manually"
msgstr "Nyomtatási sorrend kézi beállítása"
msgctxt "user_defined_print_order_enabled description"
msgid "Allows to order the object list to set the print sequence manually. First object from the list will be printed first."
msgstr "Lehetővé teszi az objektumlista rendezését a nyomtatási sorrend kézi beállításához. A lista első objektuma lesz először nyomtatva."
msgctxt "speed_print label" msgctxt "speed_print label"
msgid "Print Speed" msgid "Print Speed"
msgstr "Nyomtatási sebesség" msgstr "Nyomtatási sebesség"

View file

@ -4931,6 +4931,14 @@ msgctxt "@action:inmenu menubar:edit"
msgid "Ungroup Models" msgid "Ungroup Models"
msgstr "Separa modelli" msgstr "Separa modelli"
msgctxt "@action:inmenu menubar:edit"
msgid "Print Before"
msgstr "Stampa prima"
msgctxt "@action:inmenu menubar:edit"
msgid "Print After"
msgstr "Stampa dopo"
msgctxt "@button" msgctxt "@button"
msgid "Uninstall" msgid "Uninstall"
msgstr "Disinstalla" msgstr "Disinstalla"

View file

@ -2580,6 +2580,14 @@ msgctxt "print_sequence label"
msgid "Print Sequence" msgid "Print Sequence"
msgstr "Sequenza di stampa" msgstr "Sequenza di stampa"
msgctxt "user_defined_print_order_enabled label"
msgid "Set Print Sequence Manually"
msgstr "Imposta manualmente la sequenza di stampa"
msgctxt "user_defined_print_order_enabled description"
msgid "Allows to order the object list to set the print sequence manually. First object from the list will be printed first."
msgstr "Consente di ordinare l'elenco degli oggetti per impostare manualmente la sequenza di stampa. Il primo oggetto dell'elenco sarà stampato per primo."
msgctxt "speed_print label" msgctxt "speed_print label"
msgid "Print Speed" msgid "Print Speed"
msgstr "Velocità di stampa" msgstr "Velocità di stampa"

View file

@ -4914,6 +4914,14 @@ msgctxt "@action:inmenu menubar:edit"
msgid "Ungroup Models" msgid "Ungroup Models"
msgstr "モデルを非グループ化" msgstr "モデルを非グループ化"
msgctxt "@action:inmenu menubar:edit"
msgid "Print Before"
msgstr "印刷前"
msgctxt "@action:inmenu menubar:edit"
msgid "Print After"
msgstr "印刷後"
msgctxt "@button" msgctxt "@button"
msgid "Uninstall" msgid "Uninstall"
msgstr "アンインストール" msgstr "アンインストール"

View file

@ -2580,6 +2580,14 @@ msgctxt "print_sequence label"
msgid "Print Sequence" msgid "Print Sequence"
msgstr "印刷頻度" msgstr "印刷頻度"
msgctxt "user_defined_print_order_enabled label"
msgid "Set Print Sequence Manually"
msgstr "手動で印刷順序を設定する"
msgctxt "user_defined_print_order_enabled description"
msgid "Allows to order the object list to set the print sequence manually. First object from the list will be printed first."
msgstr "オブジェクトリストを並べ替えて、手動で印刷順序を設定することができます。リストの最初のオブジェクトが最初に印刷されます。"
msgctxt "speed_print label" msgctxt "speed_print label"
msgid "Print Speed" msgid "Print Speed"
msgstr "印刷速度" msgstr "印刷速度"

View file

@ -4917,6 +4917,14 @@ msgctxt "@action:inmenu menubar:edit"
msgid "Ungroup Models" msgid "Ungroup Models"
msgstr "모델 그룹 해제" msgstr "모델 그룹 해제"
msgctxt "@action:inmenu menubar:edit"
msgid "Print Before"
msgstr "인쇄 전"
msgctxt "@action:inmenu menubar:edit"
msgid "Print After"
msgstr "인쇄 후"
msgctxt "@button" msgctxt "@button"
msgid "Uninstall" msgid "Uninstall"
msgstr "설치 제거" msgstr "설치 제거"

View file

@ -2580,6 +2580,14 @@ msgctxt "print_sequence label"
msgid "Print Sequence" msgid "Print Sequence"
msgstr "프린팅 순서" msgstr "프린팅 순서"
msgctxt "user_defined_print_order_enabled label"
msgid "Set Print Sequence Manually"
msgstr "수동으로 인쇄 순서 설정"
msgctxt "user_defined_print_order_enabled description"
msgid "Allows to order the object list to set the print sequence manually. First object from the list will be printed first."
msgstr "객체 목록을 정렬하여 수동으로 인쇄 순서를 설정할 수 있습니다. 목록의 첫 번째 객체가 먼저 인쇄됩니다."
msgctxt "speed_print label" msgctxt "speed_print label"
msgid "Print Speed" msgid "Print Speed"
msgstr "프린팅 속도" msgstr "프린팅 속도"

View file

@ -4925,6 +4925,14 @@ msgctxt "@action:inmenu menubar:edit"
msgid "Ungroup Models" msgid "Ungroup Models"
msgstr "Groeperen van Modellen Opheffen" msgstr "Groeperen van Modellen Opheffen"
msgctxt "@action:inmenu menubar:edit"
msgid "Print Before"
msgstr "Afdrukken voor"
msgctxt "@action:inmenu menubar:edit"
msgid "Print After"
msgstr "Afdrukken na"
msgctxt "@button" msgctxt "@button"
msgid "Uninstall" msgid "Uninstall"
msgstr "De-installeren" msgstr "De-installeren"

View file

@ -2580,6 +2580,14 @@ msgctxt "print_sequence label"
msgid "Print Sequence" msgid "Print Sequence"
msgstr "Printvolgorde" msgstr "Printvolgorde"
msgctxt "user_defined_print_order_enabled label"
msgid "Set Print Sequence Manually"
msgstr "Handmatig afdrukvolgorde instellen"
msgctxt "user_defined_print_order_enabled description"
msgid "Allows to order the object list to set the print sequence manually. First object from the list will be printed first."
msgstr "Maakt het mogelijk de objectlijst te ordenen om de afdrukvolgorde handmatig in te stellen. Het eerste object van de lijst wordt als eerste afgedrukt."
msgctxt "speed_print label" msgctxt "speed_print label"
msgid "Print Speed" msgid "Print Speed"
msgstr "Printsnelheid" msgstr "Printsnelheid"

View file

@ -4916,6 +4916,14 @@ msgctxt "@action:inmenu menubar:edit"
msgid "Ungroup Models" msgid "Ungroup Models"
msgstr "Rozgrupuj modele" msgstr "Rozgrupuj modele"
msgctxt "@action:inmenu menubar:edit"
msgid "Print Before"
msgstr "Drukuj przed"
msgctxt "@action:inmenu menubar:edit"
msgid "Print After"
msgstr "Drukuj po"
msgctxt "@button" msgctxt "@button"
msgid "Uninstall" msgid "Uninstall"
msgstr "" msgstr ""

View file

@ -2584,6 +2584,14 @@ msgctxt "print_sequence label"
msgid "Print Sequence" msgid "Print Sequence"
msgstr "Sekwencja Wydruku" msgstr "Sekwencja Wydruku"
msgctxt "user_defined_print_order_enabled label"
msgid "Set Print Sequence Manually"
msgstr "Ręczne ustawienie kolejności drukowania"
msgctxt "user_defined_print_order_enabled description"
msgid "Allows to order the object list to set the print sequence manually. First object from the list will be printed first."
msgstr "Umożliwia ręczne ustawienie kolejności drukowania na liście obiektów. Pierwszy obiekt z listy zostanie wydrukowany jako pierwszy."
msgctxt "speed_print label" msgctxt "speed_print label"
msgid "Print Speed" msgid "Print Speed"
msgstr "Prędkość Druku" msgstr "Prędkość Druku"

View file

@ -4942,6 +4942,14 @@ msgctxt "@action:inmenu menubar:edit"
msgid "Ungroup Models" msgid "Ungroup Models"
msgstr "Desagrupar Modelos" msgstr "Desagrupar Modelos"
msgctxt "@action:inmenu menubar:edit"
msgid "Print Before"
msgstr "Imprimir antes"
msgctxt "@action:inmenu menubar:edit"
msgid "Print After"
msgstr "Imprimir depois"
msgctxt "@button" msgctxt "@button"
msgid "Uninstall" msgid "Uninstall"
msgstr "Desinstalar" msgstr "Desinstalar"

View file

@ -2585,6 +2585,14 @@ msgctxt "print_sequence label"
msgid "Print Sequence" msgid "Print Sequence"
msgstr "Sequência de Impressão" msgstr "Sequência de Impressão"
msgctxt "user_defined_print_order_enabled label"
msgid "Set Print Sequence Manually"
msgstr "Definir sequência de impressão manualmente"
msgctxt "user_defined_print_order_enabled description"
msgid "Allows to order the object list to set the print sequence manually. First object from the list will be printed first."
msgstr "Permite ordenar a lista de objetos para definir a sequência de impressão manualmente. O primeiro objeto da lista será impresso primeiro."
msgctxt "speed_print label" msgctxt "speed_print label"
msgid "Print Speed" msgid "Print Speed"
msgstr "Velocidade de Impressão" msgstr "Velocidade de Impressão"

View file

@ -4932,6 +4932,14 @@ msgctxt "@action:inmenu menubar:edit"
msgid "Ungroup Models" msgid "Ungroup Models"
msgstr "Desagrupar Modelos" msgstr "Desagrupar Modelos"
msgctxt "@action:inmenu menubar:edit"
msgid "Print Before"
msgstr "Imprimir antes"
msgctxt "@action:inmenu menubar:edit"
msgid "Print After"
msgstr "Imprimir depois"
msgctxt "@button" msgctxt "@button"
msgid "Uninstall" msgid "Uninstall"
msgstr "Desinstalar" msgstr "Desinstalar"

View file

@ -2580,6 +2580,14 @@ msgctxt "print_sequence label"
msgid "Print Sequence" msgid "Print Sequence"
msgstr "Sequência de impressão" msgstr "Sequência de impressão"
msgctxt "user_defined_print_order_enabled label"
msgid "Set Print Sequence Manually"
msgstr "Definir sequência de impressão manualmente"
msgctxt "user_defined_print_order_enabled description"
msgid "Allows to order the object list to set the print sequence manually. First object from the list will be printed first."
msgstr "Permite ordenar a lista de objetos para definir a sequência de impressão manualmente. O primeiro objeto da lista será impresso primeiro."
msgctxt "speed_print label" msgctxt "speed_print label"
msgid "Print Speed" msgid "Print Speed"
msgstr "Velocidade de Impressão" msgstr "Velocidade de Impressão"

View file

@ -4955,6 +4955,14 @@ msgctxt "@action:inmenu menubar:edit"
msgid "Ungroup Models" msgid "Ungroup Models"
msgstr "Разгруппировать модели" msgstr "Разгруппировать модели"
msgctxt "@action:inmenu menubar:edit"
msgid "Print Before"
msgstr "Печатать до"
msgctxt "@action:inmenu menubar:edit"
msgid "Print After"
msgstr "Печатать после"
msgctxt "@button" msgctxt "@button"
msgid "Uninstall" msgid "Uninstall"
msgstr "Удалить" msgstr "Удалить"

View file

@ -2580,6 +2580,14 @@ msgctxt "print_sequence label"
msgid "Print Sequence" msgid "Print Sequence"
msgstr "Последовательная печать" msgstr "Последовательная печать"
msgctxt "user_defined_print_order_enabled label"
msgid "Set Print Sequence Manually"
msgstr "Установить последовательность печати вручную"
msgctxt "user_defined_print_order_enabled description"
msgid "Allows to order the object list to set the print sequence manually. First object from the list will be printed first."
msgstr "Позволяет упорядочить список объектов для ручной настройки последовательности печати. Первый объект из списка будет напечатан первым."
msgctxt "speed_print label" msgctxt "speed_print label"
msgid "Print Speed" msgid "Print Speed"
msgstr "Скорость печати" msgstr "Скорость печати"

View file

@ -4931,6 +4931,14 @@ msgctxt "@action:inmenu menubar:edit"
msgid "Ungroup Models" msgid "Ungroup Models"
msgstr "Model Grubunu Çöz" msgstr "Model Grubunu Çöz"
msgctxt "@action:inmenu menubar:edit"
msgid "Print Before"
msgstr "Önce Yazdır"
msgctxt "@action:inmenu menubar:edit"
msgid "Print After"
msgstr "Sonra Yazdır"
msgctxt "@button" msgctxt "@button"
msgid "Uninstall" msgid "Uninstall"
msgstr "Kaldır" msgstr "Kaldır"

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