mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-25 15:44:04 -06:00
Merge pull request #18371 from Ultimaker/CURA-11403_save-PAP
Cura 11403 save pap
This commit is contained in:
commit
58eb62d976
24 changed files with 1161 additions and 163 deletions
|
@ -601,7 +601,9 @@ class CuraApplication(QtApplication):
|
||||||
preferences.addPreference("mesh/scale_to_fit", False)
|
preferences.addPreference("mesh/scale_to_fit", False)
|
||||||
preferences.addPreference("mesh/scale_tiny_meshes", True)
|
preferences.addPreference("mesh/scale_tiny_meshes", True)
|
||||||
preferences.addPreference("cura/dialog_on_project_save", True)
|
preferences.addPreference("cura/dialog_on_project_save", True)
|
||||||
|
preferences.addPreference("cura/dialog_on_ucp_project_save", True)
|
||||||
preferences.addPreference("cura/asked_dialog_on_project_save", False)
|
preferences.addPreference("cura/asked_dialog_on_project_save", False)
|
||||||
|
preferences.addPreference("cura/asked_dialog_on_ucp_project_save", False)
|
||||||
preferences.addPreference("cura/choice_on_profile_override", "always_ask")
|
preferences.addPreference("cura/choice_on_profile_override", "always_ask")
|
||||||
preferences.addPreference("cura/choice_on_open_project", "always_ask")
|
preferences.addPreference("cura/choice_on_open_project", "always_ask")
|
||||||
preferences.addPreference("cura/use_multi_build_plate", False)
|
preferences.addPreference("cura/use_multi_build_plate", False)
|
||||||
|
@ -1142,6 +1144,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()
|
||||||
|
|
38
plugins/3MFReader/SpecificSettingsModel.py
Normal file
38
plugins/3MFReader/SpecificSettingsModel.py
Normal 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 Qt
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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 = SettingDefinition.settingValueToString(setting_type, value) + " " + unit
|
||||||
|
else:
|
||||||
|
value = str(value)
|
||||||
|
|
||||||
|
self.appendItem({
|
||||||
|
"category": category,
|
||||||
|
"label": stack.getProperty(setting, "label"),
|
||||||
|
"value": value
|
||||||
|
})
|
|
@ -41,7 +41,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"]
|
||||||
)
|
)
|
||||||
|
|
|
@ -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:
|
||||||
|
@ -141,10 +143,15 @@ 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._load_profile = False
|
||||||
|
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._load_profile = False
|
||||||
|
self._user_settings = {}
|
||||||
|
|
||||||
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.
|
||||||
|
@ -228,11 +235,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 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 +260,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 +607,37 @@ 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()
|
||||||
|
if 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()
|
||||||
|
|
||||||
|
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,8 +658,11 @@ 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.setHasVisibleSelectSameProfileChanged(is_ucp)
|
||||||
|
self._dialog.setAllowCreatemachine(not 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
|
||||||
|
@ -648,6 +692,8 @@ 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()
|
||||||
|
self._dialog.setSelectSameProfileChecked(self._dialog.isCompatibleMachine)
|
||||||
|
|
||||||
# Block until the dialog is closed.
|
# Block until the dialog is closed.
|
||||||
self._dialog.waitForClose()
|
self._dialog.waitForClose()
|
||||||
|
@ -655,6 +701,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
if self._dialog.getResult() == {}:
|
if self._dialog.getResult() == {}:
|
||||||
return WorkspaceReader.PreReadResult.cancelled
|
return WorkspaceReader.PreReadResult.cancelled
|
||||||
|
|
||||||
|
self._load_profile = not is_ucp or (self._dialog.selectSameProfileChecked and self._dialog.isCompatibleMachine)
|
||||||
|
|
||||||
self._resolve_strategies = self._dialog.getResult()
|
self._resolve_strategies = self._dialog.getResult()
|
||||||
#
|
#
|
||||||
# There can be 3 resolve strategies coming from the dialog:
|
# There can be 3 resolve strategies coming from the dialog:
|
||||||
|
@ -690,16 +738,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 +809,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 +825,89 @@ 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 self._load_profile:
|
||||||
# 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)
|
||||||
|
|
||||||
# Prepare the machine
|
if not materials:
|
||||||
self._applyChangesToMachine(global_stack, extruder_stack_dict)
|
# 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 self._load_profile:
|
||||||
|
# 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, 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.
|
||||||
|
@ -866,6 +919,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
# 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 not self._load_profile:
|
||||||
|
# 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:
|
||||||
|
@ -1177,21 +1234,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:
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
@ -71,6 +73,11 @@ class WorkspaceDialog(QObject):
|
||||||
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._has_visible_select_same_profile = False
|
||||||
|
self._select_same_profile_checked = True
|
||||||
|
self._allow_create_machine = True
|
||||||
|
self._exported_settings_model = SpecificSettingsModel()
|
||||||
|
|
||||||
machineConflictChanged = pyqtSignal()
|
machineConflictChanged = pyqtSignal()
|
||||||
qualityChangesConflictChanged = pyqtSignal()
|
qualityChangesConflictChanged = pyqtSignal()
|
||||||
|
@ -94,6 +101,9 @@ class WorkspaceDialog(QObject):
|
||||||
extrudersChanged = pyqtSignal()
|
extrudersChanged = pyqtSignal()
|
||||||
isPrinterGroupChanged = pyqtSignal()
|
isPrinterGroupChanged = pyqtSignal()
|
||||||
missingPackagesChanged = pyqtSignal()
|
missingPackagesChanged = pyqtSignal()
|
||||||
|
isCompatibleMachineChanged = pyqtSignal()
|
||||||
|
hasVisibleSelectSameProfileChanged = pyqtSignal()
|
||||||
|
selectSameProfileCheckedChanged = pyqtSignal()
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = isPrinterGroupChanged)
|
@pyqtProperty(bool, notify = isPrinterGroupChanged)
|
||||||
def isPrinterGroup(self) -> bool:
|
def isPrinterGroup(self) -> bool:
|
||||||
|
@ -292,6 +302,50 @@ 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 setHasVisibleSelectSameProfileChanged(self, has_visible_select_same_profile):
|
||||||
|
if has_visible_select_same_profile != self._has_visible_select_same_profile:
|
||||||
|
self._has_visible_select_same_profile = has_visible_select_same_profile
|
||||||
|
self.hasVisibleSelectSameProfileChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = hasVisibleSelectSameProfileChanged)
|
||||||
|
def hasVisibleSelectSameProfile(self):
|
||||||
|
return self._has_visible_select_same_profile
|
||||||
|
|
||||||
|
def setSelectSameProfileChecked(self, select_same_profile_checked):
|
||||||
|
if select_same_profile_checked != self._select_same_profile_checked:
|
||||||
|
self._select_same_profile_checked = select_same_profile_checked
|
||||||
|
self.selectSameProfileCheckedChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = selectSameProfileCheckedChanged, fset = setSelectSameProfileChecked)
|
||||||
|
def selectSameProfileChecked(self):
|
||||||
|
return self._select_same_profile_checked
|
||||||
|
|
||||||
|
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, constant = True)
|
||||||
|
def exportedSettingModel(self):
|
||||||
|
return self._exported_settings_model
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def closeBackend(self) -> None:
|
def closeBackend(self) -> None:
|
||||||
|
|
|
@ -6,7 +6,7 @@ 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
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -165,26 +171,71 @@ 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.selectSameProfileChecked && 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.selectSameProfileChecked && manager.isCompatibleMachine
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkspaceRow
|
||||||
|
{
|
||||||
|
leftLabelText: catalog.i18nc("@action:label", "Specific settings")
|
||||||
|
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")
|
||||||
|
visible: !manager.selectSameProfileChecked || !manager.isCompatibleMachine
|
||||||
|
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.selectSameProfileChecked || !manager.isCompatibleMachine)
|
||||||
|
property bool shouldBeVisible: false
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UM.CheckBox
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@action:checkbox", "Select the same profile")
|
||||||
|
onEnabledChanged: manager.selectSameProfileChecked = enabled
|
||||||
|
tooltip: enabled ? "" : catalog.i18nc("@tooltip", "You can use the same profile only if you have the same printer as the project was published with")
|
||||||
|
visible: manager.hasVisibleSelectSameProfile && manager.isCompatibleMachine
|
||||||
|
|
||||||
|
checked: manager.selectSameProfileChecked
|
||||||
|
onCheckedChanged: manager.selectSameProfileChecked = checked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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,13 @@ 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
|
|
||||||
height: childrenRect.height
|
|
||||||
hoverEnabled: true
|
|
||||||
|
|
||||||
UM.ColorImage
|
text: comboboxTooltipText
|
||||||
{
|
visible: comboboxTooltipText != ""
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
38
plugins/3MFWriter/SettingExport.py
Normal file
38
plugins/3MFWriter/SettingExport.py
Normal 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
|
38
plugins/3MFWriter/SettingSelection.qml
Normal file
38
plugins/3MFWriter/SettingSelection.qml
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
// 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
|
||||||
|
enabled: modelData.selectable
|
||||||
|
}
|
||||||
|
|
||||||
|
UM.Label
|
||||||
|
{
|
||||||
|
text: modelData.value
|
||||||
|
}
|
||||||
|
|
||||||
|
UM.HelpIcon
|
||||||
|
{
|
||||||
|
UM.I18nCatalog { id: catalog; name: "cura" }
|
||||||
|
|
||||||
|
text: catalog.i18nc("@tooltip",
|
||||||
|
"This setting can't be exported because it depends on the used printer capacities")
|
||||||
|
visible: !modelData.selectable
|
||||||
|
}
|
||||||
|
}
|
49
plugins/3MFWriter/SettingsExportGroup.py
Normal file
49
plugins/3MFWriter/SettingsExportGroup.py
Normal 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
|
130
plugins/3MFWriter/SettingsExportModel.py
Normal file
130
plugins/3MFWriter/SettingsExportModel.py
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
# 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'}
|
||||||
|
|
||||||
|
def __init__(self, parent = None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._settings_groups = []
|
||||||
|
|
||||||
|
application = CuraApplication.getInstance()
|
||||||
|
|
||||||
|
# Display global settings
|
||||||
|
global_stack = application.getGlobalContainerStack()
|
||||||
|
self._settings_groups.append(SettingsExportGroup(global_stack,
|
||||||
|
"Global settings",
|
||||||
|
SettingsExportGroup.Category.Global,
|
||||||
|
self._exportSettings(global_stack)))
|
||||||
|
|
||||||
|
# Display per-extruder settings
|
||||||
|
extruders_stacks = ExtruderManager.getInstance().getUsedExtruderStacks()
|
||||||
|
for extruder_stack in extruders_stacks:
|
||||||
|
color = ""
|
||||||
|
if extruder_stack.material:
|
||||||
|
color = extruder_stack.material.getMetaDataEntry("color_code")
|
||||||
|
|
||||||
|
self._settings_groups.append(SettingsExportGroup(extruder_stack,
|
||||||
|
"Extruder settings",
|
||||||
|
SettingsExportGroup.Category.Extruder,
|
||||||
|
self._exportSettings(extruder_stack),
|
||||||
|
extruder_index=extruder_stack.position,
|
||||||
|
extruder_color=color))
|
||||||
|
|
||||||
|
# Display per-model settings
|
||||||
|
scene_root = application.getController().getScene().getRoot()
|
||||||
|
for scene_node in scene_root.getChildren():
|
||||||
|
per_model_stack = scene_node.callDecoration("getStack")
|
||||||
|
if per_model_stack is not None:
|
||||||
|
self._settings_groups.append(SettingsExportGroup(per_model_stack,
|
||||||
|
"Model settings",
|
||||||
|
SettingsExportGroup.Category.Model,
|
||||||
|
self._exportSettings(per_model_stack),
|
||||||
|
scene_node.getName()))
|
||||||
|
|
||||||
|
@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()
|
||||||
|
|
||||||
|
settings_export = []
|
||||||
|
|
||||||
|
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:
|
||||||
|
# This is not very good looking, but will do for now
|
||||||
|
value = SettingDefinition.settingValueToString(setting_type, value) + " " + unit
|
||||||
|
else:
|
||||||
|
value = str(value)
|
||||||
|
|
||||||
|
settings_export.append(SettingExport(setting_to_export,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
setting_to_export in SettingsExportModel.EXPORTABLE_SETTINGS))
|
||||||
|
|
||||||
|
return settings_export
|
87
plugins/3MFWriter/SettingsSelectionGroup.qml
Normal file
87
plugins/3MFWriter/SettingsSelectionGroup.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
||||||
|
|
||||||
|
@ -20,10 +23,11 @@ from cura.Utils.Threading import call_on_qt_thread
|
||||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
from cura.Snapshot import Snapshot
|
from cura.Snapshot import Snapshot
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
@ -38,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")
|
||||||
|
|
||||||
|
@ -85,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.
|
||||||
|
@ -127,13 +136,22 @@ 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):
|
if isinstance(um_node, CuraSceneNode):
|
||||||
savitar_node.setSetting("cura:print_order", str(um_node.printOrder))
|
savitar_node.setSetting("cura:print_order", str(um_node.printOrder))
|
||||||
|
@ -146,7 +164,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)
|
||||||
|
|
||||||
|
@ -155,7 +174,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 _addShareLogoToThumbnail(self, primary_image):
|
||||||
|
# Load the icon png image
|
||||||
|
icon_image = QImage(Resources.getPath(Resources.Images, "cura-share.png"))
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
@ -179,6 +215,8 @@ 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._addShareLogoToThumbnail(snapshot)
|
||||||
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")
|
||||||
|
@ -233,14 +271,19 @@ 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)
|
||||||
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)
|
||||||
|
|
||||||
|
@ -396,3 +439,59 @@ 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):
|
||||||
|
preferences = CuraApplication.getInstance().getPreferences()
|
||||||
|
if preferences.getValue("cura/dialog_on_ucp_project_save"):
|
||||||
|
self._config_dialog = UCPDialog()
|
||||||
|
self._config_dialog.show()
|
||||||
|
else:
|
||||||
|
application = CuraApplication.getInstance()
|
||||||
|
workspace_handler = application.getInstance().getWorkspaceFileHandler()
|
||||||
|
|
||||||
|
# Set the model to the workspace writer
|
||||||
|
mesh_writer = workspace_handler.getWriter("3MFWriter")
|
||||||
|
mesh_writer.setExportModel(SettingsExportModel())
|
||||||
|
|
||||||
|
# Open file dialog and write the file
|
||||||
|
device = application.getOutputDeviceManager().getOutputDevice("local_file")
|
||||||
|
nodes = [application.getController().getScene().getRoot()]
|
||||||
|
|
||||||
|
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(
|
||||||
|
catalog.i18nc("@info:error", "Unable to write to file: {0}", file_name),
|
||||||
|
title=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()
|
||||||
|
|
114
plugins/3MFWriter/UCPDialog.py
Normal file
114
plugins/3MFWriter/UCPDialog.py
Normal 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)
|
125
plugins/3MFWriter/UCPDialog.qml
Normal file
125
plugins/3MFWriter/UCPDialog.qml
Normal file
|
@ -0,0 +1,125 @@
|
||||||
|
// 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")
|
||||||
|
property bool dontShowAgain: false
|
||||||
|
|
||||||
|
function storeDontShowAgain()
|
||||||
|
{
|
||||||
|
UM.Preferences.setValue("cura/dialog_on_ucp_project_save", !dontShowAgainCheckbox.checked)
|
||||||
|
UM.Preferences.setValue("cura/asked_dialog_on_ucp_project_save", false)
|
||||||
|
}
|
||||||
|
|
||||||
|
onVisibleChanged:
|
||||||
|
{
|
||||||
|
if(visible && UM.Preferences.getValue("cura/asked_dialog_on_ucp_project_save"))
|
||||||
|
{
|
||||||
|
dontShowAgain = !UM.Preferences.getValue("cura/dialog_on_ucp_project_save")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
leftButtons:
|
||||||
|
[
|
||||||
|
UM.CheckBox
|
||||||
|
{
|
||||||
|
id: dontShowAgainCheckbox
|
||||||
|
text: catalog.i18nc("@action:label", "Don't show project summary on save again")
|
||||||
|
checked: dontShowAgain
|
||||||
|
}
|
||||||
|
]
|
||||||
|
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:
|
||||||
|
{
|
||||||
|
storeDontShowAgain()
|
||||||
|
manager.notifyClosed()
|
||||||
|
}
|
||||||
|
onRejected: storeDontShowAgain()
|
||||||
|
onAccepted: storeDontShowAgain()
|
||||||
|
}
|
|
@ -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:
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
BIN
resources/images/cura-share.png
Normal file
BIN
resources/images/cura-share.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.9 KiB |
|
@ -25,7 +25,7 @@ UM.Dialog
|
||||||
function storeDontShowAgain()
|
function storeDontShowAgain()
|
||||||
{
|
{
|
||||||
UM.Preferences.setValue("cura/dialog_on_project_save", !dontShowAgainCheckbox.checked)
|
UM.Preferences.setValue("cura/dialog_on_project_save", !dontShowAgainCheckbox.checked)
|
||||||
UM.Preferences.setValue("asked_dialog_on_project_save", true)
|
UM.Preferences.setValue("cura/asked_dialog_on_project_save", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
onClosing: storeDontShowAgain()
|
onClosing: storeDontShowAgain()
|
||||||
|
|
|
@ -15,6 +15,7 @@ Item
|
||||||
property int iconSize: UM.Theme.getSize("extruder_icon").width
|
property int iconSize: UM.Theme.getSize("extruder_icon").width
|
||||||
property string iconVariant: "medium"
|
property string iconVariant: "medium"
|
||||||
property alias font: extruderNumberText.font
|
property alias font: extruderNumberText.font
|
||||||
|
property alias text: extruderNumberText.text
|
||||||
|
|
||||||
implicitWidth: iconSize
|
implicitWidth: iconSize
|
||||||
implicitHeight: iconSize
|
implicitHeight: iconSize
|
||||||
|
|
|
@ -47,14 +47,18 @@ Cura.Menu
|
||||||
enabled: UM.WorkspaceFileHandler.enabled && saveProjectMenu.model.count == 1
|
enabled: UM.WorkspaceFileHandler.enabled && saveProjectMenu.model.count == 1
|
||||||
onTriggered:
|
onTriggered:
|
||||||
{
|
{
|
||||||
var args = { "filter_by_machine": false, "file_type": "workspace", "preferred_mimetypes": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml" };
|
if (UM.Preferences.getValue("cura/dialog_on_project_save"))
|
||||||
if(UM.Preferences.getValue("cura/dialog_on_project_save"))
|
|
||||||
{
|
{
|
||||||
saveWorkspaceDialog.args = args
|
saveWorkspaceDialog.args = args
|
||||||
saveWorkspaceDialog.open()
|
saveWorkspaceDialog.open()
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
const args = {
|
||||||
|
"filter_by_machine": false,
|
||||||
|
"file_type": "workspace",
|
||||||
|
"preferred_mimetypes": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
||||||
|
};
|
||||||
UM.OutputDeviceManager.requestWriteToDevice("local_file", PrintInformation.jobName, args)
|
UM.OutputDeviceManager.requestWriteToDevice("local_file", PrintInformation.jobName, args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,6 +74,14 @@ Cura.Menu
|
||||||
enabled: UM.WorkspaceFileHandler.enabled
|
enabled: UM.WorkspaceFileHandler.enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Cura.MenuItem
|
||||||
|
{
|
||||||
|
id: saveUCPMenu
|
||||||
|
text: catalog.i18nc("@title:menu menubar:file", "&Save Universal Cura Project...")
|
||||||
|
enabled: UM.WorkspaceFileHandler.enabled && CuraApplication.getPackageManager().allEnabledPackages.includes("3MFWriter")
|
||||||
|
onTriggered: CuraApplication.exportUcp()
|
||||||
|
}
|
||||||
|
|
||||||
Cura.MenuSeparator { }
|
Cura.MenuSeparator { }
|
||||||
|
|
||||||
Cura.MenuItem
|
Cura.MenuItem
|
||||||
|
@ -78,8 +90,11 @@ Cura.Menu
|
||||||
text: catalog.i18nc("@title:menu menubar:file", "&Export...")
|
text: catalog.i18nc("@title:menu menubar:file", "&Export...")
|
||||||
onTriggered:
|
onTriggered:
|
||||||
{
|
{
|
||||||
var localDeviceId = "local_file"
|
const args = {
|
||||||
UM.OutputDeviceManager.requestWriteToDevice(localDeviceId, PrintInformation.jobName, { "filter_by_machine": false, "preferred_mimetypes": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml"})
|
"filter_by_machine": false,
|
||||||
|
"preferred_mimetypes": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
||||||
|
};
|
||||||
|
UM.OutputDeviceManager.requestWriteToDevice("local_file", PrintInformation.jobName, args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +104,13 @@ Cura.Menu
|
||||||
text: catalog.i18nc("@action:inmenu menubar:file", "Export Selection...")
|
text: catalog.i18nc("@action:inmenu menubar:file", "Export Selection...")
|
||||||
enabled: UM.Selection.hasSelection
|
enabled: UM.Selection.hasSelection
|
||||||
icon.name: "document-save-as"
|
icon.name: "document-save-as"
|
||||||
onTriggered: UM.OutputDeviceManager.requestWriteSelectionToDevice("local_file", PrintInformation.jobName, { "filter_by_machine": false, "preferred_mimetypes": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml"})
|
onTriggered: {
|
||||||
|
const args = {
|
||||||
|
"filter_by_machine": false,
|
||||||
|
"preferred_mimetypes": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
|
||||||
|
};
|
||||||
|
UM.OutputDeviceManager.requestWriteSelectionToDevice("local_file", PrintInformation.jobName, args);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Cura.MenuSeparator { }
|
Cura.MenuSeparator { }
|
||||||
|
|
|
@ -785,6 +785,20 @@ UM.PreferencesPage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UM.TooltipArea
|
||||||
|
{
|
||||||
|
width: childrenRect.width
|
||||||
|
height: childrenRect.height
|
||||||
|
text: catalog.i18nc("@info:tooltip", "Should a summary be shown when saving a UCP project file?")
|
||||||
|
|
||||||
|
UM.CheckBox
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@option:check", "Show summary dialog when saving a UCP project")
|
||||||
|
checked: boolCheck(UM.Preferences.getValue("cura/dialog_on_ucp_project_save"))
|
||||||
|
onCheckedChanged: UM.Preferences.setValue("cura/dialog_on_ucp_project_save", checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
UM.TooltipArea
|
UM.TooltipArea
|
||||||
{
|
{
|
||||||
width: childrenRect.width
|
width: childrenRect.width
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue