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