diff --git a/plugins/PCBWriter/PCBDialog.py b/plugins/PCBWriter/PCBDialog.py index 385b60272e..089fa259ac 100644 --- a/plugins/PCBWriter/PCBDialog.py +++ b/plugins/PCBWriter/PCBDialog.py @@ -1,43 +1,56 @@ # Copyright (c) 2024 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from PyQt6.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication, QUrl, pyqtSlot -from PyQt6.QtGui import QDesktopServices -from typing import List, Optional, Dict, cast +import os -from cura.Machines.Models.MachineListModel import MachineListModel -from cura.Machines.Models.IntentTranslations import intent_translations -from cura.Settings.GlobalStack import GlobalStack -from UM.Application import Application +from PyQt6.QtCore import pyqtSignal, QObject from UM.FlameProfiler import pyqtSlot from UM.i18n import i18nCatalog -from UM.Logger import Logger -from UM.Message import Message -from UM.PluginRegistry import PluginRegistry -from UM.Settings.ContainerRegistry import ContainerRegistry - -import os -import threading -import time from cura.CuraApplication import CuraApplication +from .SettingsExportModel import SettingsExportModel + i18n_catalog = i18nCatalog("cura") class PCBDialog(QObject): - finished = pyqtSignal() + 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, 'PCBDialog.qml') - self._view = CuraApplication.getInstance().createQmlComponent(dialog_path, {"manager": self}) + 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._view.show() + def getModel(self) -> SettingsExportModel: + return self._model + @pyqtSlot() def notifyClosed(self): - self.finished.emit() + self._onFinished() + + @pyqtSlot() + def _onAccepted(self): + self._accepted = True + self._onFinished() + + @pyqtSlot() + def _onRejected(self): + self._onFinished() + + def _onFinished(self): + if not self._finished: # Make sure we don't send the finished signal twice, whatever happens + self._finished = True + self.finished.emit(self._accepted) diff --git a/plugins/PCBWriter/PCBDialog.qml b/plugins/PCBWriter/PCBDialog.qml index ddf99d5e1f..8264e3ee96 100644 --- a/plugins/PCBWriter/PCBDialog.qml +++ b/plugins/PCBWriter/PCBDialog.qml @@ -12,7 +12,7 @@ import PCBWriter 1.0 as PCBWriter UM.Dialog { - id: workspaceDialog + id: exportDialog title: catalog.i18nc("@title:window", "Export pre-configured build batch") margin: UM.Theme.getSize("default_margin").width @@ -23,8 +23,6 @@ UM.Dialog headerComponent: Rectangle { - UM.I18nCatalog { id: catalog; name: "cura" } - height: childrenRect.height + 2 * UM.Theme.getSize("default_margin").height color: UM.Theme.getColor("main_background") @@ -62,7 +60,7 @@ UM.Dialog anchors.fill: parent color: UM.Theme.getColor("main_background") - PCBWriter.SettingsExportModel{ id: settingsExportModel } + UM.I18nCatalog { id: catalog; name: "cura" } ListView { @@ -79,55 +77,19 @@ UM.Dialog } } - footerComponent: Rectangle - { - color: warning ? UM.Theme.getColor("warning") : "transparent" - anchors.bottom: parent.bottom - width: parent.width - height: childrenRect.height + (warning ? 2 * workspaceDialog.margin : workspaceDialog.margin) - - Column + rightButtons: + [ + Cura.TertiaryButton { - height: childrenRect.height - spacing: workspaceDialog.margin - - anchors.leftMargin: workspaceDialog.margin - anchors.rightMargin: workspaceDialog.margin - anchors.bottomMargin: workspaceDialog.margin - anchors.topMargin: warning ? workspaceDialog.margin : 0 - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - - RowLayout - { - id: warningRow - height: childrenRect.height - visible: warning - spacing: workspaceDialog.margin - UM.ColorImage - { - width: UM.Theme.getSize("extruder_icon").width - height: UM.Theme.getSize("extruder_icon").height - source: UM.Theme.getIcon("Warning") - } - - UM.Label - { - id: warningText - text: catalog.i18nc("@label", "This project contains materials or plugins that are currently not installed in Cura.
Install the missing packages and reopen the project.") - } - } - - Loader - { - width: parent.width - height: childrenRect.height - sourceComponent: buttonRow - } + 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 diff --git a/plugins/PCBWriter/PCBWriter.py b/plugins/PCBWriter/PCBWriter.py index 26e552f583..794eac9d4a 100644 --- a/plugins/PCBWriter/PCBWriter.py +++ b/plugins/PCBWriter/PCBWriter.py @@ -1,71 +1,460 @@ # Copyright (c) 2024 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import json +import zipfile +import datetime +import numpy import re - +from dataclasses import asdict +from typing import Optional, cast, List, Dict, Pattern, Set, Union, Mapping, Any from threading import Lock +from io import StringIO # For converting g-code to bytes. -from typing import Optional, cast, List, Dict, Pattern, Set +import pySavitar as Savitar + +from PyQt6.QtCore import QBuffer from UM.Mesh.MeshWriter import MeshWriter -from UM.Math.Vector import Vector from UM.Logger import Logger -from UM.Math.Matrix import Matrix -from UM.Application import Application -from UM.Message import Message -from UM.Resources import Resources from UM.Scene.SceneNode import SceneNode +from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator +from UM.i18n import i18nCatalog +from UM.Settings.InstanceContainer import InstanceContainer +from UM.Settings.SettingFunction import SettingFunction from UM.Settings.ContainerRegistry import ContainerRegistry -from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer -from PyQt6.QtQml import qmlRegisterType +from UM.Math.Matrix import Matrix +from UM.Math.Vector import Vector from cura.CuraApplication import CuraApplication from cura.CuraPackageManager import CuraPackageManager from cura.Settings import CuraContainerStack +from cura.Settings.GlobalStack import GlobalStack from cura.Utils.Threading import call_on_qt_thread from cura.Snapshot import Snapshot -from PyQt6.QtCore import QBuffer - -import pySavitar as Savitar - -import numpy -import datetime - -import zipfile -import UM.Application - from .PCBDialog import PCBDialog from .SettingsExportModel import SettingsExportModel from .SettingsExportGroup import SettingsExportGroup -from UM.i18n import i18nCatalog +MYPY = False +try: + if not MYPY: + import xml.etree.cElementTree as ET +except ImportError: + Logger.log("w", "Unable to load cElementTree, switching to slower version") + import xml.etree.ElementTree as ET + catalog = i18nCatalog("cura") +THUMBNAIL_PATH = "Metadata/thumbnail.png" +MODEL_PATH = "3D/3dmodel.model" +PACKAGE_METADATA_PATH = "Cura/packages.json" +USER_SETTINGS_PATH = "Cura/user-settings.json" + class PCBWriter(MeshWriter): def __init__(self): super().__init__() - - qmlRegisterType(SettingsExportModel, "PCBWriter", 1, 0, "SettingsExportModel") - qmlRegisterType(SettingsExportGroup, "PCBWriter", 1, 0, "SettingsExportGroup") - #qmlRegisterUncreatableType(SettingsExportGroup.Category, "PCBWriter", 1, 0, "SettingsExportGroup.Category") + self._namespaces = { + "3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02", + "content-types": "http://schemas.openxmlformats.org/package/2006/content-types", + "relationships": "http://schemas.openxmlformats.org/package/2006/relationships", + "cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10" + } self._config_dialog = None self._main_thread_lock = Lock() + self._success = False + self._export_model = None def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode) -> bool: + self._success = False + self._export_model = None + self._main_thread_lock.acquire() # Start configuration window in main application thread CuraApplication.getInstance().callLater(self._write, stream, nodes, mode) self._main_thread_lock.acquire() # Block until lock has been released, meaning the config is over self._main_thread_lock.release() - return True + + if self._export_model is not None: + archive = zipfile.ZipFile(stream, "w", compression=zipfile.ZIP_DEFLATED) + try: + model_file = zipfile.ZipInfo(MODEL_PATH) + # Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo. + model_file.compress_type = zipfile.ZIP_DEFLATED + + # Create content types file + content_types_file = zipfile.ZipInfo("[Content_Types].xml") + content_types_file.compress_type = zipfile.ZIP_DEFLATED + content_types = ET.Element("Types", xmlns=self._namespaces["content-types"]) + rels_type = ET.SubElement(content_types, "Default", Extension="rels", + ContentType="application/vnd.openxmlformats-package.relationships+xml") + model_type = ET.SubElement(content_types, "Default", Extension="model", + ContentType="application/vnd.ms-package.3dmanufacturing-3dmodel+xml") + + # Create _rels/.rels file + relations_file = zipfile.ZipInfo("_rels/.rels") + relations_file.compress_type = zipfile.ZIP_DEFLATED + relations_element = ET.Element("Relationships", xmlns=self._namespaces["relationships"]) + model_relation_element = ET.SubElement(relations_element, "Relationship", Target="/" + MODEL_PATH, + Id="rel0", + Type="http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel") + + # Attempt to add a thumbnail + snapshot = self._createSnapshot() + if snapshot: + thumbnail_buffer = QBuffer() + thumbnail_buffer.open(QBuffer.OpenModeFlag.ReadWrite) + snapshot.save(thumbnail_buffer, "PNG") + + thumbnail_file = zipfile.ZipInfo(THUMBNAIL_PATH) + # Don't try to compress snapshot file, because the PNG is pretty much as compact as it will get + archive.writestr(thumbnail_file, thumbnail_buffer.data()) + + # Add PNG to content types file + thumbnail_type = ET.SubElement(content_types, "Default", Extension="png", ContentType="image/png") + # Add thumbnail relation to _rels/.rels file + thumbnail_relation_element = ET.SubElement(relations_element, "Relationship", + Target="/" + THUMBNAIL_PATH, Id="rel1", + Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail") + + # Write material metadata + packages_metadata = self._getMaterialPackageMetadata() + self._getPluginPackageMetadata() + self._storeMetadataJson({"packages": packages_metadata}, archive, PACKAGE_METADATA_PATH) + + # Write user settings data + user_settings_data = self._getUserSettings(self._export_model) + self._storeMetadataJson(user_settings_data, archive, USER_SETTINGS_PATH) + + savitar_scene = Savitar.Scene() + + scene_metadata = CuraApplication.getInstance().getController().getScene().getMetaData() + + for key, value in scene_metadata.items(): + savitar_scene.setMetaDataEntry(key, value) + + current_time_string = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + if "Application" not in scene_metadata: + # This might sound a bit strange, but this field should store the original application that created + # the 3mf. So if it was already set, leave it to whatever it was. + savitar_scene.setMetaDataEntry("Application", + CuraApplication.getInstance().getApplicationDisplayName()) + if "CreationDate" not in scene_metadata: + savitar_scene.setMetaDataEntry("CreationDate", current_time_string) + + savitar_scene.setMetaDataEntry("ModificationDate", current_time_string) + + transformation_matrix = Matrix() + transformation_matrix._data[1, 1] = 0 + transformation_matrix._data[1, 2] = -1 + transformation_matrix._data[2, 1] = 1 + transformation_matrix._data[2, 2] = 0 + + global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() + # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the + # build volume. + if global_container_stack: + translation_vector = Vector(x=global_container_stack.getProperty("machine_width", "value") / 2, + y=global_container_stack.getProperty("machine_depth", "value") / 2, + z=0) + translation_matrix = Matrix() + translation_matrix.setByTranslation(translation_vector) + transformation_matrix.preMultiply(translation_matrix) + + root_node = CuraApplication.getInstance().getController().getScene().getRoot() + exported_model_settings = PCBWriter._extractModelExportedSettings(self._export_model) + for node in nodes: + if node == root_node: + for root_child in node.getChildren(): + savitar_node = PCBWriter._convertUMNodeToSavitarNode(root_child, + transformation_matrix, + exported_model_settings) + if savitar_node: + savitar_scene.addSceneNode(savitar_node) + else: + savitar_node = self._convertUMNodeToSavitarNode(node, + transformation_matrix, + exported_model_settings) + if savitar_node: + savitar_scene.addSceneNode(savitar_node) + + parser = Savitar.ThreeMFParser() + scene_string = parser.sceneToString(savitar_scene) + + archive.writestr(model_file, scene_string) + archive.writestr(content_types_file, + b' \n' + ET.tostring(content_types)) + archive.writestr(relations_file, + b' \n' + ET.tostring(relations_element)) + except Exception as error: + Logger.logException("e", "Error writing zip file") + self.setInformation(str(error)) + return False + finally: + archive.close() + + return True + else: + return False def _write(self, stream, nodes, mode): self._config_dialog = PCBDialog() - self._config_dialog.finished.connect(self._onDialogClosed) + self._config_dialog.finished.connect(self._onDialogFinished) self._config_dialog.show() - def _onDialogClosed(self): + def _onDialogFinished(self, accepted: bool): + if accepted: + self._export_model = self._config_dialog.getModel() + self._main_thread_lock.release() + + @staticmethod + def _extractModelExportedSettings(model: SettingsExportModel) -> Mapping[str, Set[str]]: + extra_settings = {} + + 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 + + @staticmethod + def _convertUMNodeToSavitarNode(um_node, + transformation: Matrix = Matrix(), + exported_settings: Mapping[str, Set[str]] = None): + """Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode + + :returns: Uranium Scene node. + """ + if not isinstance(um_node, SceneNode): + return None + + active_build_plate_nr = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate + if um_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr: + return + + savitar_node = Savitar.SceneNode() + savitar_node.setName(um_node.getName()) + + node_matrix = Matrix() + mesh_data = um_node.getMeshData() + # compensate for original center position, if object(s) is/are not around its zero position + if mesh_data is not None: + extents = mesh_data.getExtents() + if extents is not None: + # We use a different coordinate space while writing, so flip Z and Y + center_vector = Vector(extents.center.x, extents.center.z, extents.center.y) + node_matrix.setByTranslation(center_vector) + node_matrix.multiply(um_node.getLocalTransformation()) + + matrix_string = PCBWriter._convertMatrixToString(node_matrix.preMultiply(transformation)) + + savitar_node.setTransformation(matrix_string) + if mesh_data is not None: + savitar_node.getMeshData().setVerticesFromBytes(mesh_data.getVerticesAsByteArray()) + indices_array = mesh_data.getIndicesAsByteArray() + if indices_array is not None: + savitar_node.getMeshData().setFacesFromBytes(indices_array) + else: + savitar_node.getMeshData().setFacesFromBytes( + numpy.arange(mesh_data.getVertices().size / 3, dtype=numpy.int32).tostring()) + + # Handle per object settings (if any) + stack = um_node.callDecoration("getStack") + if stack is not None: + 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"))) + + # Store the metadata. + for key, value in um_node.metadata.items(): + savitar_node.setSetting(key, value) + + for child_node in um_node.getChildren(): + # only save the nodes on the active build plate + if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr: + continue + savitar_child_node = PCBWriter._convertUMNodeToSavitarNode(child_node, + exported_settings = exported_settings) + if savitar_child_node is not None: + savitar_node.addChild(savitar_child_node) + + return savitar_node + + @call_on_qt_thread # must be called from the main thread because of OpenGL + def _createSnapshot(self): + Logger.log("d", "Creating thumbnail image...") + if not CuraApplication.getInstance().isVisible: + Logger.log("w", "Can't create snapshot when renderer not initialized.") + return None + try: + snapshot = Snapshot.snapshot(width=300, height=300) + except: + Logger.logException("w", "Failed to create snapshot image") + return None + + return snapshot + + @staticmethod + def _storeMetadataJson(metadata: Union[Dict[str, List[Dict[str, str]]], Dict[str, Dict[str, Any]]], + archive: zipfile.ZipFile, path + : str) -> None: + """Stores metadata inside archive path as json file""" + metadata_file = zipfile.ZipInfo(path) + # We have to set the compress type of each file as well (it doesn't keep the type of the entire archive) + metadata_file.compress_type = zipfile.ZIP_DEFLATED + archive.writestr(metadata_file, + json.dumps(metadata, separators=(", ", ": "), indent=4, skipkeys=True, ensure_ascii=False)) + + @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 + + @staticmethod + def _getPluginPackageMetadata() -> List[Dict[str, str]]: + """Get metadata for all backend plugins that are used in the project. + + :return: List of material metadata dictionaries. + """ + + backend_plugin_enum_value_regex = re.compile( + r"PLUGIN::(?P\w+)@(?P\d+.\d+.\d+)::(?P\w+)") + # This regex parses enum values to find if they contain custom + # backend engine values. These custom enum values are in the format + # PLUGIN::@:: + # where + # - plugin_id is the id of the plugin + # - version is in the semver format + # - value is the value of the enum + + plugin_ids = set() + + def addPluginIdsInStack(stack: CuraContainerStack) -> None: + for key in stack.getAllKeys(): + value = str(stack.getProperty(key, "value")) + for plugin_id, _version, _value in backend_plugin_enum_value_regex.findall(value): + plugin_ids.add(plugin_id) + + # Go through all stacks and find all the plugin id contained in the project + global_stack = CuraApplication.getInstance().getMachineManager().activeMachine + addPluginIdsInStack(global_stack) + + for container in global_stack.getContainers(): + addPluginIdsInStack(container) + + for extruder_stack in global_stack.extruderList: + addPluginIdsInStack(extruder_stack) + + for container in extruder_stack.getContainers(): + addPluginIdsInStack(container) + + metadata = {} + + package_manager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager()) + for plugin_id in plugin_ids: + package_data = package_manager.getInstalledPackageInfo(plugin_id) + + metadata[plugin_id] = { + "id": plugin_id, + "display_name": package_data.get("display_name") if package_data.get("display_name") else "", + "package_version": package_data.get("package_version") if package_data.get("package_version") else "", + "sdk_version_semver": package_data.get("sdk_version_semver") if package_data.get( + "sdk_version_semver") else "", + "type": "plugin", + } + + # Storing in a dict and fetching values to avoid duplicates + return list(metadata.values()) + + @staticmethod + def _getMaterialPackageMetadata() -> List[Dict[str, str]]: + """Get metadata for installed materials in active extruder stack, this does not include bundled materials. + + :return: List of material metadata dictionaries. + """ + metadata = {} + + package_manager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager()) + + for extruder in CuraApplication.getInstance().getExtruderManager().getActiveExtruderStacks(): + if not extruder.isEnabled: + # Don't export materials not in use + continue + + if isinstance(extruder.material, type(ContainerRegistry.getInstance().getEmptyInstanceContainer())): + # This is an empty material container, no material to export + continue + + if package_manager.isMaterialBundled(extruder.material.getFileName(), + extruder.material.getMetaDataEntry("GUID")): + # Don't export bundled materials + continue + + package_id = package_manager.getMaterialFilePackageId(extruder.material.getFileName(), + extruder.material.getMetaDataEntry("GUID")) + package_data = package_manager.getInstalledPackageInfo(package_id) + + # We failed to find the package for this material + if not package_data: + Logger.info(f"Could not find package for material in extruder {extruder.id}, skipping.") + continue + + material_metadata = { + "id": package_id, + "display_name": package_data.get("display_name") if package_data.get("display_name") else "", + "package_version": package_data.get("package_version") if package_data.get("package_version") else "", + "sdk_version_semver": package_data.get("sdk_version_semver") if package_data.get( + "sdk_version_semver") else "", + "type": "material", + } + + metadata[package_id] = material_metadata + + # Storing in a dict and fetching values to avoid duplicates + return list(metadata.values()) + + @staticmethod + def _convertMatrixToString(matrix): + result = "" + result += str(matrix._data[0, 0]) + " " + result += str(matrix._data[1, 0]) + " " + result += str(matrix._data[2, 0]) + " " + result += str(matrix._data[0, 1]) + " " + result += str(matrix._data[1, 1]) + " " + result += str(matrix._data[2, 1]) + " " + result += str(matrix._data[0, 2]) + " " + result += str(matrix._data[1, 2]) + " " + result += str(matrix._data[2, 2]) + " " + result += str(matrix._data[0, 3]) + " " + result += str(matrix._data[1, 3]) + " " + result += str(matrix._data[2, 3]) + return result \ No newline at end of file diff --git a/plugins/PCBWriter/SettingExport.py b/plugins/PCBWriter/SettingExport.py index 0a761bb6b9..73dd818897 100644 --- a/plugins/PCBWriter/SettingExport.py +++ b/plugins/PCBWriter/SettingExport.py @@ -1,15 +1,17 @@ # Copyright (c) 2024 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from PyQt6.QtCore import QObject, pyqtProperty +from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal -class SettingsExport(QObject): +class SettingExport(QObject): - def __init__(self, name, value): + def __init__(self, id, name, value): super().__init__() + self.id = id self._name = name self._value = value + self._selected = True @pyqtProperty(str, constant=True) def name(self): @@ -18,3 +20,14 @@ class SettingsExport(QObject): @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 diff --git a/plugins/PCBWriter/SettingSelection.qml b/plugins/PCBWriter/SettingSelection.qml index 9b09593a7d..6439542f3f 100644 --- a/plugins/PCBWriter/SettingSelection.qml +++ b/plugins/PCBWriter/SettingSelection.qml @@ -17,7 +17,8 @@ RowLayout { text: modelData.name Layout.preferredWidth: UM.Theme.getSize("setting").width - checked: true + checked: modelData.selected + onClicked: modelData.selected = checked } UM.Label diff --git a/plugins/PCBWriter/SettingsExportGroup.py b/plugins/PCBWriter/SettingsExportGroup.py index 2b21c42b78..cc3fc7b4f5 100644 --- a/plugins/PCBWriter/SettingsExportGroup.py +++ b/plugins/PCBWriter/SettingsExportGroup.py @@ -14,8 +14,9 @@ class SettingsExportGroup(QObject): Extruder = 1 Model = 2 - def __init__(self, name, category, settings, category_details = '', extruder_index = 0, extruder_color = ''): + 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 diff --git a/plugins/PCBWriter/SettingsExportModel.py b/plugins/PCBWriter/SettingsExportModel.py index b09717298a..992adeb089 100644 --- a/plugins/PCBWriter/SettingsExportModel.py +++ b/plugins/PCBWriter/SettingsExportModel.py @@ -1,13 +1,21 @@ # 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 .SettingsExportGroup import SettingsExportGroup -from .SettingExport import SettingsExport -from cura.CuraApplication import CuraApplication 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): @@ -61,7 +69,8 @@ class SettingsExportModel(QObject): # Display global settings global_stack = application.getGlobalContainerStack() - self._settings_groups.append(SettingsExportGroup("Global settings", + self._settings_groups.append(SettingsExportGroup(global_stack, + "Global settings", SettingsExportGroup.Category.Global, self._exportSettings(global_stack))) @@ -72,7 +81,8 @@ class SettingsExportModel(QObject): if extruder_stack.material: color = extruder_stack.material.getMetaDataEntry("color_code") - self._settings_groups.append(SettingsExportGroup("Extruder settings", + self._settings_groups.append(SettingsExportGroup(extruder_stack, + "Extruder settings", SettingsExportGroup.Category.Extruder, self._exportSettings(extruder_stack), extruder_index=extruder_stack.position, @@ -83,13 +93,14 @@ class SettingsExportModel(QObject): 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("Model settings", + 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): + def settingsGroups(self) -> List[SettingsExportGroup]: return self._settings_groups @staticmethod @@ -110,6 +121,6 @@ class SettingsExportModel(QObject): else: value = str(value) - settings_export.append(SettingsExport(label, value)) + settings_export.append(SettingExport(setting_to_export, label, value)) - return settings_export \ No newline at end of file + return settings_export diff --git a/plugins/PCBWriter/__init__.py b/plugins/PCBWriter/__init__.py index 3ec2eba95f..da4205a7d7 100644 --- a/plugins/PCBWriter/__init__.py +++ b/plugins/PCBWriter/__init__.py @@ -2,9 +2,14 @@ # Cura is released under the terms of the LGPLv3 or higher. import sys -from . import PCBWriter +from PyQt6.QtQml import qmlRegisterType + from UM.i18n import i18nCatalog +from . import PCBWriter +from .SettingsExportModel import SettingsExportModel +from .SettingsExportGroup import SettingsExportGroup + i18n_catalog = i18nCatalog("cura") def getMetaData(): @@ -12,10 +17,13 @@ def getMetaData(): "output": [{ "extension": "pcb", "description": i18n_catalog.i18nc("@item:inlistbox", "Pre-Configured Batch file"), - "mime_type": "application/vnd.um.preconfigured-batch+3mf", + "mime_type": "application/x-pcb", "mode": PCBWriter.PCBWriter.OutputMode.BinaryMode }] }} def register(app): + qmlRegisterType(SettingsExportModel, "PCBWriter", 1, 0, "SettingsExportModel") + qmlRegisterType(SettingsExportGroup, "PCBWriter", 1, 0, "SettingsExportGroup") + return {"mesh_writer": PCBWriter.PCBWriter() }