From 54af5bca3c5e9b5700ce106f854f97e3ed102842 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Thu, 25 Jan 2024 13:30:56 +0100 Subject: [PATCH 01/39] Add export sub-menu CURA-11561 --- resources/qml/Menus/ExportMenu.qml | 44 ++++++++++++++++++++++++++++++ resources/qml/Menus/FileMenu.qml | 26 +++++++++--------- 2 files changed, 57 insertions(+), 13 deletions(-) create mode 100644 resources/qml/Menus/ExportMenu.qml diff --git a/resources/qml/Menus/ExportMenu.qml b/resources/qml/Menus/ExportMenu.qml new file mode 100644 index 0000000000..70560f0772 --- /dev/null +++ b/resources/qml/Menus/ExportMenu.qml @@ -0,0 +1,44 @@ +// Copyright (c) 2024 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.2 +import QtQuick.Controls 2.1 + +import UM 1.5 as UM +import Cura 1.1 as Cura + +import "../Dialogs" + +Cura.Menu +{ + id: exportMenu + property alias model: meshWriters.model + property bool selectionOnly: false + + Instantiator + { + id: meshWriters + Cura.MenuItem + { + text: model.description + onTriggered: + { + var localDeviceId = "local_file" + var file_name = PrintInformation.jobName + var args = { "filter_by_machine": false, "limit_mimetypes": model.mime_type} + if(exportMenu.selectionOnly) + { + UM.OutputDeviceManager.requestWriteSelectionToDevice(localDeviceId, file_name, args) + } + else + { + UM.OutputDeviceManager.requestWriteToDevice(localDeviceId, file_name, args) + } + } + shortcut: model.shortcut + enabled: exportMenu.shouldBeVisible + } + onObjectAdded: function(index, object) { exportMenu.insertItem(index, object)} + onObjectRemoved: function(index, object) { exportMenu.removeItem(object)} + } +} diff --git a/resources/qml/Menus/FileMenu.qml b/resources/qml/Menus/FileMenu.qml index 0884053ef3..254c0d5468 100644 --- a/resources/qml/Menus/FileMenu.qml +++ b/resources/qml/Menus/FileMenu.qml @@ -4,7 +4,7 @@ import QtQuick 2.2 import QtQuick.Controls 2.1 -import UM 1.6 as UM +import UM 1.7 as UM import Cura 1.0 as Cura Cura.Menu @@ -72,24 +72,24 @@ Cura.Menu Cura.MenuSeparator { } - Cura.MenuItem + UM.MeshWritersModel { id: meshWritersModel } + + ExportMenu { - id: saveAsMenu - text: catalog.i18nc("@title:menu menubar:file", "&Export...") - onTriggered: - { - var localDeviceId = "local_file" - UM.OutputDeviceManager.requestWriteToDevice(localDeviceId, PrintInformation.jobName, { "filter_by_machine": false, "preferred_mimetypes": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml"}) - } + id: exportMenu + title: catalog.i18nc("@title:menu menubar:file", "&Export...") + model: meshWritersModel + shouldBeVisible: model.count > 0 } - Cura.MenuItem + ExportMenu { id: exportSelectionMenu - text: catalog.i18nc("@action:inmenu menubar:file", "Export Selection...") + title: catalog.i18nc("@action:inmenu menubar:file", "Export Selection...") + model: meshWritersModel + shouldBeVisible: model.count > 0 enabled: UM.Selection.hasSelection - 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"}) + selectionOnly: true } Cura.MenuSeparator { } From edd5cee41a770bc844a262a3079e1f3618c0b228 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Thu, 25 Jan 2024 14:04:47 +0100 Subject: [PATCH 02/39] Fix exporting binary/ascii STL CURA-11561 --- resources/qml/Menus/ExportMenu.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/Menus/ExportMenu.qml b/resources/qml/Menus/ExportMenu.qml index 70560f0772..5c08b04f0f 100644 --- a/resources/qml/Menus/ExportMenu.qml +++ b/resources/qml/Menus/ExportMenu.qml @@ -25,7 +25,7 @@ Cura.Menu { var localDeviceId = "local_file" var file_name = PrintInformation.jobName - var args = { "filter_by_machine": false, "limit_mimetypes": model.mime_type} + var args = { "filter_by_machine": false, "limit_mimetypes": [model.mime_type], "limit_modes": [model.mode]} if(exportMenu.selectionOnly) { UM.OutputDeviceManager.requestWriteSelectionToDevice(localDeviceId, file_name, args) From b5b0575baf505d4902d4aa1b72c940485914419a Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Fri, 26 Jan 2024 14:01:26 +0100 Subject: [PATCH 03/39] Display (uncomplete) configuration UI to export PCB file CURA-11561 --- plugins/PCBWriter/PCBDialog.py | 37 +++++ plugins/PCBWriter/PCBDialog.qml | 148 +++++++++++++++++++ plugins/PCBWriter/PCBWriter.py | 67 +++++++++ plugins/PCBWriter/SettingExport.py | 24 +++ plugins/PCBWriter/SettingsExportGroup.py | 59 ++++++++ plugins/PCBWriter/SettingsExportModel.py | 31 ++++ plugins/PCBWriter/SettingsSelectionGroup.qml | 70 +++++++++ plugins/PCBWriter/__init__.py | 21 +++ plugins/PCBWriter/plugin.json | 8 + resources/qml/ExtruderIcon.qml | 1 + 10 files changed, 466 insertions(+) create mode 100644 plugins/PCBWriter/PCBDialog.py create mode 100644 plugins/PCBWriter/PCBDialog.qml create mode 100644 plugins/PCBWriter/PCBWriter.py create mode 100644 plugins/PCBWriter/SettingExport.py create mode 100644 plugins/PCBWriter/SettingsExportGroup.py create mode 100644 plugins/PCBWriter/SettingsExportModel.py create mode 100644 plugins/PCBWriter/SettingsSelectionGroup.qml create mode 100644 plugins/PCBWriter/__init__.py create mode 100644 plugins/PCBWriter/plugin.json diff --git a/plugins/PCBWriter/PCBDialog.py b/plugins/PCBWriter/PCBDialog.py new file mode 100644 index 0000000000..f31c87a61b --- /dev/null +++ b/plugins/PCBWriter/PCBDialog.py @@ -0,0 +1,37 @@ +# 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 +from PyQt6.QtGui import QDesktopServices +from typing import List, Optional, Dict, cast + +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 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 + +i18n_catalog = i18nCatalog("cura") + + +class PCBDialog(QObject): + 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}) + + def show(self) -> None: + self._view.show() diff --git a/plugins/PCBWriter/PCBDialog.qml b/plugins/PCBWriter/PCBDialog.qml new file mode 100644 index 0000000000..1937c00828 --- /dev/null +++ b/plugins/PCBWriter/PCBDialog.qml @@ -0,0 +1,148 @@ +// 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 PCBWriter 1.0 as PCBWriter + +UM.Dialog +{ + id: workspaceDialog + title: catalog.i18nc("@title:window", "Export pre-configured build batch") + + margin: UM.Theme.getSize("default_margin").width + minimumWidth: UM.Theme.getSize("modal_window_minimum").width + minimumHeight: UM.Theme.getSize("modal_window_minimum").height + + backgroundColor: UM.Theme.getColor("detail_background") + + headerComponent: Rectangle + { + UM.I18nCatalog { id: catalog; name: "cura" } + + 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 - Pre-configured build batch") + font: UM.Theme.getFont("large") + } + + UM.Label + { + id: descriptionLabel + text: catalog.i18nc("@action:description", "When exporting a build batch, 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") + + PCBWriter.SettingsExportModel{ id: settingsExportModel } + + ListView + { + id: settingsExportList + anchors.fill: parent + anchors.margins: UM.Theme.getSize("default_margin").width + spacing: UM.Theme.getSize("default_margin").height + model: settingsExportModel.settingsGroups + clip: true + + ScrollBar.vertical: UM.ScrollBar { id: verticalScrollBar } + + delegate: SettingsSelectionGroup { Layout.margins: 0 } + } + + // Flickable + // { + // Column + // { + // width: parent.width - scrollbar.width - UM.Theme.getSize("default_margin").width + // height: childrenRect.height + // + // spacing: UM.Theme.getSize("default_margin").height + // leftPadding: UM.Theme.getSize("default_margin").width + // rightPadding: leftPadding + // topPadding: UM.Theme.getSize("default_margin").height + // bottomPadding: topPadding + // } + // } + } + + 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 + { + 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 + } + } + } + + buttonSpacing: UM.Theme.getSize("wide_margin").width +} diff --git a/plugins/PCBWriter/PCBWriter.py b/plugins/PCBWriter/PCBWriter.py new file mode 100644 index 0000000000..6391493ae3 --- /dev/null +++ b/plugins/PCBWriter/PCBWriter.py @@ -0,0 +1,67 @@ +# Copyright (c) 2024 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +import json +import re + +from threading import Lock + +from typing import Optional, cast, List, Dict, Pattern, Set + +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.Settings.ContainerRegistry import ContainerRegistry +from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer +from PyQt6.QtQml import qmlRegisterType + +from cura.CuraApplication import CuraApplication +from cura.CuraPackageManager import CuraPackageManager +from cura.Settings import CuraContainerStack +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 +catalog = i18nCatalog("cura") + +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._config_dialog = None + self._main_thread_lock = Lock() + + def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode) -> bool: + 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 + + def _write(self, stream, nodes, mode): + self._config_dialog = PCBDialog() + self._config_dialog.show() diff --git a/plugins/PCBWriter/SettingExport.py b/plugins/PCBWriter/SettingExport.py new file mode 100644 index 0000000000..901dcdc804 --- /dev/null +++ b/plugins/PCBWriter/SettingExport.py @@ -0,0 +1,24 @@ +# Copyright (c) 2024 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from PyQt6.QtCore import Qt, QObject, pyqtProperty +from UM.FlameProfiler import pyqtSlot +from UM.Application import Application +from UM.Qt.ListModel import ListModel +from UM.Logger import Logger + + +class SettingsExport(): + + def __init__(self): + super().__init__() + self._name = "Generate Support" + self._value = "Enabled" + + @pyqtProperty(str, constant=True) + def name(self): + return self._name + + @pyqtProperty(str, constant=True) + def value(self): + return self._value diff --git a/plugins/PCBWriter/SettingsExportGroup.py b/plugins/PCBWriter/SettingsExportGroup.py new file mode 100644 index 0000000000..355b2347f6 --- /dev/null +++ b/plugins/PCBWriter/SettingsExportGroup.py @@ -0,0 +1,59 @@ +# 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 Qt, QObject, pyqtProperty, pyqtEnum +from UM.FlameProfiler import pyqtSlot +from UM.Application import Application +from UM.Qt.ListModel import ListModel +from UM.Logger import Logger + +from .SettingExport import SettingsExport + + +class SettingsExportGroup(QObject): + + @pyqtEnum + class Category(IntEnum): + Global = 0 + Extruder = 1 + Model = 2 + + def __init__(self, name, category, category_details = '', extruder_index = 0, extruder_color = ''): + super().__init__() + self._name = name + self._settings = [] + self._category = category + self._category_details = category_details + self._extruder_index = extruder_index + self._extruder_color = extruder_color + self._updateSettings() + + @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 + + def _updateSettings(self): + self._settings.append(SettingsExport()) + self._settings.append(SettingsExport()) \ No newline at end of file diff --git a/plugins/PCBWriter/SettingsExportModel.py b/plugins/PCBWriter/SettingsExportModel.py new file mode 100644 index 0000000000..3351e22b59 --- /dev/null +++ b/plugins/PCBWriter/SettingsExportModel.py @@ -0,0 +1,31 @@ +# Copyright (c) 2024 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from PyQt6.QtCore import QObject, Qt, pyqtProperty +from UM.FlameProfiler import pyqtSlot +from UM.Application import Application +from UM.Qt.ListModel import ListModel +from UM.Logger import Logger + +from .SettingsExportGroup import SettingsExportGroup + + +class SettingsExportModel(QObject): + + def __init__(self, parent = None): + super().__init__(parent) + self._settingsGroups = [] + self._updateSettingsExportGroups() + + @pyqtProperty(list, constant=True) + def settingsGroups(self): + return self._settingsGroups + + def _updateSettingsExportGroups(self): + self._settingsGroups.append(SettingsExportGroup("Global settings", SettingsExportGroup.Category.Global)) + self._settingsGroups.append(SettingsExportGroup("Extruder settings", SettingsExportGroup.Category.Extruder, extruder_index=1, extruder_color='#ff0000')) + self._settingsGroups.append(SettingsExportGroup("Extruder settings", SettingsExportGroup.Category.Extruder, extruder_index=8, extruder_color='#008fff')) + self._settingsGroups.append(SettingsExportGroup("Model settings", + SettingsExportGroup.Category.Model, 'hypercube.stl')) + self._settingsGroups.append(SettingsExportGroup("Model settings", + SettingsExportGroup.Category.Model, 'homer-simpson.stl')) diff --git a/plugins/PCBWriter/SettingsSelectionGroup.qml b/plugins/PCBWriter/SettingsSelectionGroup.qml new file mode 100644 index 0000000000..12829c96d4 --- /dev/null +++ b/plugins/PCBWriter/SettingsSelectionGroup.qml @@ -0,0 +1,70 @@ +// 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 PCBWriter 1.0 as PCBWriter + +Column +{ + id: settingsGroup + + UM.I18nCatalog { id: catalog; name: "cura" } + + Row + { + id: settingsGroupTitleRow + spacing: UM.Theme.getSize("default_margin").width + + Item + { + id: icon + anchors.verticalCenter: parent.verticalCenter + height: UM.Theme.getSize("medium_button_icon").height + width: height + + UM.ColorImage + { + id: settingsMainImage + anchors.fill: parent + source: + { + switch(modelData.category) + { + case PCBWriter.SettingsExportGroup.Global: + return UM.Theme.getIcon("Sliders") + case PCBWriter.SettingsExportGroup.Model: + return UM.Theme.getIcon("View3D") + default: + return "" + } + } + + color: UM.Theme.getColor("text") + } + + Cura.ExtruderIcon + { + id: settingsExtruderIcon + anchors.fill: parent + visible: modelData.category === PCBWriter.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) : '') + anchors.verticalCenter: parent.verticalCenter + font: UM.Theme.getFont("default_bold") + } + } +} diff --git a/plugins/PCBWriter/__init__.py b/plugins/PCBWriter/__init__.py new file mode 100644 index 0000000000..3ec2eba95f --- /dev/null +++ b/plugins/PCBWriter/__init__.py @@ -0,0 +1,21 @@ +# Copyright (c) 2024 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +import sys + +from . import PCBWriter +from UM.i18n import i18nCatalog + +i18n_catalog = i18nCatalog("cura") + +def getMetaData(): + return {"mesh_writer": { + "output": [{ + "extension": "pcb", + "description": i18n_catalog.i18nc("@item:inlistbox", "Pre-Configured Batch file"), + "mime_type": "application/vnd.um.preconfigured-batch+3mf", + "mode": PCBWriter.PCBWriter.OutputMode.BinaryMode + }] + }} + +def register(app): + return {"mesh_writer": PCBWriter.PCBWriter() } diff --git a/plugins/PCBWriter/plugin.json b/plugins/PCBWriter/plugin.json new file mode 100644 index 0000000000..6571185779 --- /dev/null +++ b/plugins/PCBWriter/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "Pre-Configured Batch Writer", + "author": "Ultimaker B.V.", + "version": "1.0.0", + "description": "Provides support for writing Pre-Configured Batch files.", + "api": 8, + "i18n-catalog": "cura" +} diff --git a/resources/qml/ExtruderIcon.qml b/resources/qml/ExtruderIcon.qml index 3231d924ee..bd15df7848 100644 --- a/resources/qml/ExtruderIcon.qml +++ b/resources/qml/ExtruderIcon.qml @@ -15,6 +15,7 @@ Item property int iconSize: UM.Theme.getSize("extruder_icon").width property string iconVariant: "medium" property alias font: extruderNumberText.font + property alias text: extruderNumberText.text implicitWidth: iconSize implicitHeight: iconSize From 2aef33f52143f210be44f4f4a84712c68ee1d1b1 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 30 Jan 2024 14:35:55 +0100 Subject: [PATCH 04/39] Completed configuration UI to export PCB file CURA-11561 --- plugins/PCBWriter/PCBDialog.qml | 17 +----------- plugins/PCBWriter/SettingExport.py | 8 +++--- plugins/PCBWriter/SettingSelection.qml | 27 ++++++++++++++++++ plugins/PCBWriter/SettingsExportGroup.py | 9 ++---- plugins/PCBWriter/SettingsExportModel.py | 28 +++++++++++++++---- plugins/PCBWriter/SettingsSelectionGroup.qml | 29 ++++++++++++++++---- 6 files changed, 80 insertions(+), 38 deletions(-) create mode 100644 plugins/PCBWriter/SettingSelection.qml diff --git a/plugins/PCBWriter/PCBDialog.qml b/plugins/PCBWriter/PCBDialog.qml index 1937c00828..213bed108c 100644 --- a/plugins/PCBWriter/PCBDialog.qml +++ b/plugins/PCBWriter/PCBDialog.qml @@ -69,7 +69,7 @@ UM.Dialog id: settingsExportList anchors.fill: parent anchors.margins: UM.Theme.getSize("default_margin").width - spacing: UM.Theme.getSize("default_margin").height + spacing: UM.Theme.getSize("thick_margin").height model: settingsExportModel.settingsGroups clip: true @@ -77,21 +77,6 @@ UM.Dialog delegate: SettingsSelectionGroup { Layout.margins: 0 } } - - // Flickable - // { - // Column - // { - // width: parent.width - scrollbar.width - UM.Theme.getSize("default_margin").width - // height: childrenRect.height - // - // spacing: UM.Theme.getSize("default_margin").height - // leftPadding: UM.Theme.getSize("default_margin").width - // rightPadding: leftPadding - // topPadding: UM.Theme.getSize("default_margin").height - // bottomPadding: topPadding - // } - // } } footerComponent: Rectangle diff --git a/plugins/PCBWriter/SettingExport.py b/plugins/PCBWriter/SettingExport.py index 901dcdc804..2c230f9ab3 100644 --- a/plugins/PCBWriter/SettingExport.py +++ b/plugins/PCBWriter/SettingExport.py @@ -8,12 +8,12 @@ from UM.Qt.ListModel import ListModel from UM.Logger import Logger -class SettingsExport(): +class SettingsExport(QObject): - def __init__(self): + def __init__(self, name, value): super().__init__() - self._name = "Generate Support" - self._value = "Enabled" + self._name = name + self._value = value @pyqtProperty(str, constant=True) def name(self): diff --git a/plugins/PCBWriter/SettingSelection.qml b/plugins/PCBWriter/SettingSelection.qml new file mode 100644 index 0000000000..9b09593a7d --- /dev/null +++ b/plugins/PCBWriter/SettingSelection.qml @@ -0,0 +1,27 @@ +// 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 + +RowLayout +{ + id: settingSelection + + UM.CheckBox + { + text: modelData.name + Layout.preferredWidth: UM.Theme.getSize("setting").width + checked: true + } + + UM.Label + { + text: modelData.value + } +} diff --git a/plugins/PCBWriter/SettingsExportGroup.py b/plugins/PCBWriter/SettingsExportGroup.py index 355b2347f6..0a721a7fb8 100644 --- a/plugins/PCBWriter/SettingsExportGroup.py +++ b/plugins/PCBWriter/SettingsExportGroup.py @@ -20,15 +20,14 @@ class SettingsExportGroup(QObject): Extruder = 1 Model = 2 - def __init__(self, name, category, category_details = '', extruder_index = 0, extruder_color = ''): + def __init__(self, name, category, settings, category_details = '', extruder_index = 0, extruder_color = ''): super().__init__() self._name = name - self._settings = [] + self._settings = settings self._category = category self._category_details = category_details self._extruder_index = extruder_index self._extruder_color = extruder_color - self._updateSettings() @pyqtProperty(str, constant=True) def name(self): @@ -53,7 +52,3 @@ class SettingsExportGroup(QObject): @pyqtProperty(str, constant=True) def extruder_color(self): return self._extruder_color - - def _updateSettings(self): - self._settings.append(SettingsExport()) - self._settings.append(SettingsExport()) \ No newline at end of file diff --git a/plugins/PCBWriter/SettingsExportModel.py b/plugins/PCBWriter/SettingsExportModel.py index 3351e22b59..0f1ec5113a 100644 --- a/plugins/PCBWriter/SettingsExportModel.py +++ b/plugins/PCBWriter/SettingsExportModel.py @@ -8,6 +8,7 @@ from UM.Qt.ListModel import ListModel from UM.Logger import Logger from .SettingsExportGroup import SettingsExportGroup +from .SettingExport import SettingsExport class SettingsExportModel(QObject): @@ -22,10 +23,27 @@ class SettingsExportModel(QObject): return self._settingsGroups def _updateSettingsExportGroups(self): - self._settingsGroups.append(SettingsExportGroup("Global settings", SettingsExportGroup.Category.Global)) - self._settingsGroups.append(SettingsExportGroup("Extruder settings", SettingsExportGroup.Category.Extruder, extruder_index=1, extruder_color='#ff0000')) - self._settingsGroups.append(SettingsExportGroup("Extruder settings", SettingsExportGroup.Category.Extruder, extruder_index=8, extruder_color='#008fff')) + self._settingsGroups.append(SettingsExportGroup("Global settings", + SettingsExportGroup.Category.Global, + [SettingsExport("Generate Support", "Enabled"), + SettingsExport("Support Type", "Tree")])) + self._settingsGroups.append(SettingsExportGroup("Extruder settings", + SettingsExportGroup.Category.Extruder, + [SettingsExport("Brim Width", "0.7mm")], + extruder_index=1, + extruder_color='#ff0000')) + self._settingsGroups.append(SettingsExportGroup("Extruder settings", + SettingsExportGroup.Category.Extruder, + [], + extruder_index=8, + extruder_color='#008fff')) self._settingsGroups.append(SettingsExportGroup("Model settings", - SettingsExportGroup.Category.Model, 'hypercube.stl')) + SettingsExportGroup.Category.Model, + [SettingsExport("Brim Width", "20.0 mm"), + SettingsExport("Z Hop when retracted", "Disabled")], + 'hypercube.stl')) self._settingsGroups.append(SettingsExportGroup("Model settings", - SettingsExportGroup.Category.Model, 'homer-simpson.stl')) + SettingsExportGroup.Category.Model, + [SettingsExport("Walls Thickness", "3.0 mm"), + SettingsExport("Enable Ironing", "Enabled")], + 'homer-simpson.stl')) diff --git a/plugins/PCBWriter/SettingsSelectionGroup.qml b/plugins/PCBWriter/SettingsSelectionGroup.qml index 12829c96d4..39299ab7c3 100644 --- a/plugins/PCBWriter/SettingsSelectionGroup.qml +++ b/plugins/PCBWriter/SettingsSelectionGroup.qml @@ -10,13 +10,12 @@ import UM 1.5 as UM import Cura 1.1 as Cura import PCBWriter 1.0 as PCBWriter -Column +ColumnLayout { id: settingsGroup + spacing: UM.Theme.getSize("narrow_margin").width - UM.I18nCatalog { id: catalog; name: "cura" } - - Row + RowLayout { id: settingsGroupTitleRow spacing: UM.Theme.getSize("default_margin").width @@ -24,7 +23,6 @@ Column Item { id: icon - anchors.verticalCenter: parent.verticalCenter height: UM.Theme.getSize("medium_button_icon").height width: height @@ -63,8 +61,27 @@ Column { id: settingsTitle text: modelData.name + (modelData.category_details ? ' (%1)'.arg(modelData.category_details) : '') - anchors.verticalCenter: parent.verticalCenter 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 + } } From 8ad4ab90a8e90a173f6e1aa08b02d75a0b822fb2 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 31 Jan 2024 09:26:49 +0100 Subject: [PATCH 05/39] User settings are now properly proposed for export CURA-11561 --- plugins/PCBWriter/PCBDialog.py | 8 +- plugins/PCBWriter/PCBDialog.qml | 2 + plugins/PCBWriter/PCBWriter.py | 4 + plugins/PCBWriter/SettingExport.py | 6 +- plugins/PCBWriter/SettingsExportGroup.py | 8 +- plugins/PCBWriter/SettingsExportModel.py | 132 +++++++++++++++++------ 6 files changed, 114 insertions(+), 46 deletions(-) diff --git a/plugins/PCBWriter/PCBDialog.py b/plugins/PCBWriter/PCBDialog.py index f31c87a61b..385b60272e 100644 --- a/plugins/PCBWriter/PCBDialog.py +++ b/plugins/PCBWriter/PCBDialog.py @@ -1,7 +1,7 @@ # 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 +from PyQt6.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication, QUrl, pyqtSlot from PyQt6.QtGui import QDesktopServices from typing import List, Optional, Dict, cast @@ -26,6 +26,8 @@ i18n_catalog = i18nCatalog("cura") class PCBDialog(QObject): + finished = pyqtSignal() + def __init__(self, parent = None) -> None: super().__init__(parent) @@ -35,3 +37,7 @@ class PCBDialog(QObject): def show(self) -> None: self._view.show() + + @pyqtSlot() + def notifyClosed(self): + self.finished.emit() diff --git a/plugins/PCBWriter/PCBDialog.qml b/plugins/PCBWriter/PCBDialog.qml index 213bed108c..ddf99d5e1f 100644 --- a/plugins/PCBWriter/PCBDialog.qml +++ b/plugins/PCBWriter/PCBDialog.qml @@ -130,4 +130,6 @@ UM.Dialog } buttonSpacing: UM.Theme.getSize("wide_margin").width + + onClosing: manager.notifyClosed() } diff --git a/plugins/PCBWriter/PCBWriter.py b/plugins/PCBWriter/PCBWriter.py index 6391493ae3..26e552f583 100644 --- a/plugins/PCBWriter/PCBWriter.py +++ b/plugins/PCBWriter/PCBWriter.py @@ -64,4 +64,8 @@ class PCBWriter(MeshWriter): def _write(self, stream, nodes, mode): self._config_dialog = PCBDialog() + self._config_dialog.finished.connect(self._onDialogClosed) self._config_dialog.show() + + def _onDialogClosed(self): + self._main_thread_lock.release() diff --git a/plugins/PCBWriter/SettingExport.py b/plugins/PCBWriter/SettingExport.py index 2c230f9ab3..0a761bb6b9 100644 --- a/plugins/PCBWriter/SettingExport.py +++ b/plugins/PCBWriter/SettingExport.py @@ -1,11 +1,7 @@ # Copyright (c) 2024 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from PyQt6.QtCore import Qt, QObject, pyqtProperty -from UM.FlameProfiler import pyqtSlot -from UM.Application import Application -from UM.Qt.ListModel import ListModel -from UM.Logger import Logger +from PyQt6.QtCore import QObject, pyqtProperty class SettingsExport(QObject): diff --git a/plugins/PCBWriter/SettingsExportGroup.py b/plugins/PCBWriter/SettingsExportGroup.py index 0a721a7fb8..2b21c42b78 100644 --- a/plugins/PCBWriter/SettingsExportGroup.py +++ b/plugins/PCBWriter/SettingsExportGroup.py @@ -3,13 +3,7 @@ from enum import IntEnum -from PyQt6.QtCore import Qt, QObject, pyqtProperty, pyqtEnum -from UM.FlameProfiler import pyqtSlot -from UM.Application import Application -from UM.Qt.ListModel import ListModel -from UM.Logger import Logger - -from .SettingExport import SettingsExport +from PyQt6.QtCore import QObject, pyqtProperty, pyqtEnum class SettingsExportGroup(QObject): diff --git a/plugins/PCBWriter/SettingsExportModel.py b/plugins/PCBWriter/SettingsExportModel.py index 0f1ec5113a..b09717298a 100644 --- a/plugins/PCBWriter/SettingsExportModel.py +++ b/plugins/PCBWriter/SettingsExportModel.py @@ -1,49 +1,115 @@ # Copyright (c) 2024 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from PyQt6.QtCore import QObject, Qt, pyqtProperty -from UM.FlameProfiler import pyqtSlot -from UM.Application import Application -from UM.Qt.ListModel import ListModel -from UM.Logger import Logger +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 cura.Settings.ExtruderManager import ExtruderManager 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._settingsGroups = [] - self._updateSettingsExportGroups() + self._settings_groups = [] + + application = CuraApplication.getInstance() + + # Display global settings + global_stack = application.getGlobalContainerStack() + self._settings_groups.append(SettingsExportGroup("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 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("Model settings", + SettingsExportGroup.Category.Model, + self._exportSettings(per_model_stack), + scene_node.getName())) @pyqtProperty(list, constant=True) def settingsGroups(self): - return self._settingsGroups + return self._settings_groups - def _updateSettingsExportGroups(self): - self._settingsGroups.append(SettingsExportGroup("Global settings", - SettingsExportGroup.Category.Global, - [SettingsExport("Generate Support", "Enabled"), - SettingsExport("Support Type", "Tree")])) - self._settingsGroups.append(SettingsExportGroup("Extruder settings", - SettingsExportGroup.Category.Extruder, - [SettingsExport("Brim Width", "0.7mm")], - extruder_index=1, - extruder_color='#ff0000')) - self._settingsGroups.append(SettingsExportGroup("Extruder settings", - SettingsExportGroup.Category.Extruder, - [], - extruder_index=8, - extruder_color='#008fff')) - self._settingsGroups.append(SettingsExportGroup("Model settings", - SettingsExportGroup.Category.Model, - [SettingsExport("Brim Width", "20.0 mm"), - SettingsExport("Z Hop when retracted", "Disabled")], - 'hypercube.stl')) - self._settingsGroups.append(SettingsExportGroup("Model settings", - SettingsExportGroup.Category.Model, - [SettingsExport("Walls Thickness", "3.0 mm"), - SettingsExport("Enable Ironing", "Enabled")], - 'homer-simpson.stl')) + @staticmethod + def _exportSettings(settings_stack): + user_settings_container = settings_stack.getTop() + user_keys = user_settings_container.getAllKeys() + + settings_export = [] + + for setting_to_export in user_keys.intersection(SettingsExportModel.EXPORTABLE_SETTINGS): + label = settings_stack.getProperty(setting_to_export, "label") + value = settings_stack.getProperty(setting_to_export, "value") + + 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) + else: + value = str(value) + + settings_export.append(SettingsExport(label, value)) + + return settings_export \ No newline at end of file From fcf1e63160007a894b8dddf8cf0a32438f95b6cb Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Thu, 1 Feb 2024 09:29:47 +0100 Subject: [PATCH 06/39] It is now possible to generate the PCB file CURA-11561 --- plugins/PCBWriter/PCBDialog.py | 49 ++- plugins/PCBWriter/PCBDialog.qml | 64 +--- plugins/PCBWriter/PCBWriter.py | 443 +++++++++++++++++++++-- plugins/PCBWriter/SettingExport.py | 19 +- plugins/PCBWriter/SettingSelection.qml | 3 +- plugins/PCBWriter/SettingsExportGroup.py | 3 +- plugins/PCBWriter/SettingsExportModel.py | 29 +- plugins/PCBWriter/__init__.py | 12 +- 8 files changed, 510 insertions(+), 112 deletions(-) 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() } From 38b67f8015e508c162c65ba35e25cdbe760d51cb Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Thu, 1 Feb 2024 10:34:19 +0100 Subject: [PATCH 07/39] Now displaying a help icon and value units CURA-11561 --- plugins/3MFReader/WorkspaceSection.qml | 29 ++++-------------------- plugins/PCBWriter/SettingExport.py | 9 ++++++-- plugins/PCBWriter/SettingSelection.qml | 12 +++++++++- plugins/PCBWriter/SettingsExportModel.py | 10 +++++--- 4 files changed, 29 insertions(+), 31 deletions(-) diff --git a/plugins/3MFReader/WorkspaceSection.qml b/plugins/3MFReader/WorkspaceSection.qml index 0c94ab5d6a..63b5e89b41 100644 --- a/plugins/3MFReader/WorkspaceSection.qml +++ b/plugins/3MFReader/WorkspaceSection.qml @@ -5,7 +5,7 @@ import QtQuick 2.10 import QtQuick.Controls 2.3 -import UM 1.5 as UM +import UM 1.8 as UM Item @@ -80,34 +80,13 @@ Item sourceComponent: combobox } - MouseArea + UM.HelpIcon { - id: helpIconMouseArea anchors.right: parent.right anchors.verticalCenter: comboboxLabel.verticalCenter - width: childrenRect.width - height: childrenRect.height - hoverEnabled: true - UM.ColorImage - { - width: UM.Theme.getSize("section_icon").width - height: width - - visible: comboboxTooltipText != "" - source: UM.Theme.getIcon("Help") - color: UM.Theme.getColor("text") - - UM.ToolTip - { - text: comboboxTooltipText - visible: helpIconMouseArea.containsMouse - targetPoint: Qt.point(parent.x + Math.round(parent.width / 2), parent.y) - x: 0 - y: parent.y + parent.height + UM.Theme.getSize("default_margin").height - width: UM.Theme.getSize("tooltip").width - } - } + text: comboboxTooltipText + visible: comboboxTooltipText != "" } } diff --git a/plugins/PCBWriter/SettingExport.py b/plugins/PCBWriter/SettingExport.py index 73dd818897..6702aa1f68 100644 --- a/plugins/PCBWriter/SettingExport.py +++ b/plugins/PCBWriter/SettingExport.py @@ -6,12 +6,13 @@ from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal class SettingExport(QObject): - def __init__(self, id, name, value): + def __init__(self, id, name, value, selectable): super().__init__() self.id = id self._name = name self._value = value - self._selected = True + self._selected = selectable + self._selectable = selectable @pyqtProperty(str, constant=True) def name(self): @@ -31,3 +32,7 @@ class SettingExport(QObject): @pyqtProperty(bool, fset = setSelected, notify = selectedChanged) def selected(self): return self._selected + + @pyqtProperty(bool, constant=True) + def selectable(self): + return self._selectable diff --git a/plugins/PCBWriter/SettingSelection.qml b/plugins/PCBWriter/SettingSelection.qml index 6439542f3f..636b67fb37 100644 --- a/plugins/PCBWriter/SettingSelection.qml +++ b/plugins/PCBWriter/SettingSelection.qml @@ -6,7 +6,7 @@ import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 import QtQuick.Window 2.2 -import UM 1.5 as UM +import UM 1.8 as UM import Cura 1.1 as Cura RowLayout @@ -19,10 +19,20 @@ RowLayout 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 too much on the used printer capacities") + visible: !modelData.selectable + } } diff --git a/plugins/PCBWriter/SettingsExportModel.py b/plugins/PCBWriter/SettingsExportModel.py index 992adeb089..a4acaf02f7 100644 --- a/plugins/PCBWriter/SettingsExportModel.py +++ b/plugins/PCBWriter/SettingsExportModel.py @@ -110,17 +110,21 @@ class SettingsExportModel(QObject): settings_export = [] - for setting_to_export in user_keys.intersection(SettingsExportModel.EXPORTABLE_SETTINGS): + 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) + value = SettingDefinition.settingValueToString(setting_type, value) + " " + unit else: value = str(value) - settings_export.append(SettingExport(setting_to_export, label, value)) + settings_export.append(SettingExport(setting_to_export, + label, + value, + setting_to_export in SettingsExportModel.EXPORTABLE_SETTINGS)) return settings_export From b931029f1c6c165bf841e512281d3c770df40c55 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Fri, 2 Feb 2024 10:03:48 +0100 Subject: [PATCH 08/39] Now using ThreeMFWriter to save PCB files CURA-11561 --- plugins/{PCBWriter => 3MFWriter}/PCBDialog.py | 0 .../{PCBWriter => 3MFWriter}/PCBDialog.qml | 1 - .../{PCBWriter => 3MFWriter}/SettingExport.py | 0 .../SettingSelection.qml | 2 +- .../SettingsExportGroup.py | 0 .../SettingsExportModel.py | 0 .../SettingsSelectionGroup.qml | 8 +- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 128 ++++- plugins/3MFWriter/ThreeMFWriter.py | 59 ++- plugins/3MFWriter/__init__.py | 45 +- plugins/PCBWriter/PCBWriter.py | 460 ------------------ plugins/PCBWriter/__init__.py | 29 -- plugins/PCBWriter/plugin.json | 8 - resources/qml/Menus/FileMenu.qml | 12 + 14 files changed, 207 insertions(+), 545 deletions(-) rename plugins/{PCBWriter => 3MFWriter}/PCBDialog.py (100%) rename plugins/{PCBWriter => 3MFWriter}/PCBDialog.qml (98%) rename plugins/{PCBWriter => 3MFWriter}/SettingExport.py (100%) rename plugins/{PCBWriter => 3MFWriter}/SettingSelection.qml (93%) rename plugins/{PCBWriter => 3MFWriter}/SettingsExportGroup.py (100%) rename plugins/{PCBWriter => 3MFWriter}/SettingsExportModel.py (100%) rename plugins/{PCBWriter => 3MFWriter}/SettingsSelectionGroup.qml (88%) delete mode 100644 plugins/PCBWriter/PCBWriter.py delete mode 100644 plugins/PCBWriter/__init__.py delete mode 100644 plugins/PCBWriter/plugin.json diff --git a/plugins/PCBWriter/PCBDialog.py b/plugins/3MFWriter/PCBDialog.py similarity index 100% rename from plugins/PCBWriter/PCBDialog.py rename to plugins/3MFWriter/PCBDialog.py diff --git a/plugins/PCBWriter/PCBDialog.qml b/plugins/3MFWriter/PCBDialog.qml similarity index 98% rename from plugins/PCBWriter/PCBDialog.qml rename to plugins/3MFWriter/PCBDialog.qml index 8264e3ee96..b65520961b 100644 --- a/plugins/PCBWriter/PCBDialog.qml +++ b/plugins/3MFWriter/PCBDialog.qml @@ -8,7 +8,6 @@ import QtQuick.Window 2.2 import UM 1.5 as UM import Cura 1.1 as Cura -import PCBWriter 1.0 as PCBWriter UM.Dialog { diff --git a/plugins/PCBWriter/SettingExport.py b/plugins/3MFWriter/SettingExport.py similarity index 100% rename from plugins/PCBWriter/SettingExport.py rename to plugins/3MFWriter/SettingExport.py diff --git a/plugins/PCBWriter/SettingSelection.qml b/plugins/3MFWriter/SettingSelection.qml similarity index 93% rename from plugins/PCBWriter/SettingSelection.qml rename to plugins/3MFWriter/SettingSelection.qml index 636b67fb37..478c2d393c 100644 --- a/plugins/PCBWriter/SettingSelection.qml +++ b/plugins/3MFWriter/SettingSelection.qml @@ -32,7 +32,7 @@ RowLayout UM.I18nCatalog { id: catalog; name: "cura" } text: catalog.i18nc("@tooltip", - "This setting can't be exported because it depends too much on the used printer capacities") + "This setting can't be exported because it depends on the used printer capacities") visible: !modelData.selectable } } diff --git a/plugins/PCBWriter/SettingsExportGroup.py b/plugins/3MFWriter/SettingsExportGroup.py similarity index 100% rename from plugins/PCBWriter/SettingsExportGroup.py rename to plugins/3MFWriter/SettingsExportGroup.py diff --git a/plugins/PCBWriter/SettingsExportModel.py b/plugins/3MFWriter/SettingsExportModel.py similarity index 100% rename from plugins/PCBWriter/SettingsExportModel.py rename to plugins/3MFWriter/SettingsExportModel.py diff --git a/plugins/PCBWriter/SettingsSelectionGroup.qml b/plugins/3MFWriter/SettingsSelectionGroup.qml similarity index 88% rename from plugins/PCBWriter/SettingsSelectionGroup.qml rename to plugins/3MFWriter/SettingsSelectionGroup.qml index 39299ab7c3..e77ba692bc 100644 --- a/plugins/PCBWriter/SettingsSelectionGroup.qml +++ b/plugins/3MFWriter/SettingsSelectionGroup.qml @@ -8,7 +8,7 @@ import QtQuick.Window 2.2 import UM 1.5 as UM import Cura 1.1 as Cura -import PCBWriter 1.0 as PCBWriter +import ThreeMFWriter 1.0 as ThreeMFWriter ColumnLayout { @@ -34,9 +34,9 @@ ColumnLayout { switch(modelData.category) { - case PCBWriter.SettingsExportGroup.Global: + case ThreeMFWriter.SettingsExportGroup.Global: return UM.Theme.getIcon("Sliders") - case PCBWriter.SettingsExportGroup.Model: + case ThreeMFWriter.SettingsExportGroup.Model: return UM.Theme.getIcon("View3D") default: return "" @@ -50,7 +50,7 @@ ColumnLayout { id: settingsExtruderIcon anchors.fill: parent - visible: modelData.category === PCBWriter.SettingsExportGroup.Extruder + visible: modelData.category === ThreeMFWriter.SettingsExportGroup.Extruder text: (modelData.extruder_index + 1).toString() font: UM.Theme.getFont("tiny_emphasis") materialColor: modelData.extruder_color diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index e89af5c70a..9715e9ac98 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -3,7 +3,9 @@ import configparser from io import StringIO +from threading import Lock import zipfile +from typing import Dict, Any from UM.Application import Application from UM.Logger import Logger @@ -13,15 +15,50 @@ from UM.Workspace.WorkspaceWriter import WorkspaceWriter from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") -from cura.Utils.Threading import call_on_qt_thread +from .PCBDialog import PCBDialog +from .ThreeMFWriter import ThreeMFWriter +from .SettingsExportModel import SettingsExportModel +from .SettingsExportGroup import SettingsExportGroup + +USER_SETTINGS_PATH = "Cura/user-settings.json" class ThreeMFWorkspaceWriter(WorkspaceWriter): def __init__(self): super().__init__() + self._main_thread_lock = Lock() + self._success = False + self._export_model = None + self._stream = None + self._nodes = None + self._mode = None + self._config_dialog = None - @call_on_qt_thread - def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode): + def _preWrite(self): + is_pcb = False + if hasattr(self._stream, 'name'): + # This only works with local file, but we don't want remote PCB files yet + is_pcb = self._stream.name.endswith('.pcb') + + if is_pcb: + self._config_dialog = PCBDialog() + self._config_dialog.finished.connect(self._onPCBConfigFinished) + self._config_dialog.show() + else: + self._doWrite() + + def _onPCBConfigFinished(self, accepted: bool): + if accepted: + self._export_model = self._config_dialog.getModel() + self._doWrite() + else: + self._main_thread_lock.release() + + def _doWrite(self): + self._write() + self._main_thread_lock.release() + + def _write(self): application = Application.getInstance() machine_manager = application.getMachineManager() @@ -30,24 +67,24 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): if not mesh_writer: # We need to have the 3mf mesh writer, otherwise we can't save the entire workspace self.setInformation(catalog.i18nc("@error:zip", "3MF Writer plug-in is corrupt.")) Logger.error("3MF Writer class is unavailable. Can't write workspace.") - return False + return global_stack = machine_manager.activeMachine 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.") - return False + return # 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) - if not mesh_writer.write(stream, nodes, mode): + if not mesh_writer.write(self._stream, self._nodes, self._mode, self._export_model): self.setInformation(mesh_writer.getInformation()) - return False + return archive = mesh_writer.getArchive() 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(self._stream, "w", compression=zipfile.ZIP_DEFLATED) try: # Add global container stack data to the archive. @@ -62,15 +99,21 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): self._writeContainerToArchive(extruder_stack, archive) for container in extruder_stack.getContainers(): self._writeContainerToArchive(container, archive) + + # Write user settings data + if self._export_model is not None: + user_settings_data = self._getUserSettings(self._export_model) + ThreeMFWriter._storeMetadataJson(user_settings_data, archive, USER_SETTINGS_PATH) except PermissionError: self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here.")) Logger.error("No permission to write workspace to this stream.") - return False + return # 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() - 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.setValue(preference, original_preferences.getValue(preference)) preferences_string = StringIO() @@ -81,7 +124,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): # Save Cura version 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.set("versions", "cura_version", application.getVersion()) version_config_parser.set("versions", "build_type", application.getBuildType()) @@ -98,13 +141,37 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): except PermissionError: self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here.")) Logger.error("No permission to write workspace to this stream.") - return False + return except EnvironmentError as e: self.setInformation(catalog.i18nc("@error:zip", str(e))) - Logger.error("EnvironmentError when writing workspace to this stream: {err}".format(err = str(e))) - return False + Logger.error("EnvironmentError when writing workspace to this stream: {err}".format(err=str(e))) + return mesh_writer.setStoreArchive(False) - return True + + self._success = True + + def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode): + self._success = False + self._export_model = None + self._stream = stream + self._nodes = nodes + self._mode = mode + self._config_dialog = None + + self._main_thread_lock.acquire() + # Export is done in main thread because it may require a few asynchronous configuration steps + Application.getInstance().callLater(self._preWrite) + self._main_thread_lock.acquire() # Block until lock has been released, meaning the config+write is over + + self._main_thread_lock.release() + + self._export_model = None + self._stream = None + self._nodes = None + self._mode = None + self._config_dialog = None + + return self._success @staticmethod def _writePluginMetadataToArchive(archive: zipfile.ZipFile) -> None: @@ -165,4 +232,27 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): archive.writestr(file_in_archive, serialized_data) except (FileNotFoundError, EnvironmentError): Logger.error("File became inaccessible while writing to it: {archive_filename}".format(archive_filename = archive.fp.name)) - return \ No newline at end of file + 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 \ No newline at end of file diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index ad4b0d8dad..8924ac0a61 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -40,6 +40,9 @@ except ImportError: import zipfile import UM.Application +from .SettingsExportModel import SettingsExportModel +from .SettingsExportGroup import SettingsExportGroup + from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") @@ -87,7 +90,9 @@ class ThreeMFWriter(MeshWriter): self._store_archive = store_archive @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 :returns: Uranium Scene node. @@ -129,13 +134,22 @@ class ThreeMFWriter(MeshWriter): if stack is not None: changed_setting_keys = stack.getTop().getAllKeys() - # Ensure that we save the extruder used for this object in a multi-extrusion setup - if stack.getProperty("machine_extruder_count", "value") > 1: - changed_setting_keys.add("extruder_nr") + if exported_settings is None: + # Ensure that we save the extruder used for this object in a multi-extrusion setup + if stack.getProperty("machine_extruder_count", "value") > 1: + changed_setting_keys.add("extruder_nr") - # Get values for all changed settings & save them. - for key in changed_setting_keys: - savitar_node.setSetting("cura:" + key, str(stack.getProperty(key, "value"))) + # Get values for all changed settings & save them. + for key in changed_setting_keys: + 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"))) # Store the metadata. for key, value in um_node.metadata.items(): @@ -145,7 +159,8 @@ class ThreeMFWriter(MeshWriter): # only save the nodes on the active build plate if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr: 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: savitar_node.addChild(savitar_child_node) @@ -154,7 +169,7 @@ class ThreeMFWriter(MeshWriter): def getArchive(self): return self._archive - def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode) -> bool: + def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode, export_settings_model = None) -> bool: self._archive = None # Reset archive archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED) try: @@ -232,14 +247,19 @@ class ThreeMFWriter(MeshWriter): transformation_matrix.preMultiply(translation_matrix) root_node = UM.Application.Application.getInstance().getController().getScene().getRoot() + exported_model_settings = ThreeMFWriter._extractModelExportedSettings(export_settings_model) for node in nodes: if node == root_node: 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: savitar_scene.addSceneNode(savitar_node) else: - savitar_node = self._convertUMNodeToSavitarNode(node, transformation_matrix) + savitar_node = self._convertUMNodeToSavitarNode(node, + transformation_matrix, + exported_model_settings) if savitar_node: savitar_scene.addSceneNode(savitar_node) @@ -395,3 +415,20 @@ class ThreeMFWriter(MeshWriter): parser = Savitar.ThreeMFParser() scene_string = parser.sceneToString(savitar_scene) 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 diff --git a/plugins/3MFWriter/__init__.py b/plugins/3MFWriter/__init__.py index eb8a596afe..e0d4037603 100644 --- a/plugins/3MFWriter/__init__.py +++ b/plugins/3MFWriter/__init__.py @@ -2,9 +2,12 @@ # Uranium is released under the terms of the LGPLv3 or higher. import sys +from PyQt6.QtQml import qmlRegisterType + from UM.Logger import Logger try: from . import ThreeMFWriter + from .SettingsExportGroup import SettingsExportGroup threemf_writer_was_imported = True except ImportError: Logger.log("w", "Could not import ThreeMFWriter; libSavitar may be missing") @@ -23,20 +26,36 @@ def getMetaData(): if threemf_writer_was_imported: metaData["mesh_writer"] = { - "output": [{ - "extension": "3mf", - "description": i18n_catalog.i18nc("@item:inlistbox", "3MF file"), - "mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml", - "mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode - }] + "output": [ + { + "extension": "3mf", + "description": i18n_catalog.i18nc("@item:inlistbox", "3MF file"), + "mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml", + "mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode + }, + { + "extension": "pcb", + "description": i18n_catalog.i18nc("@item:inlistbox", "PCB file"), + "mime_type": "application/x-pcb", + "mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode + } + ] } metaData["workspace_writer"] = { - "output": [{ - "extension": workspace_extension, - "description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"), - "mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml", - "mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode - }] + "output": [ + { + "extension": workspace_extension, + "description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"), + "mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml", + "mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode + }, + { + "extension": "pcb", + "description": i18n_catalog.i18nc("@item:inlistbox", "Pre-Configured Batch file"), + "mime_type": "application/x-pcb", + "mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode + } + ] } return metaData @@ -44,6 +63,8 @@ def getMetaData(): def register(app): if "3MFWriter.ThreeMFWriter" in sys.modules: + qmlRegisterType(SettingsExportGroup, "ThreeMFWriter", 1, 0, "SettingsExportGroup") + return {"mesh_writer": ThreeMFWriter.ThreeMFWriter(), "workspace_writer": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter()} else: diff --git a/plugins/PCBWriter/PCBWriter.py b/plugins/PCBWriter/PCBWriter.py deleted file mode 100644 index 794eac9d4a..0000000000 --- a/plugins/PCBWriter/PCBWriter.py +++ /dev/null @@ -1,460 +0,0 @@ -# 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. - -import pySavitar as Savitar - -from PyQt6.QtCore import QBuffer - -from UM.Mesh.MeshWriter import MeshWriter -from UM.Logger import Logger -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.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 .PCBDialog import PCBDialog -from .SettingsExportModel import SettingsExportModel -from .SettingsExportGroup import SettingsExportGroup - -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__() - 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() - - 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._onDialogFinished) - self._config_dialog.show() - - 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/__init__.py b/plugins/PCBWriter/__init__.py deleted file mode 100644 index da4205a7d7..0000000000 --- a/plugins/PCBWriter/__init__.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright (c) 2024 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. -import sys - -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(): - return {"mesh_writer": { - "output": [{ - "extension": "pcb", - "description": i18n_catalog.i18nc("@item:inlistbox", "Pre-Configured Batch file"), - "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() } diff --git a/plugins/PCBWriter/plugin.json b/plugins/PCBWriter/plugin.json deleted file mode 100644 index 6571185779..0000000000 --- a/plugins/PCBWriter/plugin.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "Pre-Configured Batch Writer", - "author": "Ultimaker B.V.", - "version": "1.0.0", - "description": "Provides support for writing Pre-Configured Batch files.", - "api": 8, - "i18n-catalog": "cura" -} diff --git a/resources/qml/Menus/FileMenu.qml b/resources/qml/Menus/FileMenu.qml index 254c0d5468..850c0d7e73 100644 --- a/resources/qml/Menus/FileMenu.qml +++ b/resources/qml/Menus/FileMenu.qml @@ -70,6 +70,18 @@ Cura.Menu enabled: UM.WorkspaceFileHandler.enabled } + Cura.MenuItem + { + id: savePCBMenu + text: catalog.i18nc("@title:menu menubar:file", "&Save PCB Project...") + enabled: UM.WorkspaceFileHandler.enabled + onTriggered: + { + var args = { "filter_by_machine": false, "file_type": "workspace", "preferred_mimetypes": "application/x-pcb" }; + UM.OutputDeviceManager.requestWriteToDevice("local_file", PrintInformation.jobName, args) + } + } + Cura.MenuSeparator { } UM.MeshWritersModel { id: meshWritersModel } From c6e56202954ef1b7752306a9aac41805920b2428 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Fri, 2 Feb 2024 10:07:11 +0100 Subject: [PATCH 09/39] Revert "Add export sub-menu" This reverts commit 54af5bca3c5e9b5700ce106f854f97e3ed102842. --- resources/qml/Menus/ExportMenu.qml | 44 ------------------------------ resources/qml/Menus/FileMenu.qml | 26 +++++++++--------- 2 files changed, 13 insertions(+), 57 deletions(-) delete mode 100644 resources/qml/Menus/ExportMenu.qml diff --git a/resources/qml/Menus/ExportMenu.qml b/resources/qml/Menus/ExportMenu.qml deleted file mode 100644 index 5c08b04f0f..0000000000 --- a/resources/qml/Menus/ExportMenu.qml +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2024 Ultimaker B.V. -// Cura is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.2 -import QtQuick.Controls 2.1 - -import UM 1.5 as UM -import Cura 1.1 as Cura - -import "../Dialogs" - -Cura.Menu -{ - id: exportMenu - property alias model: meshWriters.model - property bool selectionOnly: false - - Instantiator - { - id: meshWriters - Cura.MenuItem - { - text: model.description - onTriggered: - { - var localDeviceId = "local_file" - var file_name = PrintInformation.jobName - var args = { "filter_by_machine": false, "limit_mimetypes": [model.mime_type], "limit_modes": [model.mode]} - if(exportMenu.selectionOnly) - { - UM.OutputDeviceManager.requestWriteSelectionToDevice(localDeviceId, file_name, args) - } - else - { - UM.OutputDeviceManager.requestWriteToDevice(localDeviceId, file_name, args) - } - } - shortcut: model.shortcut - enabled: exportMenu.shouldBeVisible - } - onObjectAdded: function(index, object) { exportMenu.insertItem(index, object)} - onObjectRemoved: function(index, object) { exportMenu.removeItem(object)} - } -} diff --git a/resources/qml/Menus/FileMenu.qml b/resources/qml/Menus/FileMenu.qml index 850c0d7e73..36a2820087 100644 --- a/resources/qml/Menus/FileMenu.qml +++ b/resources/qml/Menus/FileMenu.qml @@ -4,7 +4,7 @@ import QtQuick 2.2 import QtQuick.Controls 2.1 -import UM 1.7 as UM +import UM 1.6 as UM import Cura 1.0 as Cura Cura.Menu @@ -84,24 +84,24 @@ Cura.Menu Cura.MenuSeparator { } - UM.MeshWritersModel { id: meshWritersModel } - - ExportMenu + Cura.MenuItem { - id: exportMenu - title: catalog.i18nc("@title:menu menubar:file", "&Export...") - model: meshWritersModel - shouldBeVisible: model.count > 0 + id: saveAsMenu + text: catalog.i18nc("@title:menu menubar:file", "&Export...") + onTriggered: + { + var localDeviceId = "local_file" + UM.OutputDeviceManager.requestWriteToDevice(localDeviceId, PrintInformation.jobName, { "filter_by_machine": false, "preferred_mimetypes": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml"}) + } } - ExportMenu + Cura.MenuItem { id: exportSelectionMenu - title: catalog.i18nc("@action:inmenu menubar:file", "Export Selection...") - model: meshWritersModel - shouldBeVisible: model.count > 0 + text: catalog.i18nc("@action:inmenu menubar:file", "Export Selection...") enabled: UM.Selection.hasSelection - selectionOnly: true + 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"}) } Cura.MenuSeparator { } From 733ef4d3d827e11bdcaefe484f17e066f1b0ce89 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Fri, 2 Feb 2024 12:17:34 +0100 Subject: [PATCH 10/39] UI now displays an option to select the same profile CURA-11561 --- plugins/3MFReader/ThreeMFReader.py | 9 ++++++- plugins/3MFReader/ThreeMFWorkspaceReader.py | 10 +++++--- plugins/3MFReader/WorkspaceDialog.py | 26 +++++++++++++++++++++ plugins/3MFReader/WorkspaceDialog.qml | 9 +++++++ plugins/3MFReader/__init__.py | 8 +++++++ plugins/3MFReader/plugin.json | 2 +- plugins/3MFWriter/plugin.json | 2 +- resources/qml/Menus/FileMenu.qml | 5 +++- 8 files changed, 64 insertions(+), 7 deletions(-) diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index 13a97d5a89..9f4a4b197b 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -46,8 +46,15 @@ class ThreeMFReader(MeshReader): suffixes=["3mf"] ) ) + MimeTypeDatabase.addMimeType( + MimeType( + name="application/x-pcb", + comment="PCB", + suffixes=["pcb"] + ) + ) - self._supported_extensions = [".3mf"] + self._supported_extensions = [".3mf", ".pcb"] self._root = None self._base_name = "" self._unit = None diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index b97cb34b01..76cd1f386b 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -112,7 +112,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): def __init__(self) -> None: super().__init__() - self._supported_extensions = [".3mf"] + self._supported_extensions = [".3mf", ".pcb"] self._dialog = WorkspaceDialog() self._3mf_mesh_reader = None self._container_registry = ContainerRegistry.getInstance() @@ -228,11 +228,14 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._resolve_strategies = {k: None for k in resolve_strategy_keys} containers_found_dict = {k: False for k in resolve_strategy_keys} + # Check whether the file is a PCB + is_pcb = file_name.endswith('.pcb') + # # Read definition containers # machine_definition_id = None - updatable_machines = [] + updatable_machines = None if is_pcb else [] machine_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)] @@ -250,7 +253,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if definition_container_type == "machine": machine_definition_id = container_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]] machine_type = definition_container["name"] variant_type_name = definition_container.get("variants_name", variant_type_name) @@ -617,6 +620,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._dialog.setVariantType(variant_type_name) self._dialog.setHasObjectsOnPlate(Application.getInstance().platformActivity) self._dialog.setMissingPackagesMetadata(missing_package_metadata) + self._dialog.setHasVisibleSelectSameProfileChanged(is_pcb) self._dialog.show() # Choosing the initially selected printer in MachineSelector diff --git a/plugins/3MFReader/WorkspaceDialog.py b/plugins/3MFReader/WorkspaceDialog.py index 0203fc92b5..c5b624d35d 100644 --- a/plugins/3MFReader/WorkspaceDialog.py +++ b/plugins/3MFReader/WorkspaceDialog.py @@ -71,6 +71,8 @@ class WorkspaceDialog(QObject): self._install_missing_package_dialog: Optional[QObject] = None self._is_abstract_machine = False self._is_networked_machine = False + self._is_compatible_machine = False + self._has_visible_select_same_profile = False machineConflictChanged = pyqtSignal() qualityChangesConflictChanged = pyqtSignal() @@ -94,6 +96,8 @@ class WorkspaceDialog(QObject): extrudersChanged = pyqtSignal() isPrinterGroupChanged = pyqtSignal() missingPackagesChanged = pyqtSignal() + isCompatibleMachineChanged = pyqtSignal() + hasVisibleSelectSameProfileChanged = pyqtSignal() @pyqtProperty(bool, notify = isPrinterGroupChanged) def isPrinterGroup(self) -> bool: @@ -291,8 +295,30 @@ class WorkspaceDialog(QObject): @pyqtSlot(str) def setMachineToOverride(self, machine_name: str) -> None: + registry = ContainerRegistry.getInstance() + containers_expected = registry.findDefinitionContainers(name = self._machine_type) + containers_selected = registry.findContainerStacks(id = machine_name) + 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() + self._override_machine = machine_name + @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 + @pyqtSlot() def closeBackend(self) -> None: """Close the backend: otherwise one could end up with "Slicing...""" diff --git a/plugins/3MFReader/WorkspaceDialog.qml b/plugins/3MFReader/WorkspaceDialog.qml index d5f9b1817d..45fe7b6989 100644 --- a/plugins/3MFReader/WorkspaceDialog.qml +++ b/plugins/3MFReader/WorkspaceDialog.qml @@ -186,6 +186,15 @@ UM.Dialog rightLabelText: catalog.i18ncp("@action:label", "%1, %2 override", "%1, %2 overrides", manager.numSettingsOverridenByQualityChanges).arg(manager.qualityType).arg(manager.numSettingsOverridenByQualityChanges) visible: manager.numSettingsOverridenByQualityChanges != 0 } + + UM.CheckBox + { + text: catalog.i18nc("@action:checkbox", "Select the same profile") + enabled: manager.isCompatibleMachine + onEnabledChanged: checked = 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 + } } comboboxVisible: manager.qualityChangesConflict diff --git a/plugins/3MFReader/__init__.py b/plugins/3MFReader/__init__.py index 5e2b68fce0..a07420d2c6 100644 --- a/plugins/3MFReader/__init__.py +++ b/plugins/3MFReader/__init__.py @@ -25,12 +25,20 @@ def getMetaData() -> Dict: { "extension": "3mf", "description": catalog.i18nc("@item:inlistbox", "3MF File") + }, + { + "extension": "pcb", + "description": catalog.i18nc("@item:inlistbox", "PCB File") } ] metaData["workspace_reader"] = [ { "extension": workspace_extension, "description": catalog.i18nc("@item:inlistbox", "3MF File") + }, + { + "extension": "pcb", + "description": catalog.i18nc("@item:inlistbox", "PCB File") } ] diff --git a/plugins/3MFReader/plugin.json b/plugins/3MFReader/plugin.json index bf0bc05364..1611c956d3 100644 --- a/plugins/3MFReader/plugin.json +++ b/plugins/3MFReader/plugin.json @@ -2,7 +2,7 @@ "name": "3MF Reader", "author": "Ultimaker B.V.", "version": "1.0.1", - "description": "Provides support for reading 3MF files.", + "description": "Provides support for reading 3MF and PCB files.", "api": 8, "i18n-catalog": "cura" } diff --git a/plugins/3MFWriter/plugin.json b/plugins/3MFWriter/plugin.json index b59d4ef8e1..be6d50267c 100644 --- a/plugins/3MFWriter/plugin.json +++ b/plugins/3MFWriter/plugin.json @@ -2,7 +2,7 @@ "name": "3MF Writer", "author": "Ultimaker B.V.", "version": "1.0.1", - "description": "Provides support for writing 3MF files.", + "description": "Provides support for writing 3MF and PCB files.", "api": 8, "i18n-catalog": "cura" } diff --git a/resources/qml/Menus/FileMenu.qml b/resources/qml/Menus/FileMenu.qml index 36a2820087..a6fb339faf 100644 --- a/resources/qml/Menus/FileMenu.qml +++ b/resources/qml/Menus/FileMenu.qml @@ -77,7 +77,10 @@ Cura.Menu enabled: UM.WorkspaceFileHandler.enabled onTriggered: { - var args = { "filter_by_machine": false, "file_type": "workspace", "preferred_mimetypes": "application/x-pcb" }; + var args = { "filter_by_machine": false, + "file_type": "workspace", + "preferred_mimetypes": "application/x-pcb", + "limit_mimetypes": "application/x-pcb"}; UM.OutputDeviceManager.requestWriteToDevice("local_file", PrintInformation.jobName, args) } } From ab0a52063d0d0f327b7b7c4ccbc28d35dd61feb1 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Fri, 2 Feb 2024 16:05:36 +0100 Subject: [PATCH 11/39] Now loading user settings CURA-11561 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 222 ++++++++++++-------- plugins/3MFReader/WorkspaceDialog.py | 29 ++- plugins/3MFReader/WorkspaceDialog.qml | 15 +- plugins/3MFWriter/SettingsExportModel.py | 2 +- 4 files changed, 178 insertions(+), 90 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 76cd1f386b..3398b2e7d5 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -5,6 +5,7 @@ from configparser import ConfigParser import zipfile import os import json +import re from typing import cast, Dict, List, Optional, Tuple, Any, Set import xml.etree.ElementTree as ET @@ -141,10 +142,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._old_new_materials: Dict[str, str] = {} self._machine_info = None + self._load_profile = False + def _clearState(self): self._id_mapping = {} self._old_new_materials = {} self._machine_info = None + self._load_profile = False 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. @@ -228,7 +232,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._resolve_strategies = {k: None for k in resolve_strategy_keys} containers_found_dict = {k: False for k in resolve_strategy_keys} - # Check whether the file is a PCB + # Check whether the file is a PCB, which changes some import options is_pcb = file_name.endswith('.pcb') # @@ -621,6 +625,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._dialog.setHasObjectsOnPlate(Application.getInstance().platformActivity) self._dialog.setMissingPackagesMetadata(missing_package_metadata) self._dialog.setHasVisibleSelectSameProfileChanged(is_pcb) + self._dialog.setAllowCreatemachine(not is_pcb) self._dialog.show() # Choosing the initially selected printer in MachineSelector @@ -652,6 +657,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._dialog.setIsNetworkedMachine(is_networked_machine) self._dialog.setIsAbstractMachine(is_abstract_machine) self._dialog.setMachineName(machine_name) + self._dialog.updateCompatibleMachine() + self._dialog.setSelectSameProfileChecked(self._dialog.isCompatibleMachine) # Block until the dialog is closed. self._dialog.waitForClose() @@ -659,6 +666,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if self._dialog.getResult() == {}: return WorkspaceReader.PreReadResult.cancelled + self._load_profile = not is_pcb or self._dialog.selectSameProfileChecked + self._resolve_strategies = self._dialog.getResult() # # There can be 3 resolve strategies coming from the dialog: @@ -694,16 +703,16 @@ class ThreeMFWorkspaceReader(WorkspaceReader): except EnvironmentError as e: message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags or !", "Project file {0} is suddenly inaccessible: {1}.", file_name, str(e)), - title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"), - message_type = Message.MessageType.ERROR) + title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"), + message_type = Message.MessageType.ERROR) message.show() self.setWorkspaceName("") return [], {} except zipfile.BadZipFile as e: message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags or !", "Project file {0} is corrupt: {1}.", file_name, str(e)), - title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"), - message_type = Message.MessageType.ERROR) + title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"), + message_type = Message.MessageType.ERROR) message.show() self.setWorkspaceName("") return [], {} @@ -765,9 +774,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Find the machine which will be overridden global_stacks = self._container_registry.findContainerStacks(id = self._dialog.getMachineToOverride(), type = "machine") if not global_stacks: - message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tag !", + message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tag !", "Project file {0} 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() self.setWorkspaceName("") return [], {} @@ -781,84 +790,107 @@ class ThreeMFWorkspaceReader(WorkspaceReader): for stack in extruder_stacks: stack.setNextStack(global_stack, connect_signals = False) - Logger.log("d", "Workspace loading is checking definitions...") - # Get all the definition files & check if they exist. If not, add them. - definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)] - for definition_container_file in definition_container_files: - container_id = self._stripFileToId(definition_container_file) + user_settings = {} - definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id) - if not definitions: - 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. + if self._load_profile: + Logger.log("d", "Workspace loading is checking definitions...") + # Get all the definition files & check if they exist. If not, add them. + definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)] + for definition_container_file in definition_container_files: + container_id = self._stripFileToId(definition_container_file) - 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) + definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id) + if not definitions: + definition_container = DefinitionContainer(container_id) try: - material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"), - file_name = container_id + "." + self._material_container_suffix) + definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"), + file_name = definition_container_file) 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) + # 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. - if global_stack: - # Handle quality changes if any - self._processQualityChanges(global_stack) + 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) - # Prepare the machine - self._applyChangesToMachine(global_stack, extruder_stack_dict) + if not materials: + # No material found, deserialize this material later and add it + to_deserialize_material = True + else: + material_container = materials[0] + old_material_root_id = material_container.getMetaDataEntry("base_file") + if old_material_root_id is not None and not self._container_registry.isReadOnly(old_material_root_id): # Only create new materials if they are not read only. + to_deserialize_material = True + + if self._resolve_strategies["material"] == "override": + # Remove the old materials and then deserialize the one from the project + root_material_id = material_container.getMetaDataEntry("base_file") + application.getContainerRegistry().removeContainer(root_material_id) + elif self._resolve_strategies["material"] == "new": + # Note that we *must* deserialize it with a new ID, as multiple containers will be + # auto created & added. + container_id = self.getNewId(container_id) + self._old_new_materials[old_material_root_id] = container_id + need_new_name = True + + if to_deserialize_material: + material_container = xml_material_profile(container_id) + try: + material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"), + file_name = container_id + "." + self._material_container_suffix) + except ContainerFormatError: + Logger.logException("e", "Failed to deserialize material file %s in project file %s", + material_container_file, file_name) + continue + if need_new_name: + new_name = ContainerRegistry.getInstance().uniqueName(material_container.getName()) + material_container.setName(new_name) + material_container.setDirty(True) + self._container_registry.addContainer(material_container) + Job.yieldThread() + QCoreApplication.processEvents() # Ensure that the GUI does not freeze. + else: + Logger.log("d", "Workspace loading user settings...") + try: + user_settings = json.loads(archive.open("Cura/user-settings.json").read().decode("utf-8")) + except KeyError as e: + # If there is no user settings file, it's not a PCB, so notify user of failure. + Logger.log("w", "File %s is not a valid PCB.", file_name) + message = Message( + i18n_catalog.i18nc("@info:error Don't translate the XML tags or !", + "Project file {0} is corrupt: {1}.", + file_name, str(e)), + title=i18n_catalog.i18nc("@info:title", "Can't Open Project File"), + message_type=Message.MessageType.ERROR) + message.show() + self.setWorkspaceName("") + return [], {} + + + 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: + self._applyUserSettings(global_stack, extruder_stack_dict, user_settings) Logger.log("d", "Workspace loading is notifying rest of the code of changes...") # Actually change the active machine. @@ -1181,21 +1213,47 @@ class ThreeMFWorkspaceReader(WorkspaceReader): material_node = machine_node.variants[extruder_stack.variant.getName()].materials[root_material_id] extruder_stack.material = material_node.container - def _applyChangesToMachine(self, global_stack, extruder_stack_dict): - # Clear all first + def _clearMachineSettings(self, global_stack, extruder_stack_dict): self._clearStack(global_stack) for extruder_stack in extruder_stack_dict.values(): 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): + # Clear all first + self._clearMachineSettings(global_stack, extruder_stack_dict) + + 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._applyUserChanges(global_stack, extruder_stack_dict) self._applyVariants(global_stack, extruder_stack_dict) self._applyMaterials(global_stack, extruder_stack_dict) # 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: self._quality_changes_to_apply = self._machine_info.quality_changes_info.name else: diff --git a/plugins/3MFReader/WorkspaceDialog.py b/plugins/3MFReader/WorkspaceDialog.py index c5b624d35d..c0006e21a8 100644 --- a/plugins/3MFReader/WorkspaceDialog.py +++ b/plugins/3MFReader/WorkspaceDialog.py @@ -73,6 +73,8 @@ class WorkspaceDialog(QObject): 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 machineConflictChanged = pyqtSignal() qualityChangesConflictChanged = pyqtSignal() @@ -98,6 +100,7 @@ class WorkspaceDialog(QObject): missingPackagesChanged = pyqtSignal() isCompatibleMachineChanged = pyqtSignal() hasVisibleSelectSameProfileChanged = pyqtSignal() + selectSameProfileCheckedChanged = pyqtSignal() @pyqtProperty(bool, notify = isPrinterGroupChanged) def isPrinterGroup(self) -> bool: @@ -295,17 +298,19 @@ class WorkspaceDialog(QObject): @pyqtSlot(str) def setMachineToOverride(self, machine_name: str) -> None: + 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 = machine_name) + 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() - self._override_machine = machine_name - @pyqtProperty(bool, notify = isCompatibleMachineChanged) def isCompatibleMachine(self) -> bool: return self._is_compatible_machine @@ -319,6 +324,22 @@ class WorkspaceDialog(QObject): 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 + @pyqtSlot() def closeBackend(self) -> None: """Close the backend: otherwise one could end up with "Slicing...""" diff --git a/plugins/3MFReader/WorkspaceDialog.qml b/plugins/3MFReader/WorkspaceDialog.qml index 45fe7b6989..c7074ce220 100644 --- a/plugins/3MFReader/WorkspaceDialog.qml +++ b/plugins/3MFReader/WorkspaceDialog.qml @@ -120,13 +120,17 @@ UM.Dialog minDropDownWidth: machineSelector.width - buttons: [ + Component + { + id: componentNewPrinter + Cura.SecondaryButton { id: createNewPrinter text: catalog.i18nc("@button", "Create new") fixedWidthMode: true width: parent.width - leftPadding * 1.5 + visible: manager.allowCreateMachine onClicked: { toggleContent() @@ -136,7 +140,9 @@ UM.Dialog manager.setIsNetworkedMachine(false) } } - ] + } + + buttons: manager.allowCreateMachine ? [componentNewPrinter.createObject()] : [] onSelectPrinter: function(machine) { @@ -191,9 +197,12 @@ UM.Dialog { text: catalog.i18nc("@action:checkbox", "Select the same profile") enabled: manager.isCompatibleMachine - onEnabledChanged: checked = enabled + 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 + + checked: manager.selectSameProfileChecked + onCheckedChanged: manager.selectSameProfileChecked = checked } } diff --git a/plugins/3MFWriter/SettingsExportModel.py b/plugins/3MFWriter/SettingsExportModel.py index a4acaf02f7..0c34278067 100644 --- a/plugins/3MFWriter/SettingsExportModel.py +++ b/plugins/3MFWriter/SettingsExportModel.py @@ -105,7 +105,7 @@ class SettingsExportModel(QObject): @staticmethod def _exportSettings(settings_stack): - user_settings_container = settings_stack.getTop() + user_settings_container = settings_stack.userChanges user_keys = user_settings_container.getAllKeys() settings_export = [] From 2d79479a26d25d81ff523e34c346004f969fb12a Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Fri, 2 Feb 2024 16:17:27 +0100 Subject: [PATCH 12/39] Avoid displaying the discard changed dialog CURA-11561 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 3398b2e7d5..57bf4be5bb 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -890,7 +890,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Prepare the machine self._applyChangesToMachine(global_stack, extruder_stack_dict) else: - self._applyUserSettings(global_stack, extruder_stack_dict, user_settings) + # 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...") # Actually change the active machine. @@ -902,6 +903,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # To solve this, we schedule _updateActiveMachine() for later so it will have the latest data. self._updateActiveMachine(global_stack) + if not self._load_profile: + # Now we have switched, apply the user settings + self._applyUserSettings(global_stack, extruder_stack_dict, user_settings) + # Load all the nodes / mesh data of the workspace nodes = self._3mf_mesh_reader.read(file_name) if nodes is None: @@ -1224,9 +1229,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._user_settings_to_apply = None def _applyUserSettings(self, global_stack, extruder_stack_dict, user_settings): - # Clear all first - self._clearMachineSettings(global_stack, extruder_stack_dict) - for stack_name, settings in user_settings.items(): if stack_name == 'global': ThreeMFWorkspaceReader._applyUserSettingsOnStack(global_stack, settings) From 9afe5b46dbaf94d5214df9e8f285dcb39b0f3eb6 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 5 Feb 2024 12:39:56 +0100 Subject: [PATCH 13/39] We now display the global and extruder settings in the dialog CURA-11561 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 54 +++++++++++++-------- plugins/3MFReader/WorkspaceDialog.py | 7 +++ plugins/3MFReader/WorkspaceDialog.qml | 45 ++++++++++++++--- plugins/3MFReader/WorkspaceRow.qml | 20 ++++++-- 4 files changed, 96 insertions(+), 30 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 57bf4be5bb..caf411bc67 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -143,12 +143,14 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._machine_info = None self._load_profile = False + self._user_settings: Dict[str, Dict[str, Any]] = {} def _clearState(self): self._id_mapping = {} self._old_new_materials = {} self._machine_info = None self._load_profile = False + self._user_settings = {} 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. @@ -604,6 +606,36 @@ class ThreeMFWorkspaceReader(WorkspaceReader): package_metadata = self._parse_packages_metadata(archive) missing_package_metadata = self._filter_missing_package_metadata(package_metadata) + # Load the user specifically exported settings + self._dialog.exportedSettingModel.clear() + if is_pcb: + try: + self._user_settings = json.loads(archive.open("Cura/user-settings.json").read().decode("utf-8")) + any_extruder_stack = ExtruderManager.getInstance().getExtruderStack(0) + + for stack_name, settings in self._user_settings.items(): + if stack_name == 'global': + self._dialog.exportedSettingModel.addSettingsFromStack(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 PCB, so notify user of failure. + Logger.log("w", "File %s is not a valid PCB.", file_name) + message = Message( + i18n_catalog.i18nc("@info:error Don't translate the XML tags or !", + "Project file {0} is corrupt: {1}.", + 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. self._dialog.setMachineConflict(machine_conflict) self._dialog.setIsPrinterGroup(is_printer_group) @@ -628,6 +660,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._dialog.setAllowCreatemachine(not is_pcb) self._dialog.show() + # Choosing the initially selected printer in MachineSelector is_networked_machine = False is_abstract_machine = False @@ -790,8 +823,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader): for stack in extruder_stacks: stack.setNextStack(global_stack, connect_signals = False) - user_settings = {} - if self._load_profile: Logger.log("d", "Workspace loading is checking definitions...") # Get all the definition files & check if they exist. If not, add them. @@ -864,23 +895,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._container_registry.addContainer(material_container) Job.yieldThread() QCoreApplication.processEvents() # Ensure that the GUI does not freeze. - else: - Logger.log("d", "Workspace loading user settings...") - try: - user_settings = json.loads(archive.open("Cura/user-settings.json").read().decode("utf-8")) - except KeyError as e: - # If there is no user settings file, it's not a PCB, so notify user of failure. - Logger.log("w", "File %s is not a valid PCB.", file_name) - message = Message( - i18n_catalog.i18nc("@info:error Don't translate the XML tags or !", - "Project file {0} is corrupt: {1}.", - file_name, str(e)), - title=i18n_catalog.i18nc("@info:title", "Can't Open Project File"), - message_type=Message.MessageType.ERROR) - message.show() - self.setWorkspaceName("") - return [], {} - if global_stack: if self._load_profile: @@ -905,7 +919,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if not self._load_profile: # Now we have switched, apply the user settings - self._applyUserSettings(global_stack, extruder_stack_dict, user_settings) + self._applyUserSettings(global_stack, extruder_stack_dict, self._user_settings) # Load all the nodes / mesh data of the workspace nodes = self._3mf_mesh_reader.read(file_name) diff --git a/plugins/3MFReader/WorkspaceDialog.py b/plugins/3MFReader/WorkspaceDialog.py index c0006e21a8..1fafcf59f5 100644 --- a/plugins/3MFReader/WorkspaceDialog.py +++ b/plugins/3MFReader/WorkspaceDialog.py @@ -22,6 +22,8 @@ import time from cura.CuraApplication import CuraApplication +from .SpecificSettingsModel import SpecificSettingsModel + i18n_catalog = i18nCatalog("cura") @@ -75,6 +77,7 @@ class WorkspaceDialog(QObject): 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() qualityChangesConflictChanged = pyqtSignal() @@ -340,6 +343,10 @@ class WorkspaceDialog(QObject): def allowCreateMachine(self): return self._allow_create_machine + @pyqtProperty(QObject, constant = True) + def exportedSettingModel(self): + return self._exported_settings_model + @pyqtSlot() def closeBackend(self) -> None: """Close the backend: otherwise one could end up with "Slicing...""" diff --git a/plugins/3MFReader/WorkspaceDialog.qml b/plugins/3MFReader/WorkspaceDialog.qml index c7074ce220..b6a9d59751 100644 --- a/plugins/3MFReader/WorkspaceDialog.qml +++ b/plugins/3MFReader/WorkspaceDialog.qml @@ -1,12 +1,12 @@ // Copyright (c) 2022 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. -import QtQuick 2.10 +import QtQuick 2.14 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 import QtQuick.Window 2.2 -import UM 1.5 as UM +import UM 1.6 as UM import Cura 1.1 as Cura UM.Dialog @@ -171,35 +171,68 @@ UM.Dialog { leftLabelText: catalog.i18nc("@action:label", "Name") rightLabelText: manager.qualityName + visible: manager.isCompatibleMachine } WorkspaceRow { leftLabelText: catalog.i18nc("@action:label", "Intent") rightLabelText: manager.intentName + visible: manager.isCompatibleMachine } WorkspaceRow { leftLabelText: catalog.i18nc("@action:label", "Not in profile") 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 { 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) - 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") - enabled: manager.isCompatibleMachine 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 + visible: manager.hasVisibleSelectSameProfile && manager.isCompatibleMachine checked: manager.selectSameProfileChecked onCheckedChanged: manager.selectSameProfileChecked = checked diff --git a/plugins/3MFReader/WorkspaceRow.qml b/plugins/3MFReader/WorkspaceRow.qml index 8d9f1f25b3..855b8c18b0 100644 --- a/plugins/3MFReader/WorkspaceRow.qml +++ b/plugins/3MFReader/WorkspaceRow.qml @@ -9,26 +9,38 @@ import QtQuick.Window 2.2 import UM 1.5 as UM import Cura 1.1 as Cura -Row +RowLayout { + id: root + property alias leftLabelText: leftLabel.text property alias rightLabelText: rightLabel.text + property alias buttonText: button.text + signal buttonClicked width: parent.width - height: visible ? childrenRect.height : 0 UM.Label { id: leftLabel text: catalog.i18nc("@action:label", "Type") - width: Math.round(parent.width / 4) + Layout.preferredWidth: Math.round(parent.width / 4) wrapMode: Text.WordWrap } + UM.Label { id: rightLabel text: manager.machineType - width: Math.round(parent.width / 3) wrapMode: Text.WordWrap } + + Cura.TertiaryButton + { + id: button + visible: !text.isEmpty + Layout.maximumHeight: leftLabel.implicitHeight + Layout.fillWidth: true + onClicked: root.buttonClicked() + } } \ No newline at end of file From 2ae990833486e035c8291732e211b32d2ff55799 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 5 Feb 2024 12:58:02 +0100 Subject: [PATCH 14/39] Fix crash CURA-11561 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index caf411bc67..c28880211d 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -699,7 +699,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if self._dialog.getResult() == {}: return WorkspaceReader.PreReadResult.cancelled - self._load_profile = not is_pcb or self._dialog.selectSameProfileChecked + self._load_profile = not is_pcb or (self._dialog.selectSameProfileChecked and self._dialog.isCompatibleMachine) self._resolve_strategies = self._dialog.getResult() # From 63c1eb8990ff3b86337fddb0331bc8ca24ea1092 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Mon, 5 Feb 2024 14:15:43 +0100 Subject: [PATCH 15/39] Rename to Universal Cura Project CURA-11561 --- plugins/3MFReader/ThreeMFReader.py | 8 ++++---- plugins/3MFReader/ThreeMFWorkspaceReader.py | 20 +++++++++---------- plugins/3MFReader/__init__.py | 8 ++++---- plugins/3MFReader/plugin.json | 2 +- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 16 +++++++-------- .../3MFWriter/{PCBDialog.py => UCPDialog.py} | 4 ++-- .../{PCBDialog.qml => UCPDialog.qml} | 6 +++--- plugins/3MFWriter/__init__.py | 12 +++++------ plugins/3MFWriter/plugin.json | 2 +- resources/qml/Menus/FileMenu.qml | 8 ++++---- 10 files changed, 43 insertions(+), 43 deletions(-) rename plugins/3MFWriter/{PCBDialog.py => UCPDialog.py} (94%) rename plugins/3MFWriter/{PCBDialog.qml => UCPDialog.qml} (86%) diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index 9f4a4b197b..13d069f5a7 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -48,13 +48,13 @@ class ThreeMFReader(MeshReader): ) MimeTypeDatabase.addMimeType( MimeType( - name="application/x-pcb", - comment="PCB", - suffixes=["pcb"] + name="application/x-ucp", + comment="UCP", + suffixes=["ucp"] ) ) - self._supported_extensions = [".3mf", ".pcb"] + self._supported_extensions = [".3mf", ".ucp"] self._root = None self._base_name = "" self._unit = None diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index c28880211d..e3056065a8 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -113,7 +113,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): def __init__(self) -> None: super().__init__() - self._supported_extensions = [".3mf", ".pcb"] + self._supported_extensions = [".3mf", ".ucp"] self._dialog = WorkspaceDialog() self._3mf_mesh_reader = None self._container_registry = ContainerRegistry.getInstance() @@ -234,14 +234,14 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._resolve_strategies = {k: None for k in resolve_strategy_keys} containers_found_dict = {k: False for k in resolve_strategy_keys} - # Check whether the file is a PCB, which changes some import options - is_pcb = file_name.endswith('.pcb') + # Check whether the file is a UCP, which changes some import options + is_ucp = file_name.endswith('.ucp') # # Read definition containers # machine_definition_id = None - updatable_machines = None if is_pcb else [] + updatable_machines = None if is_ucp else [] machine_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)] @@ -608,7 +608,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Load the user specifically exported settings self._dialog.exportedSettingModel.clear() - if is_pcb: + 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) @@ -625,8 +625,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): "Extruder {0}", extruder_nr + 1), settings) except KeyError as e: - # If there is no user settings file, it's not a PCB, so notify user of failure. - Logger.log("w", "File %s is not a valid PCB.", file_name) + # 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 or !", "Project file {0} is corrupt: {1}.", @@ -656,8 +656,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._dialog.setVariantType(variant_type_name) self._dialog.setHasObjectsOnPlate(Application.getInstance().platformActivity) self._dialog.setMissingPackagesMetadata(missing_package_metadata) - self._dialog.setHasVisibleSelectSameProfileChanged(is_pcb) - self._dialog.setAllowCreatemachine(not is_pcb) + self._dialog.setHasVisibleSelectSameProfileChanged(is_ucp) + self._dialog.setAllowCreatemachine(not is_ucp) self._dialog.show() @@ -699,7 +699,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if self._dialog.getResult() == {}: return WorkspaceReader.PreReadResult.cancelled - self._load_profile = not is_pcb or (self._dialog.selectSameProfileChecked and self._dialog.isCompatibleMachine) + self._load_profile = not is_ucp or (self._dialog.selectSameProfileChecked and self._dialog.isCompatibleMachine) self._resolve_strategies = self._dialog.getResult() # diff --git a/plugins/3MFReader/__init__.py b/plugins/3MFReader/__init__.py index a07420d2c6..101337f05f 100644 --- a/plugins/3MFReader/__init__.py +++ b/plugins/3MFReader/__init__.py @@ -27,8 +27,8 @@ def getMetaData() -> Dict: "description": catalog.i18nc("@item:inlistbox", "3MF File") }, { - "extension": "pcb", - "description": catalog.i18nc("@item:inlistbox", "PCB File") + "extension": "ucp", + "description": catalog.i18nc("@item:inlistbox", "UCP File") } ] metaData["workspace_reader"] = [ @@ -37,8 +37,8 @@ def getMetaData() -> Dict: "description": catalog.i18nc("@item:inlistbox", "3MF File") }, { - "extension": "pcb", - "description": catalog.i18nc("@item:inlistbox", "PCB File") + "extension": "ucp", + "description": catalog.i18nc("@item:inlistbox", "UCP File") } ] diff --git a/plugins/3MFReader/plugin.json b/plugins/3MFReader/plugin.json index 1611c956d3..010adbb501 100644 --- a/plugins/3MFReader/plugin.json +++ b/plugins/3MFReader/plugin.json @@ -2,7 +2,7 @@ "name": "3MF Reader", "author": "Ultimaker B.V.", "version": "1.0.1", - "description": "Provides support for reading 3MF and PCB files.", + "description": "Provides support for reading 3MF and UCP files.", "api": 8, "i18n-catalog": "cura" } diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index 9715e9ac98..7cdf884709 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -15,7 +15,7 @@ from UM.Workspace.WorkspaceWriter import WorkspaceWriter from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") -from .PCBDialog import PCBDialog +from .UCPDialog import UCPDialog from .ThreeMFWriter import ThreeMFWriter from .SettingsExportModel import SettingsExportModel from .SettingsExportGroup import SettingsExportGroup @@ -35,19 +35,19 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): self._config_dialog = None def _preWrite(self): - is_pcb = False + is_ucp = False if hasattr(self._stream, 'name'): - # This only works with local file, but we don't want remote PCB files yet - is_pcb = self._stream.name.endswith('.pcb') + # This only works with local file, but we don't want remote UCP files yet + is_ucp = self._stream.name.endswith('.ucp') - if is_pcb: - self._config_dialog = PCBDialog() - self._config_dialog.finished.connect(self._onPCBConfigFinished) + if is_ucp: + self._config_dialog = UCPDialog() + self._config_dialog.finished.connect(self._onUCPConfigFinished) self._config_dialog.show() else: self._doWrite() - def _onPCBConfigFinished(self, accepted: bool): + def _onUCPConfigFinished(self, accepted: bool): if accepted: self._export_model = self._config_dialog.getModel() self._doWrite() diff --git a/plugins/3MFWriter/PCBDialog.py b/plugins/3MFWriter/UCPDialog.py similarity index 94% rename from plugins/3MFWriter/PCBDialog.py rename to plugins/3MFWriter/UCPDialog.py index 089fa259ac..fb214aded0 100644 --- a/plugins/3MFWriter/PCBDialog.py +++ b/plugins/3MFWriter/UCPDialog.py @@ -14,14 +14,14 @@ from .SettingsExportModel import SettingsExportModel i18n_catalog = i18nCatalog("cura") -class PCBDialog(QObject): +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, 'PCBDialog.qml') + dialog_path = os.path.join(plugin_path, 'UCPDialog.qml') self._model = SettingsExportModel() self._view = CuraApplication.getInstance().createQmlComponent(dialog_path, {"manager": self, diff --git a/plugins/3MFWriter/PCBDialog.qml b/plugins/3MFWriter/UCPDialog.qml similarity index 86% rename from plugins/3MFWriter/PCBDialog.qml rename to plugins/3MFWriter/UCPDialog.qml index b65520961b..88552cc292 100644 --- a/plugins/3MFWriter/PCBDialog.qml +++ b/plugins/3MFWriter/UCPDialog.qml @@ -12,7 +12,7 @@ import Cura 1.1 as Cura UM.Dialog { id: exportDialog - title: catalog.i18nc("@title:window", "Export pre-configured build batch") + title: catalog.i18nc("@title:window", "Export Universal Cura Project") margin: UM.Theme.getSize("default_margin").width minimumWidth: UM.Theme.getSize("modal_window_minimum").width @@ -39,14 +39,14 @@ UM.Dialog UM.Label { id: titleLabel - text: catalog.i18nc("@action:title", "Summary - Pre-configured build batch") + 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 build batch, 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.") + 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 diff --git a/plugins/3MFWriter/__init__.py b/plugins/3MFWriter/__init__.py index e0d4037603..40fd42b199 100644 --- a/plugins/3MFWriter/__init__.py +++ b/plugins/3MFWriter/__init__.py @@ -34,9 +34,9 @@ def getMetaData(): "mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode }, { - "extension": "pcb", - "description": i18n_catalog.i18nc("@item:inlistbox", "PCB file"), - "mime_type": "application/x-pcb", + "extension": "ucp", + "description": i18n_catalog.i18nc("@item:inlistbox", "Universal Cura Project"), + "mime_type": "application/x-ucp", "mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode } ] @@ -50,9 +50,9 @@ def getMetaData(): "mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode }, { - "extension": "pcb", - "description": i18n_catalog.i18nc("@item:inlistbox", "Pre-Configured Batch file"), - "mime_type": "application/x-pcb", + "extension": "ucp", + "description": i18n_catalog.i18nc("@item:inlistbox", "Universal Cura Project"), + "mime_type": "application/x-ucp", "mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode } ] diff --git a/plugins/3MFWriter/plugin.json b/plugins/3MFWriter/plugin.json index be6d50267c..254384dc25 100644 --- a/plugins/3MFWriter/plugin.json +++ b/plugins/3MFWriter/plugin.json @@ -2,7 +2,7 @@ "name": "3MF Writer", "author": "Ultimaker B.V.", "version": "1.0.1", - "description": "Provides support for writing 3MF and PCB files.", + "description": "Provides support for writing 3MF and UCP files.", "api": 8, "i18n-catalog": "cura" } diff --git a/resources/qml/Menus/FileMenu.qml b/resources/qml/Menus/FileMenu.qml index a6fb339faf..4f7734cb11 100644 --- a/resources/qml/Menus/FileMenu.qml +++ b/resources/qml/Menus/FileMenu.qml @@ -72,15 +72,15 @@ Cura.Menu Cura.MenuItem { - id: savePCBMenu - text: catalog.i18nc("@title:menu menubar:file", "&Save PCB Project...") + id: saveUCPMenu + text: catalog.i18nc("@title:menu menubar:file", "&Save Universal Cura Project...") enabled: UM.WorkspaceFileHandler.enabled onTriggered: { var args = { "filter_by_machine": false, "file_type": "workspace", - "preferred_mimetypes": "application/x-pcb", - "limit_mimetypes": "application/x-pcb"}; + "preferred_mimetypes": "application/x-ucp", + "limit_mimetypes": "application/x-ucp"}; UM.OutputDeviceManager.requestWriteToDevice("local_file", PrintInformation.jobName, args) } } From 31f3d6161d764e66c469f9c1d3d6de5cff1c8b76 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Tue, 6 Feb 2024 13:19:42 +0100 Subject: [PATCH 16/39] Add missing file CURA-11561 --- plugins/3MFReader/SpecificSettingsModel.py | 38 ++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 plugins/3MFReader/SpecificSettingsModel.py diff --git a/plugins/3MFReader/SpecificSettingsModel.py b/plugins/3MFReader/SpecificSettingsModel.py new file mode 100644 index 0000000000..fd5719d6b3 --- /dev/null +++ b/plugins/3MFReader/SpecificSettingsModel.py @@ -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 + }) From 671698d1a3efe77043501b462507d8f8a2ed7429 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Wed, 7 Feb 2024 08:49:05 +0100 Subject: [PATCH 17/39] Restore useless import version bump CURA-11561 --- plugins/3MFReader/WorkspaceDialog.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/3MFReader/WorkspaceDialog.qml b/plugins/3MFReader/WorkspaceDialog.qml index b6a9d59751..8d06b32e14 100644 --- a/plugins/3MFReader/WorkspaceDialog.qml +++ b/plugins/3MFReader/WorkspaceDialog.qml @@ -1,7 +1,7 @@ // Copyright (c) 2022 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. -import QtQuick 2.14 +import QtQuick 2.10 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.3 import QtQuick.Window 2.2 From 93cb35859959394ac67a2fd52c2c04825c8e5c8b Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Fri, 9 Feb 2024 07:43:48 +0100 Subject: [PATCH 18/39] Add FIXMEs where big changes are required CURA-11561 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 1 + plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index e3056065a8..92a62ddca2 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -235,6 +235,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): containers_found_dict = {k: False for k in resolve_strategy_keys} # Check whether the file is a UCP, which changes some import options + #FIXME Instead of this, we should just check for the presence of the user-settings file, whatever the extension is_ucp = file_name.endswith('.ucp') # diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index 7cdf884709..d0a843af69 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -34,6 +34,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): self._mode = None self._config_dialog = None + #FIXME We should have proper preWrite/write methods like the readers have a preRead/read, and have them called by the global process def _preWrite(self): is_ucp = False if hasattr(self._stream, 'name'): @@ -150,6 +151,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): self._success = True + #FIXME We should somehow give the information of the file type so that we know what to write, like the mode but for other files types (give mimetype ?) def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode): self._success = False self._export_model = None From 345ddc40bb3dd32c2a88990e99073702c4e4e721 Mon Sep 17 00:00:00 2001 From: Erwan MATHIEU Date: Fri, 9 Feb 2024 11:54:49 +0100 Subject: [PATCH 19/39] Fix file opening failure CURA-11561 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 92a62ddca2..d96de5b324 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -613,10 +613,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader): 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(global_stack, i18n_catalog.i18nc("@label", "Global"), settings) + 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: From b641741e49a72dc39110dc8cf4dacf6b315fe62f Mon Sep 17 00:00:00 2001 From: "saumya.jain" Date: Fri, 16 Feb 2024 14:11:07 +0100 Subject: [PATCH 20/39] Added preference to show UFP saving dialog Changes saving to .ucp to .3mf --- cura/CuraApplication.py | 2 ++ plugins/3MFReader/ThreeMFReader.py | 4 ++-- plugins/3MFReader/ThreeMFWorkspaceReader.py | 4 ++-- plugins/3MFReader/plugin.json | 2 +- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 2 +- plugins/3MFWriter/UCPDialog.qml | 25 ++++++++++++++++++++- plugins/3MFWriter/__init__.py | 4 ++-- resources/qml/Preferences/GeneralPage.qml | 14 ++++++++++++ 8 files changed, 48 insertions(+), 9 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 20e54fa57c..7b588c836a 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -601,7 +601,9 @@ class CuraApplication(QtApplication): preferences.addPreference("mesh/scale_to_fit", False) preferences.addPreference("mesh/scale_tiny_meshes", 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_ucp_project_save", False) preferences.addPreference("cura/choice_on_profile_override", "always_ask") preferences.addPreference("cura/choice_on_open_project", "always_ask") preferences.addPreference("cura/use_multi_build_plate", False) diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index a715d990eb..c7a15e043e 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -50,11 +50,11 @@ class ThreeMFReader(MeshReader): MimeType( name="application/x-ucp", comment="UCP", - suffixes=["ucp"] + suffixes=["3mf"] ) ) - self._supported_extensions = [".3mf", ".ucp"] + self._supported_extensions = [".3mf"] self._root = None self._base_name = "" self._unit = None diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index d96de5b324..c9692599e5 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -113,7 +113,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): def __init__(self) -> None: super().__init__() - self._supported_extensions = [".3mf", ".ucp"] + self._supported_extensions = [".3mf"] self._dialog = WorkspaceDialog() self._3mf_mesh_reader = None self._container_registry = ContainerRegistry.getInstance() @@ -236,7 +236,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Check whether the file is a UCP, which changes some import options #FIXME Instead of this, we should just check for the presence of the user-settings file, whatever the extension - is_ucp = file_name.endswith('.ucp') + is_ucp = file_name.endswith('.3mf') # # Read definition containers diff --git a/plugins/3MFReader/plugin.json b/plugins/3MFReader/plugin.json index 010adbb501..bf0bc05364 100644 --- a/plugins/3MFReader/plugin.json +++ b/plugins/3MFReader/plugin.json @@ -2,7 +2,7 @@ "name": "3MF Reader", "author": "Ultimaker B.V.", "version": "1.0.1", - "description": "Provides support for reading 3MF and UCP files.", + "description": "Provides support for reading 3MF files.", "api": 8, "i18n-catalog": "cura" } diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index d0a843af69..c67da24569 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -39,7 +39,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): is_ucp = False if hasattr(self._stream, 'name'): # This only works with local file, but we don't want remote UCP files yet - is_ucp = self._stream.name.endswith('.ucp') + is_ucp = self._stream.name.endswith('.3mf') if is_ucp: self._config_dialog = UCPDialog() diff --git a/plugins/3MFWriter/UCPDialog.qml b/plugins/3MFWriter/UCPDialog.qml index 88552cc292..7eaf57c96b 100644 --- a/plugins/3MFWriter/UCPDialog.qml +++ b/plugins/3MFWriter/UCPDialog.qml @@ -19,6 +19,21 @@ UM.Dialog minimumHeight: UM.Theme.getSize("modal_window_minimum").height backgroundColor: UM.Theme.getColor("detail_background") + property bool dontShowAgain: true + + function storeDontShowAgain() + { + UM.Preferences.setValue("cura/dialog_on_ucp_project_save", !dontShowAgainCheckbox.checked) + UM.Preferences.setValue("asked_dialog_on_ucp_project_save", true) + } + + 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 { @@ -75,7 +90,15 @@ UM.Dialog 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 diff --git a/plugins/3MFWriter/__init__.py b/plugins/3MFWriter/__init__.py index 40fd42b199..1cecf4c3f8 100644 --- a/plugins/3MFWriter/__init__.py +++ b/plugins/3MFWriter/__init__.py @@ -34,7 +34,7 @@ def getMetaData(): "mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode }, { - "extension": "ucp", + "extension": "3mf", "description": i18n_catalog.i18nc("@item:inlistbox", "Universal Cura Project"), "mime_type": "application/x-ucp", "mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode @@ -50,7 +50,7 @@ def getMetaData(): "mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode }, { - "extension": "ucp", + "extension": "3mf", "description": i18n_catalog.i18nc("@item:inlistbox", "Universal Cura Project"), "mime_type": "application/x-ucp", "mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode diff --git a/resources/qml/Preferences/GeneralPage.qml b/resources/qml/Preferences/GeneralPage.qml index 69607a3f6b..0ca905cf85 100644 --- a/resources/qml/Preferences/GeneralPage.qml +++ b/resources/qml/Preferences/GeneralPage.qml @@ -735,6 +735,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 { width: childrenRect.width From ec871782c77b73ca53c30daa0fd6174311bc4cc4 Mon Sep 17 00:00:00 2001 From: Saumya Jain Date: Tue, 20 Feb 2024 10:47:27 +0100 Subject: [PATCH 21/39] PAP adding save dialog before filesave window CURA-11403 --- cura/CuraApplication.py | 10 +++++ plugins/3MFReader/ThreeMFWorkspaceReader.py | 4 +- plugins/3MFReader/__init__.py | 8 ---- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 45 ++++++--------------- plugins/3MFWriter/ThreeMFWriter.py | 8 +++- plugins/3MFWriter/UCPDialog.py | 12 ++++++ resources/qml/Menus/FileMenu.qml | 8 +--- 7 files changed, 46 insertions(+), 49 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 7b588c836a..1af8380fd6 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1143,6 +1143,16 @@ class CuraApplication(QtApplication): self._build_plate_model = BuildPlateModel(self) 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: if self._cura_scene_controller is None: self._cura_scene_controller = CuraSceneController.createCuraSceneController() diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index c9692599e5..04885a961b 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -236,8 +236,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Check whether the file is a UCP, which changes some import options #FIXME Instead of this, we should just check for the presence of the user-settings file, whatever the extension - is_ucp = file_name.endswith('.3mf') - + if file_name.endswith('.3mf'): + is_ucp = True # # Read definition containers # diff --git a/plugins/3MFReader/__init__.py b/plugins/3MFReader/__init__.py index 101337f05f..5e2b68fce0 100644 --- a/plugins/3MFReader/__init__.py +++ b/plugins/3MFReader/__init__.py @@ -25,20 +25,12 @@ def getMetaData() -> Dict: { "extension": "3mf", "description": catalog.i18nc("@item:inlistbox", "3MF File") - }, - { - "extension": "ucp", - "description": catalog.i18nc("@item:inlistbox", "UCP File") } ] metaData["workspace_reader"] = [ { "extension": workspace_extension, "description": catalog.i18nc("@item:inlistbox", "3MF File") - }, - { - "extension": "ucp", - "description": catalog.i18nc("@item:inlistbox", "UCP File") } ] diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index c67da24569..745627ed93 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -15,7 +15,6 @@ from UM.Workspace.WorkspaceWriter import WorkspaceWriter from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") -from .UCPDialog import UCPDialog from .ThreeMFWriter import ThreeMFWriter from .SettingsExportModel import SettingsExportModel from .SettingsExportGroup import SettingsExportGroup @@ -32,32 +31,12 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): self._stream = None self._nodes = None self._mode = None - self._config_dialog = None + self._is_ucp = False - #FIXME We should have proper preWrite/write methods like the readers have a preRead/read, and have them called by the global process - def _preWrite(self): - is_ucp = False - if hasattr(self._stream, 'name'): - # This only works with local file, but we don't want remote UCP files yet - is_ucp = self._stream.name.endswith('.3mf') - if is_ucp: - self._config_dialog = UCPDialog() - self._config_dialog.finished.connect(self._onUCPConfigFinished) - self._config_dialog.show() - else: - self._doWrite() - - def _onUCPConfigFinished(self, accepted: bool): - if accepted: - self._export_model = self._config_dialog.getModel() - self._doWrite() - else: - self._main_thread_lock.release() - - def _doWrite(self): - self._write() - self._main_thread_lock.release() + def setExportModel(self, model): + if self._export_model != model: + self._export_model = model def _write(self): application = Application.getInstance() @@ -153,19 +132,21 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): #FIXME We should somehow give the information of the file type so that we know what to write, like the mode but for other files types (give mimetype ?) def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode): + print("Application.getInstance().getPreferences().getValue(\"local_file/last_used_type\")", Application.getInstance().getPreferences().getValue("local_file/last_used_type")) + self._success = False self._export_model = None self._stream = stream self._nodes = nodes self._mode = mode self._config_dialog = None - - self._main_thread_lock.acquire() - # Export is done in main thread because it may require a few asynchronous configuration steps - Application.getInstance().callLater(self._preWrite) - self._main_thread_lock.acquire() # Block until lock has been released, meaning the config+write is over - - self._main_thread_lock.release() + # + # self._main_thread_lock.acquire() + # # Export is done in main thread because it may require a few asynchronous configuration steps + Application.getInstance().callLater(self._write()) + # self._main_thread_lock.acquire() # Block until lock has been released, meaning the config+write is over + # + # self._main_thread_lock.release() self._export_model = None self._stream = None diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index 2caa71353c..e60aae8dd9 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -23,7 +23,7 @@ from cura.Snapshot import Snapshot from PyQt6.QtCore import QBuffer import pySavitar as Savitar - +from .UCPDialog import UCPDialog import numpy import datetime @@ -61,6 +61,7 @@ class ThreeMFWriter(MeshWriter): self._unit_matrix_string = ThreeMFWriter._convertMatrixToString(Matrix()) self._archive: Optional[zipfile.ZipFile] = None self._store_archive = False + self._is_ucp = False @staticmethod def _convertMatrixToString(matrix): @@ -433,3 +434,8 @@ class ThreeMFWriter(MeshWriter): extra_settings[group.category_details] = exported_model_settings return extra_settings + + def exportUcp(self): + self._is_ucp = True + self._config_dialog = UCPDialog() + self._config_dialog.show() diff --git a/plugins/3MFWriter/UCPDialog.py b/plugins/3MFWriter/UCPDialog.py index fb214aded0..ecffb64338 100644 --- a/plugins/3MFWriter/UCPDialog.py +++ b/plugins/3MFWriter/UCPDialog.py @@ -4,6 +4,8 @@ import os from PyQt6.QtCore import pyqtSignal, QObject + +import UM from UM.FlameProfiler import pyqtSlot from UM.i18n import i18nCatalog @@ -44,6 +46,16 @@ class UCPDialog(QObject): @pyqtSlot() def _onAccepted(self): self._accepted = True + mesh_writer = CuraApplication.getInstance().getMeshFileHandler().getWriter("3MFWriter") + mesh_writer.custom_data = "My custom data" + + device = CuraApplication.getInstance().getOutputDeviceManager().getOutputDevice("local_file") + file_handler = UM.Qt.QtApplication.QtApplication.getInstance().getWorkspaceFileHandler() + nodes = [CuraApplication.getInstance().getController().getScene().getRoot()] + device.requestWrite(nodes, "test.3mf", ["application/x-ucp"], file_handler, + preferred_mimetype_list="application/x-ucp") + #TODO: update _export_model in threeMFWorkspacewriter and set is_ucp is true + # = self._config_dialog.getModel() self._onFinished() @pyqtSlot() diff --git a/resources/qml/Menus/FileMenu.qml b/resources/qml/Menus/FileMenu.qml index 4f7734cb11..bc0d1e6aef 100644 --- a/resources/qml/Menus/FileMenu.qml +++ b/resources/qml/Menus/FileMenu.qml @@ -74,14 +74,10 @@ Cura.Menu { id: saveUCPMenu text: catalog.i18nc("@title:menu menubar:file", "&Save Universal Cura Project...") - enabled: UM.WorkspaceFileHandler.enabled + enabled: UM.WorkspaceFileHandler.enabled && CuraApplication.getPackageManager().allEnabledPackages.includes("3MFWriter") onTriggered: { - var args = { "filter_by_machine": false, - "file_type": "workspace", - "preferred_mimetypes": "application/x-ucp", - "limit_mimetypes": "application/x-ucp"}; - UM.OutputDeviceManager.requestWriteToDevice("local_file", PrintInformation.jobName, args) + CuraApplication.exportUcp() } } From efd6284f6e591564f1bc2a5e0e0cc5457730d51f Mon Sep 17 00:00:00 2001 From: "c.lamboo" Date: Tue, 20 Feb 2024 13:47:51 +0100 Subject: [PATCH 22/39] Make ucp model available in workspace writer CURA-11403 --- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 6 ++-- plugins/3MFWriter/UCPDialog.py | 35 +++++++++++++++------ 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index 745627ed93..16b05b30ec 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -27,7 +27,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): super().__init__() self._main_thread_lock = Lock() self._success = False - self._export_model = None + self._ucp_model = None self._stream = None self._nodes = None self._mode = None @@ -35,8 +35,8 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): def setExportModel(self, model): - if self._export_model != model: - self._export_model = model + if self._ucp_model != model: + self._ucp_model = model def _write(self): application = Application.getInstance() diff --git a/plugins/3MFWriter/UCPDialog.py b/plugins/3MFWriter/UCPDialog.py index ecffb64338..1100ddc7a9 100644 --- a/plugins/3MFWriter/UCPDialog.py +++ b/plugins/3MFWriter/UCPDialog.py @@ -7,6 +7,7 @@ from PyQt6.QtCore import pyqtSignal, QObject import UM from UM.FlameProfiler import pyqtSlot +from UM.Workspace.WorkspaceWriter import WorkspaceWriter from UM.i18n import i18nCatalog from cura.CuraApplication import CuraApplication @@ -45,24 +46,38 @@ class UCPDialog(QObject): @pyqtSlot() def _onAccepted(self): - self._accepted = True - mesh_writer = CuraApplication.getInstance().getMeshFileHandler().getWriter("3MFWriter") - mesh_writer.custom_data = "My custom data" + application = CuraApplication.getInstance() + workspace_handler = application.getInstance().getWorkspaceFileHandler() - device = CuraApplication.getInstance().getOutputDeviceManager().getOutputDevice("local_file") - file_handler = UM.Qt.QtApplication.QtApplication.getInstance().getWorkspaceFileHandler() - nodes = [CuraApplication.getInstance().getController().getScene().getRoot()] - device.requestWrite(nodes, "test.3mf", ["application/x-ucp"], file_handler, + # 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(self._onRejected) + device.writeSuccess.connect(self._onSuccess) + device.writeFinished.connect(self._onFinished) + + device.requestWrite(nodes, application.getPrintInformation().jobName, ["application/x-ucp"], workspace_handler, preferred_mimetype_list="application/x-ucp") - #TODO: update _export_model in threeMFWorkspacewriter and set is_ucp is true - # = self._config_dialog.getModel() - self._onFinished() @pyqtSlot() def _onRejected(self): self._onFinished() + def _onSuccess(self): + self._accepted = True + self._onFinished() + def _onFinished(self): if not self._finished: # Make sure we don't send the finished signal twice, whatever happens 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) From bd52c91c94fbe58056be8f7923912d76b6ba7690 Mon Sep 17 00:00:00 2001 From: Saumya Jain Date: Tue, 20 Feb 2024 14:41:48 +0100 Subject: [PATCH 23/39] adding lambda for number of args CURA-11403 --- plugins/3MFWriter/UCPDialog.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/3MFWriter/UCPDialog.py b/plugins/3MFWriter/UCPDialog.py index 1100ddc7a9..1ac435c809 100644 --- a/plugins/3MFWriter/UCPDialog.py +++ b/plugins/3MFWriter/UCPDialog.py @@ -57,9 +57,9 @@ class UCPDialog(QObject): device = application.getOutputDeviceManager().getOutputDevice("local_file") nodes = [application.getController().getScene().getRoot()] - device.writeError.connect(self._onRejected) - device.writeSuccess.connect(self._onSuccess) - device.writeFinished.connect(self._onFinished) + device.writeError.connect(lambda: self._onRejected()) + device.writeSuccess.connect(lambda: self._onSuccess()) + device.writeFinished.connect(lambda: self._onFinished()) device.requestWrite(nodes, application.getPrintInformation().jobName, ["application/x-ucp"], workspace_handler, preferred_mimetype_list="application/x-ucp") From f61991e261d963b1d688c3d9c908e165c0136ec7 Mon Sep 17 00:00:00 2001 From: "c.lamboo" Date: Tue, 20 Feb 2024 15:04:44 +0100 Subject: [PATCH 24/39] Untangle write function CURA-11403 --- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 50 +++++---------------- 1 file changed, 10 insertions(+), 40 deletions(-) diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index 16b05b30ec..8e20bdb411 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -26,11 +26,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): def __init__(self): super().__init__() self._main_thread_lock = Lock() - self._success = False self._ucp_model = None - self._stream = None - self._nodes = None - self._mode = None self._is_ucp = False @@ -38,7 +34,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): if self._ucp_model != model: self._ucp_model = model - def _write(self): + def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode): application = Application.getInstance() machine_manager = application.getMachineManager() @@ -47,24 +43,24 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): if not mesh_writer: # We need to have the 3mf mesh writer, otherwise we can't save the entire workspace self.setInformation(catalog.i18nc("@error:zip", "3MF Writer plug-in is corrupt.")) Logger.error("3MF Writer class is unavailable. Can't write workspace.") - return + return False global_stack = machine_manager.activeMachine if global_stack is None: 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.") - return + return False # 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) - if not mesh_writer.write(self._stream, self._nodes, self._mode, self._export_model): + if not mesh_writer.write(stream, nodes, mode, self._ucp_model): self.setInformation(mesh_writer.getInformation()) - return + return False archive = mesh_writer.getArchive() if archive is None: # This happens if there was no mesh data to write. - archive = zipfile.ZipFile(self._stream, "w", compression=zipfile.ZIP_DEFLATED) + archive = zipfile.ZipFile(stream, "w", compression=zipfile.ZIP_DEFLATED) try: # Add global container stack data to the archive. @@ -81,13 +77,13 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): self._writeContainerToArchive(container, archive) # Write user settings data - if self._export_model is not None: - user_settings_data = self._getUserSettings(self._export_model) + 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: self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here.")) Logger.error("No permission to write workspace to this stream.") - return + return False # Write preferences to archive original_preferences = Application.getInstance().getPreferences() # Copy only the preferences that we use to the workspace. @@ -128,33 +124,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): return mesh_writer.setStoreArchive(False) - self._success = True - - #FIXME We should somehow give the information of the file type so that we know what to write, like the mode but for other files types (give mimetype ?) - def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode): - print("Application.getInstance().getPreferences().getValue(\"local_file/last_used_type\")", Application.getInstance().getPreferences().getValue("local_file/last_used_type")) - - self._success = False - self._export_model = None - self._stream = stream - self._nodes = nodes - self._mode = mode - self._config_dialog = None - # - # self._main_thread_lock.acquire() - # # Export is done in main thread because it may require a few asynchronous configuration steps - Application.getInstance().callLater(self._write()) - # self._main_thread_lock.acquire() # Block until lock has been released, meaning the config+write is over - # - # self._main_thread_lock.release() - - self._export_model = None - self._stream = None - self._nodes = None - self._mode = None - self._config_dialog = None - - return self._success + return True @staticmethod def _writePluginMetadataToArchive(archive: zipfile.ZipFile) -> None: From a463e5178838cfd92855c33334f4a80244bc5e70 Mon Sep 17 00:00:00 2001 From: "c.lamboo" Date: Tue, 20 Feb 2024 15:07:34 +0100 Subject: [PATCH 25/39] Remove unneeded thread lock CURA-11403 --- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index 8e20bdb411..ddff454516 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -25,11 +25,9 @@ USER_SETTINGS_PATH = "Cura/user-settings.json" class ThreeMFWorkspaceWriter(WorkspaceWriter): def __init__(self): super().__init__() - self._main_thread_lock = Lock() self._ucp_model = None self._is_ucp = False - def setExportModel(self, model): if self._ucp_model != model: self._ucp_model = model From e0754092437f0ed78c86633cbeeeac630db48ac0 Mon Sep 17 00:00:00 2001 From: "c.lamboo" Date: Tue, 20 Feb 2024 15:07:48 +0100 Subject: [PATCH 26/39] Return `False` in case of exception CURA-11403 --- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index ddff454516..eb0ecf5ed5 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -115,11 +115,11 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): except PermissionError: self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here.")) Logger.error("No permission to write workspace to this stream.") - return + return False except EnvironmentError as e: self.setInformation(catalog.i18nc("@error:zip", str(e))) Logger.error("EnvironmentError when writing workspace to this stream: {err}".format(err=str(e))) - return + return False mesh_writer.setStoreArchive(False) return True From 6bbdd543426b9a80c14dc2d6cb67b7fed9a8b594 Mon Sep 17 00:00:00 2001 From: "c.lamboo" Date: Tue, 20 Feb 2024 16:17:39 +0100 Subject: [PATCH 27/39] Remove unused `_is_ucp` CURA-11403 --- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 7 ++++--- plugins/3MFWriter/ThreeMFWriter.py | 2 -- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index eb0ecf5ed5..cff938788b 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -1,6 +1,8 @@ # Copyright (c) 2020 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from typing import Optional + import configparser from io import StringIO from threading import Lock @@ -25,10 +27,9 @@ USER_SETTINGS_PATH = "Cura/user-settings.json" class ThreeMFWorkspaceWriter(WorkspaceWriter): def __init__(self): super().__init__() - self._ucp_model = None - self._is_ucp = False + self._ucp_model: Optional[SettingsExportModel] = None - def setExportModel(self, model): + def setExportModel(self, model: SettingsExportModel) -> None: if self._ucp_model != model: self._ucp_model = model diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index e60aae8dd9..6e3dc86890 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -61,7 +61,6 @@ class ThreeMFWriter(MeshWriter): self._unit_matrix_string = ThreeMFWriter._convertMatrixToString(Matrix()) self._archive: Optional[zipfile.ZipFile] = None self._store_archive = False - self._is_ucp = False @staticmethod def _convertMatrixToString(matrix): @@ -436,6 +435,5 @@ class ThreeMFWriter(MeshWriter): return extra_settings def exportUcp(self): - self._is_ucp = True self._config_dialog = UCPDialog() self._config_dialog.show() From 11a4588546c05209e407c89a684bc118ed015a59 Mon Sep 17 00:00:00 2001 From: "c.lamboo" Date: Tue, 20 Feb 2024 16:27:47 +0100 Subject: [PATCH 28/39] Properly handle exceptions CURA-11403 --- plugins/3MFWriter/UCPDialog.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/plugins/3MFWriter/UCPDialog.py b/plugins/3MFWriter/UCPDialog.py index 1ac435c809..355c70602c 100644 --- a/plugins/3MFWriter/UCPDialog.py +++ b/plugins/3MFWriter/UCPDialog.py @@ -7,8 +7,11 @@ 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 @@ -61,8 +64,22 @@ class UCPDialog(QObject): device.writeSuccess.connect(lambda: self._onSuccess()) device.writeFinished.connect(lambda: self._onFinished()) - device.requestWrite(nodes, application.getPrintInformation().jobName, ["application/x-ucp"], workspace_handler, + file_name = application.getPrintInformation().jobName + + try: + device.requestWrite(nodes, file_name, ["application/x-ucp"], workspace_handler, preferred_mimetype_list="application/x-ucp") + 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() @pyqtSlot() def _onRejected(self): From ce3baa15e2b196fdaeb223a434c6fa6fc395d8cb Mon Sep 17 00:00:00 2001 From: Saumya Jain Date: Tue, 20 Feb 2024 16:56:05 +0100 Subject: [PATCH 29/39] file name is coming from CuraApplication Instance CURA-11403 --- plugins/3MFWriter/UCPDialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/3MFWriter/UCPDialog.py b/plugins/3MFWriter/UCPDialog.py index 355c70602c..20b3a37909 100644 --- a/plugins/3MFWriter/UCPDialog.py +++ b/plugins/3MFWriter/UCPDialog.py @@ -64,7 +64,7 @@ class UCPDialog(QObject): device.writeSuccess.connect(lambda: self._onSuccess()) device.writeFinished.connect(lambda: self._onFinished()) - file_name = application.getPrintInformation().jobName + file_name = CuraApplication.getInstance().getPrintInformation().jobName try: device.requestWrite(nodes, file_name, ["application/x-ucp"], workspace_handler, From d2566d72ff3d4db3270246c9a2c04c6dd383ce73 Mon Sep 17 00:00:00 2001 From: Saumya Jain Date: Tue, 20 Feb 2024 16:56:34 +0100 Subject: [PATCH 30/39] for preference to open dialo while saving ucp CURA-11403 --- plugins/3MFWriter/UCPDialog.qml | 12 +++++++++--- resources/qml/Dialogs/WorkspaceSummaryDialog.qml | 2 +- resources/qml/Menus/FileMenu.qml | 13 ++++++++++++- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/plugins/3MFWriter/UCPDialog.qml b/plugins/3MFWriter/UCPDialog.qml index 7eaf57c96b..3a0e6bf842 100644 --- a/plugins/3MFWriter/UCPDialog.qml +++ b/plugins/3MFWriter/UCPDialog.qml @@ -19,12 +19,12 @@ UM.Dialog minimumHeight: UM.Theme.getSize("modal_window_minimum").height backgroundColor: UM.Theme.getColor("detail_background") - property bool dontShowAgain: true + property bool dontShowAgain: false function storeDontShowAgain() { UM.Preferences.setValue("cura/dialog_on_ucp_project_save", !dontShowAgainCheckbox.checked) - UM.Preferences.setValue("asked_dialog_on_ucp_project_save", true) + UM.Preferences.setValue("cura/asked_dialog_on_ucp_project_save", false) } onVisibleChanged: @@ -115,5 +115,11 @@ UM.Dialog buttonSpacing: UM.Theme.getSize("wide_margin").width - onClosing: manager.notifyClosed() + onClosing: + { + storeDontShowAgain() + manager.notifyClosed() + } + onRejected: storeDontShowAgain() + onAccepted: storeDontShowAgain() } diff --git a/resources/qml/Dialogs/WorkspaceSummaryDialog.qml b/resources/qml/Dialogs/WorkspaceSummaryDialog.qml index a174959807..1eca2f395c 100644 --- a/resources/qml/Dialogs/WorkspaceSummaryDialog.qml +++ b/resources/qml/Dialogs/WorkspaceSummaryDialog.qml @@ -25,7 +25,7 @@ UM.Dialog function storeDontShowAgain() { 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() diff --git a/resources/qml/Menus/FileMenu.qml b/resources/qml/Menus/FileMenu.qml index bc0d1e6aef..3f8c0daac0 100644 --- a/resources/qml/Menus/FileMenu.qml +++ b/resources/qml/Menus/FileMenu.qml @@ -77,7 +77,18 @@ Cura.Menu enabled: UM.WorkspaceFileHandler.enabled && CuraApplication.getPackageManager().allEnabledPackages.includes("3MFWriter") onTriggered: { - CuraApplication.exportUcp() + if(UM.Preferences.getValue("cura/dialog_on_ucp_project_save")) + { + CuraApplication.exportUcp() + } + else + { + var args = { "filter_by_machine": false, + "file_type": "workspace", + "preferred_mimetypes": "application/x-ucp", + "limit_mimetypes": ["application/x-ucp"]}; + UM.OutputDeviceManager.requestWriteToDevice("local_file", PrintInformation.jobName, args) + } } } From 942589d3a21ee7677ef9062b26ea9500bf73a798 Mon Sep 17 00:00:00 2001 From: Saumya Jain Date: Tue, 20 Feb 2024 17:05:23 +0100 Subject: [PATCH 31/39] filename is basename without the printer info CURA-11403 --- plugins/3MFWriter/UCPDialog.py | 2 +- resources/qml/Menus/FileMenu.qml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/3MFWriter/UCPDialog.py b/plugins/3MFWriter/UCPDialog.py index 20b3a37909..8e62666818 100644 --- a/plugins/3MFWriter/UCPDialog.py +++ b/plugins/3MFWriter/UCPDialog.py @@ -64,7 +64,7 @@ class UCPDialog(QObject): device.writeSuccess.connect(lambda: self._onSuccess()) device.writeFinished.connect(lambda: self._onFinished()) - file_name = CuraApplication.getInstance().getPrintInformation().jobName + file_name = CuraApplication.getInstance().getPrintInformation().baseName try: device.requestWrite(nodes, file_name, ["application/x-ucp"], workspace_handler, diff --git a/resources/qml/Menus/FileMenu.qml b/resources/qml/Menus/FileMenu.qml index 3f8c0daac0..7af21182cd 100644 --- a/resources/qml/Menus/FileMenu.qml +++ b/resources/qml/Menus/FileMenu.qml @@ -87,7 +87,7 @@ Cura.Menu "file_type": "workspace", "preferred_mimetypes": "application/x-ucp", "limit_mimetypes": ["application/x-ucp"]}; - UM.OutputDeviceManager.requestWriteToDevice("local_file", PrintInformation.jobName, args) + UM.OutputDeviceManager.requestWriteToDevice("local_file", PrintInformation.baseName, args) } } } From 08b70252a4ad3fb8af11d9d70d037fc2a990b5a2 Mon Sep 17 00:00:00 2001 From: "c.lamboo" Date: Wed, 21 Feb 2024 11:23:05 +0100 Subject: [PATCH 32/39] Remove duplicated mime type CURA-11403 --- plugins/3MFReader/ThreeMFReader.py | 9 +------- plugins/3MFWriter/UCPDialog.py | 35 ++++++++++++++++++++---------- plugins/3MFWriter/__init__.py | 12 ---------- 3 files changed, 25 insertions(+), 31 deletions(-) diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index c7a15e043e..ac94282136 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -41,18 +41,11 @@ class ThreeMFReader(MeshReader): MimeTypeDatabase.addMimeType( MimeType( - name = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml", + name="application/vnd.ms-package.3dmanufacturing-3dmodel+xml", comment="3MF", suffixes=["3mf"] ) ) - MimeTypeDatabase.addMimeType( - MimeType( - name="application/x-ucp", - comment="UCP", - suffixes=["3mf"] - ) - ) self._supported_extensions = [".3mf"] self._root = None diff --git a/plugins/3MFWriter/UCPDialog.py b/plugins/3MFWriter/UCPDialog.py index 8e62666818..b2ad5834eb 100644 --- a/plugins/3MFWriter/UCPDialog.py +++ b/plugins/3MFWriter/UCPDialog.py @@ -29,15 +29,21 @@ class UCPDialog(QObject): 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 = 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: @@ -67,8 +73,13 @@ class UCPDialog(QObject): file_name = CuraApplication.getInstance().getPrintInformation().baseName try: - device.requestWrite(nodes, file_name, ["application/x-ucp"], workspace_handler, - preferred_mimetype_list="application/x-ucp") + 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: @@ -90,11 +101,13 @@ class UCPDialog(QObject): self._onFinished() def _onFinished(self): - if not self._finished: # Make sure we don't send the finished signal twice, whatever happens - self._finished = True + # 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) + # Reset the model to the workspace writer + mesh_writer = CuraApplication.getInstance().getInstance().getWorkspaceFileHandler().getWriter("3MFWriter") + mesh_writer.setExportModel(None) - self.finished.emit(self._accepted) + self.finished.emit(self._accepted) diff --git a/plugins/3MFWriter/__init__.py b/plugins/3MFWriter/__init__.py index 1cecf4c3f8..0b2976c386 100644 --- a/plugins/3MFWriter/__init__.py +++ b/plugins/3MFWriter/__init__.py @@ -32,12 +32,6 @@ def getMetaData(): "description": i18n_catalog.i18nc("@item:inlistbox", "3MF file"), "mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml", "mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode - }, - { - "extension": "3mf", - "description": i18n_catalog.i18nc("@item:inlistbox", "Universal Cura Project"), - "mime_type": "application/x-ucp", - "mode": ThreeMFWriter.ThreeMFWriter.OutputMode.BinaryMode } ] } @@ -48,12 +42,6 @@ def getMetaData(): "description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"), "mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml", "mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode - }, - { - "extension": "3mf", - "description": i18n_catalog.i18nc("@item:inlistbox", "Universal Cura Project"), - "mime_type": "application/x-ucp", - "mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode } ] } From 62aff0be12396b934c0ca74b6f712d814c90cac4 Mon Sep 17 00:00:00 2001 From: "c.lamboo" Date: Wed, 21 Feb 2024 11:40:51 +0100 Subject: [PATCH 33/39] Determine `ucp` based on included file CURA-11403 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 04885a961b..25e2afa8bd 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -58,6 +58,7 @@ _ignored_machine_network_metadata: Set[str] = { "is_abstract_machine" } +USER_SETTINGS_PATH = "Cura/user-settings.json" class ContainerInfo: def __init__(self, file_name: Optional[str], serialized: Optional[str], parser: Optional[ConfigParser]) -> None: @@ -235,9 +236,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): containers_found_dict = {k: False for k in resolve_strategy_keys} # Check whether the file is a UCP, which changes some import options - #FIXME Instead of this, we should just check for the presence of the user-settings file, whatever the extension - if file_name.endswith('.3mf'): - is_ucp = True + is_ucp = USER_SETTINGS_PATH in cura_file_names + # # Read definition containers # From 909a4156f3ccc64b86a3b0c4864b8c20fd5105e8 Mon Sep 17 00:00:00 2001 From: Saumya Jain Date: Wed, 21 Feb 2024 15:13:27 +0100 Subject: [PATCH 34/39] Updating _ucp_model to None everytime a write is done CURA-11403 --- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index cff938788b..2536f5dacb 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -33,7 +33,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): if self._ucp_model != model: self._ucp_model = model - def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode): + def _write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode): application = Application.getInstance() machine_manager = application.getMachineManager() @@ -125,6 +125,11 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): 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 def _writePluginMetadataToArchive(archive: zipfile.ZipFile) -> None: file_name_template = "%s/plugin_metadata.json" From 2681932fec55545c4ae4c9b07d52bf30568b29a6 Mon Sep 17 00:00:00 2001 From: Saumya Jain Date: Wed, 21 Feb 2024 15:20:38 +0100 Subject: [PATCH 35/39] Adding cura share icon to thumbnail CURA-11624 --- plugins/3MFWriter/ThreeMFWriter.py | 24 +++++++++++++++++++++++- resources/images/cura-share.png | Bin 0 -> 8108 bytes 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 resources/images/cura-share.png diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index 6e3dc86890..9e519e255d 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -10,6 +10,7 @@ from UM.Math.Vector import Vector from UM.Logger import Logger from UM.Math.Matrix import Matrix from UM.Application import Application +from UM.Resources import Resources from UM.Scene.SceneNode import SceneNode from UM.Settings.ContainerRegistry import ContainerRegistry @@ -20,7 +21,8 @@ from cura.Utils.Threading import call_on_qt_thread from cura.Scene.CuraSceneNode import CuraSceneNode 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 from .UCPDialog import UCPDialog @@ -170,6 +172,24 @@ class ThreeMFWriter(MeshWriter): def getArchive(self): return self._archive + 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/3 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() + primary_image.save("test.png", "PNG") + def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode, export_settings_model = None) -> bool: self._archive = None # Reset archive archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED) @@ -194,6 +214,8 @@ class ThreeMFWriter(MeshWriter): # Attempt to add a thumbnail snapshot = self._createSnapshot() if snapshot: + if export_settings_model != None: + self._addShareLogoToThumbnail(snapshot) thumbnail_buffer = QBuffer() thumbnail_buffer.open(QBuffer.OpenModeFlag.ReadWrite) snapshot.save(thumbnail_buffer, "PNG") diff --git a/resources/images/cura-share.png b/resources/images/cura-share.png new file mode 100644 index 0000000000000000000000000000000000000000..60de85194c7a9c71f93ddd90f2f885d18d49c11e GIT binary patch literal 8108 zcmV;dA5-9oP)fwN*)&Y5%AKPG@R68dS6o4Q-iL zD^N$P!e$tF}W>hG{zx>P$r~2?|tWZt#&8keJuK_dBcmvDVsa@3YT$zVF_f zna(r|zMS_t`>fwy&%O4!2F?(jJb7|;uh)CTuC9i&G&C7F19TRQ+0(oOau$ra(Yyk3 z7L2*mya6%@jHAbA;Kh@D7@ADiK^(B3FUIVc-q*IBUk5Gs%V@x%#z{#3Ai~5&iyLRn z3m}8Qm^^qI-t|}i?U!IG`!n``_t>x=nqa6UCN|5=YZDc<0XiGP$Y5eJFVU@IpSElN zL$^)q?$|d!^Q$niVo`F(F_EIvp7d!^_&Px^-hWYv4}j{zK@iXEkX~fD8oVkOO0n2g8^iNk=LbhS!s&CX*~0 z`#p6qQ3Rr-t_Ix4Ux)Dqu5Ef-ZiRvQez%nZ4~pw#b?9Io6yw1G#;k^{Es=gGRn<@`OYa}7?e#f{1u>&p2Dv`#516z`sVl?MQrXqoZwm+_8R{J>d%T`U z3F!vo`yLFZrWg#bj7$Lx6B`}hUiVZZ?;m@rI+a)Q&Z|)|foCN_GP&)i^9(@4FEF|t ze7;jy2&(Icn?w+dx;5Jv*FJ=j8Xg&tE--HV#M7|b8H@Rw+4K!yT?Btwk2k_?kcNCOn!m2}Iji#EgpG)7GW)27O3r)NK!q#wxP z{J9O4j|v<gV2O?ofCj&4ieG{#wVZkU<4p~p#`)QSJwm7lnj$_!YB?v z%Fu+X`ZP8W=^-@wa#RKxQ~hT0E5S=Knq*HCX)>Iv@{4T zEdZ&t)PNX;L*|)en_dzSBh&PW!bBw70jcycugR*D$ExgsZks|Cd%&CzO(jV|ibH&> zg5)?pg&?vHJS$KuAj5;vBabQolBlRW_Q=##Cm8vz>^$$!Gb31)GjPJN@biqSQTAzB z3YU-ybq8GM67T~UNa7OktLX0hP^T3fC{+qdB8D|}N*6qiC?TVPQKevz%3=vIEGZCG zR^)}z+?EA0pQ0Wsd^kZSUNAOvn&xnYok;QbL}Yf@b<;x?fjrmB>6 zpe0FxEdBk0-Sq0qGoBP459ZHPAHiFCAqkn8F6h}Pej8Ryv3*?t5tfI*P;nII+F z@X|elA+JKGav<6`D#j>4(xbdHl|2x-a&`V$x zBJxtBVt<6FvJ4aqHD>F?XqDOyK+KS&xb;3PvBMA7U%d=gUN{aD%NNkQO~J13ANM~* zl6r4PSRI2`Grm2B{Vk)UG`2Us4@&?6M79Z~17to&4#-fsDGB;G;L`>?2P2mbfW+Me zNWd@&IAo|<8ienvXD2GEj$;oFn7U6OSc$OfPBKdw0;=%>V^Q*xl zeS2)yb_&?=iR&tHM%-`m@eVpJxvPAH(7lMt^s~gs35-S=088h^JPKTg?Q10a>Tk8ESdPuCy-1Oe@tuu#vX=!>hXc;e(8F?-wsU2T7fWg zTJ~?+une}m{pBzlascl5&?~H}t2`iv+rd&uj75m)%xD`?uxQ!|)AeX*go{T{%!D!U z!>1Do;ACMQ_?d!KL^YM_O=TrWB9w7PCEEVi7v={briuB?cn0B=0^^k5UilJ$Ex&aM z%!M2%_q_WO-v<9vpu~1Z)v5!{vLK|MT^lwHKwh+fINM=LOc#T$4Yrb&hOvs0zycdR zM)g86_|G8N-^^41!82Z~qVNmbr;>eq?)q1sXT7L}FjsQ-S-)m+lwQfD(^8CifxkxY zjlN^tE>j^=H;@=+VQQsRO%?4!o&piqHueHZ)BypZr9vWVtrq%XKaB64(Qq$WWMJLI zIdGlzm{wVRf7Pl*u*~-6rHh9j+BtfB8Xnty5SE@h4jb047Xt47xaBP?;FjN5F_Nb= zh~E9l$1E7+Ia-=LZ~{K}wP)djo3AXM?*mAwtE#1o#b2TKvm@V#QKCJCu1Gz-C+Bgh z%t&WhTcafgKPaR|eTb@}9Fqb8 ztQWQ99Dw`Y_X=-N&jc;As>ykhx}l|#&V)oW$G76$em`bjmfMkJ*RgrS7OhpaHUQdt zeF9i|kr;d52mEtsL`@lH@ucdh=+as*Y3(KB@X*KCoG~yQ{l&YlR!ufm0vEvtH?>R0 z?#Yu7rJddnKvcGAQ9bkkwXRhbj;OM-VOP_LzV$Or^U=D(&kIFf+sf$!3iNE0z47XE z;hy(gI&Ze3jW+-KrLbY$3fTV0UT?^6dh_a5gYIbezEhC6|5`(s@UBK{BfxZ>Pt3P* zXq9DJ*7bI-o($w|^23^3X(1{q8t)Ko2X(dawdcXTR!xmUPVMae(Q$Zk-*MPuf0G9- z<0yA@{p***tv5`xUh7oU2RC0e%wu53V@J}oP*F3q*@_$pXxWa`>h>mS4=5OJy>3*D zL_G#2`G?V+bzt;ol--!NQFR6P+`I~grCr}U3U_^JzgHzkPxL{YeR8KA@Vh7X!{%SP z*!KLn)sj!z)X}b|Pua}T0&)M1_A2@P6aKcj#98NV(R2fhy1G##4WiFfDvQRd18-=k zl;a7p8!;*9az#6CUo%u(vFoW9;jS<5w+uP~k}>G@Aju(7CpdO$#-Dq~@2rNo(2X|h z>j$mu4@qUAi2<@$g+wT*gXDtKkfM=n2)16pH}vBbhJrr2BD%KjA3b76VDzU&I(#}2gD z{9(zd!L8eW0y`c*Cg8^*YvtIG*M)8Nf!yTC_mZt?(hy3v3=R)jif7KCNGd&MYc(Kl zd&xq}q~D$x_|(aRC*fUpe#Z_Q*FlF@V@g6)Z{<{ge_vN(6FfOn1f=DscCrUUw+Gu+F8c*7KYyGnKK@lM3pW9i%ss84OFG z`l1~|J>=9>0K;%m1*;DN1vvu^q_PY-5y%r~tP!p&0cp>VYuNqV^LG7+Z?iDW$5!^< zV?2doOcieQ;`1ea1TvJsWvWpj@fv2y#koj`+!#_%LiV9}8Nr~^v@Ra%BCFqjP(OcN{4fxmt48qmQN>X=7mosqNC0*qES zk;%S_qza(8Y7*II@Oldb%%qa05Ex@4JyPJ2$&I>6Hw)AO2$qFScJg2iD*Ms5-b~NN z7_;;LHEn#|OJKtl%hG34yJU~i07aX^HYb8IuAG9NI0^pO8xvKSls9>&frCH;jcIp^ zu&_2*ZI!80WwkpJNbyZjT}b&VCmlCmdjZTB-T1}}K#U}{29@Dtm6rJY1dP$rLK9F~ zQUXXlaS;D)*0IBX8}=+EwkgWBVgNf{*>fcjfxsc8uHIw=MhX((x

oB|iO;XyPeXxY;3*eD*I*QbjWE7>Z*1os6um6nL@b>@m^x&!-C&hlft^FH z4v_em&}%>cFV7CVX5xZzNa}nOjja-z4GJK!)UaBMRWQ*p0Nx&n;sIEsJJuwaW#hq7~c2*6`L~PUJ(wkAK;86&(CFD#gkeP+s+wh~+^He$@ zPOJ&N#h0HBpv}Lw3UUMBM(qYs7y~bv&GL5KkB$Bt<{{P+(1uc0>JljO24*O53IG?k z&4BAt8XNd5(|)0h3jv5X!ju1Z?>pIQM{fPi%l$P@b-j3e%7Zaz^v-2pUA8ymv?%TX zU2>F7e&dd>Jsd8OZs>c=UO9FyzFylJeI~@m=c_`|uC>^j0(Ba75$WVX5OYAtK6)kI z%g%2f9cb9!Ye8|pVd%jpg3*`xCG0U#hjFdp5qRahjJh zjH~BraP}Y$mLW~#fm#<(yY{Mej<3XN7{b=+)*+}7ivkl(nX$9VWnq2>XF+LZ)lr+M zb)d|Z?*ESic3fl{@*d1CEcbep^o<5VWuJKxPhSru$Ep|fV>R}i{!9c&%~j-S609ZD zBu-}3i{6#?g?U#Q-0ny{yN-oAEe)c`whE~SmyWpms|W3X&(C4D53`vuI*U6q>Zey zQ2q39rU!^q5q^+o9B%&meN<>R8oANJci#Dg9TPbXS_%;L6t*YtxrZv=4 z>0A;ug#l##$+Rl?R?-6D8B!b@|7mAk!zsefYq|j`JxB#oMy11zWLQHI@7SpxY`o)H zYX^QZ%I#tMzx^2YA2<;i4PoMcOkh&pOn%eCK-6_#T>F*GF(F`P)EpB63PgQPOQnoR zO3hU??6IY0Ah9pv>BXZ^SS?Y9mo>ocWrK>WzN*jymm6}%(Twxp8r=Q0Lva72hyCQs z^;azE9KwC?4VS{sZygTB4M-Cwjhk9VBY^VY9ybQK>I7(d1M2Av1mqjmEcVkH!jy(^ zt#dvev|iz^@1L+S)al@updF3u(V&}?3HnloLG6J-x-K8M!LszAFm50yoo;WNQRn+c zTY|C(L2poJZ2tB?KMh;%+vj_qtH*laqp$H}9wl-GZufoQHSn%GzZ*VO;Cj55#A)y1 z`uo@JlS?#8D1Nw}_!CDi9T2y@{iUrn!*05?qx&B{0{2+kxBti#u(uXUz{bhF2O6!! z$aiMQP##$KX(Jo;^sX@&j0Rq%vdNb4xC4*Y?LSh($N%w%Fo@i!;RAp2T06Efu8JjQ zL(ovJ0W5nJB693J@aD~BrJ-egn#J3sa+xbC%!gFTVCg{->Rafks1J2@#| zO1#2>XEAjSP_@M&sS~GXT2TtSVqnH-=ycqJ5$dZJ!poDQsH5L<+%!+Eej~X8_xh{3~5y zZ?Z%z?4Om}YdZHEqf+wh0#n6?8;Rr5I$r@;5hxCvv4!xS&pzuXm<}RWW_ri(UIzF6 z(Hg7j#)8qEAyB+wK1Dw^)ZynwEN5G_=xFQam%~~&S3LnE{Z>ly-O;ub2~pyi;cNgg z$)fck-i8t)u@G5H%vchi1W*=wfR3H+!OfrfpP{1q>tBB%{O4WQzylw7HEg_kNthm8 z)3j)k4!^H|$`^3ox^eijqY^nu*?QB(;z}noaiwk<$d;76sJ1|{v_Dcuaxqs;EhnN; zmze1F$WzCBle7;l4k7B@9FBuUP7TemcUw6Y$tmFWBYrFo~1m zm0PdBWKI)9}lKHo-e-hpL(ilh*c>3<`|h(YgNU^G!X5ebQa2ue9UCo zQBy_t*nh|cAb$2Jyud@>ISw~}_6H+P*l`SJa7Lvk_r3s+J$Vq8ox1=wtXpnv)w!+b z-nij>*!9d$DJTq*ks5K6nrp#Ab3p;an=R_Cqfic8M5HZQ78#9H{F)XY7*&dkqKVya zrv>G@t>1$kAAZ%mO!{x52fz7a_`Of;lI@9$&V_&a^Eb7E;=MD})ivzbhYHCg9+71x z8FEPPFFvl@sGGt13L0Ull1@G*pZPOvU-Gh?1^&1H`7kHL1(9cp{_MVQgU*9Z9yku4 z|62PHau||E-~?lXp-2H_((Bt)mim%WIxAGD)9pnVmRK+GI^RLkks!5F6@U1QYNMp0 zaNL)VZu!D1Q~|EtV=lUG^E=m$GXN5Ysi>ZKHH_O!5UGPoEUTCbSi5o&Y`Ee)Z*bi_kF~2hdbpx>mz@s}-*qiKy8EF2h(OoJ zXIyLWvD1Bc+{OiuKVup7m7f8snqnTRz3m+n_IFupQ{BAu8-8iHkDJ|V zM?Q@%Ed`VQVHc7F&~}2hQ4OS_+8=>x((psL2*VMUq)6XF*%CFSAR7FhRHW;v1R(0n zO&vHVMEg9YU~D0&xUl^ELkH5&XUvjZ_IJwN*o!jEoUqzC-XZj7-Q#zb-Q#anfGt| z0nCNm=VR~r$^i=i7im`wTf+GX|M~E2EFYmRN{Rz!V)-~c@W-zy&ZF;(1zVUAzitic+wV)y3ArgZroNJ@^!He|*2Nm(lN|=)EZl4c zE~F|;40@)Lpz!TTH=}IF?XT_x!)7!Qu-mQIX-8{kNiE(~x@eQ=`ciNx_?nz)kvHVi{yh+Kdu?qqwqh!mG0v{EZhc z86$wB3EVRGQaz>LFDuUst{cAwM`9~lfsttyL|YD^SUBRTxQ=)jT+BGcWk4>7#SjIfrb25%9x7XyVqQbw%&MY!AuHGC1R#Gu=-;h)QRiTyIn qWvu{Yyxl$m0<@dkS8ngj zyn*4U>}7f6Rh?B~oyO#B6<9q-FJ8w-+9~ zyT7o>?r_K@aqval5TmSsN>qjF%K3Q-qx60#U&H4lyw0!7?%hyZn1Q0eU`ffOWTnm8 z4x0`cg~~?V6@@t% zDoo+UP4cMXy6osUw{cCrzaU+qfvH3Y0050qKV?JQF*iM;D}_E=FdVh3ELtch3W7({ z15WCNq@oEYp(#Y}w9R@yS5N7m6-cE3O8bqn4=v}Y7$uE$@@sOBt0^5IV;V>{!woK7 zb;iJO)B;GXMn{&6npBgYDMi!DbUNI`&loj}9iY&gP1cj4pk5Sni`NwHk1f}=*pH~z zFi~A`6p(XM3+%A#x-$xfZ)rXr;>gW{m6wvvZlL>aL!S44hOk}jWi;ULT+JI8j*5Vs zrJ-qF!EiJWm5cvxY2LwbbQX}=(HQ~5(OE!7r!xkIqyGgV=nSNJP7uHV0000 Date: Wed, 21 Feb 2024 15:22:20 +0100 Subject: [PATCH 36/39] Removing debug statement CURA-11624 --- plugins/3MFWriter/ThreeMFWriter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index 9e519e255d..ce9ea33fbd 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -188,7 +188,6 @@ class ThreeMFWriter(MeshWriter): painter.drawImage(icon_position[0], icon_position[1], icon_image) painter.end() - primary_image.save("test.png", "PNG") def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode, export_settings_model = None) -> bool: self._archive = None # Reset archive From 5f7a1c7b7bdcc8307b4969d1fc0d4d8c24782073 Mon Sep 17 00:00:00 2001 From: "c.lamboo" Date: Wed, 21 Feb 2024 16:50:46 +0100 Subject: [PATCH 37/39] Use correct mime-type CURA-11403m --- resources/qml/Menus/FileMenu.qml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/resources/qml/Menus/FileMenu.qml b/resources/qml/Menus/FileMenu.qml index 7af21182cd..76d4997c3c 100644 --- a/resources/qml/Menus/FileMenu.qml +++ b/resources/qml/Menus/FileMenu.qml @@ -77,16 +77,18 @@ Cura.Menu enabled: UM.WorkspaceFileHandler.enabled && CuraApplication.getPackageManager().allEnabledPackages.includes("3MFWriter") onTriggered: { - if(UM.Preferences.getValue("cura/dialog_on_ucp_project_save")) + if (UM.Preferences.getValue("cura/dialog_on_ucp_project_save")) { CuraApplication.exportUcp() } else { - var args = { "filter_by_machine": false, - "file_type": "workspace", - "preferred_mimetypes": "application/x-ucp", - "limit_mimetypes": ["application/x-ucp"]}; + const args = { + "filter_by_machine": false, + "file_type": "workspace", + "preferred_mimetypes": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml", + "limit_mimetypes": ["application/vnd.ms-package.3dmanufacturing-3dmodel+xml"], + }; UM.OutputDeviceManager.requestWriteToDevice("local_file", PrintInformation.baseName, args) } } From 3c305bb289658dab6a7c7121313c17ec6b6cd48c Mon Sep 17 00:00:00 2001 From: "c.lamboo" Date: Wed, 21 Feb 2024 19:30:36 +0100 Subject: [PATCH 38/39] Fix `dialog_on_ucp_project_save` preference CURA-11403 --- plugins/3MFWriter/ThreeMFWriter.py | 41 ++++++++++++++++++++++++++++-- plugins/3MFWriter/UCPDialog.py | 7 ++--- resources/qml/Menus/FileMenu.qml | 41 ++++++++++++++---------------- 3 files changed, 62 insertions(+), 27 deletions(-) diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index ce9ea33fbd..34ab4b5f58 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -10,6 +10,8 @@ from UM.Math.Vector import Vector from UM.Logger import Logger from UM.Math.Matrix import Matrix 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.Settings.ContainerRegistry import ContainerRegistry @@ -456,5 +458,40 @@ class ThreeMFWriter(MeshWriter): return extra_settings def exportUcp(self): - self._config_dialog = UCPDialog() - self._config_dialog.show() + 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() diff --git a/plugins/3MFWriter/UCPDialog.py b/plugins/3MFWriter/UCPDialog.py index b2ad5834eb..bedfb4d0da 100644 --- a/plugins/3MFWriter/UCPDialog.py +++ b/plugins/3MFWriter/UCPDialog.py @@ -53,8 +53,7 @@ class UCPDialog(QObject): def notifyClosed(self): self._onFinished() - @pyqtSlot() - def _onAccepted(self): + def save3mf(self): application = CuraApplication.getInstance() workspace_handler = application.getInstance().getWorkspaceFileHandler() @@ -92,7 +91,9 @@ class UCPDialog(QObject): Logger.logException("e", "Unable to write to file %s: %s", file_name, e) self._onRejected() - @pyqtSlot() + def _onAccepted(self): + self.save3mf() + def _onRejected(self): self._onFinished() diff --git a/resources/qml/Menus/FileMenu.qml b/resources/qml/Menus/FileMenu.qml index 76d4997c3c..4ca09cc9f1 100644 --- a/resources/qml/Menus/FileMenu.qml +++ b/resources/qml/Menus/FileMenu.qml @@ -47,14 +47,18 @@ Cura.Menu enabled: UM.WorkspaceFileHandler.enabled && saveProjectMenu.model.count == 1 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.open() } 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) } } @@ -75,23 +79,7 @@ Cura.Menu id: saveUCPMenu text: catalog.i18nc("@title:menu menubar:file", "&Save Universal Cura Project...") enabled: UM.WorkspaceFileHandler.enabled && CuraApplication.getPackageManager().allEnabledPackages.includes("3MFWriter") - onTriggered: - { - if (UM.Preferences.getValue("cura/dialog_on_ucp_project_save")) - { - CuraApplication.exportUcp() - } - else - { - const args = { - "filter_by_machine": false, - "file_type": "workspace", - "preferred_mimetypes": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml", - "limit_mimetypes": ["application/vnd.ms-package.3dmanufacturing-3dmodel+xml"], - }; - UM.OutputDeviceManager.requestWriteToDevice("local_file", PrintInformation.baseName, args) - } - } + onTriggered: CuraApplication.exportUcp() } Cura.MenuSeparator { } @@ -102,8 +90,11 @@ Cura.Menu text: catalog.i18nc("@title:menu menubar:file", "&Export...") onTriggered: { - var localDeviceId = "local_file" - UM.OutputDeviceManager.requestWriteToDevice(localDeviceId, PrintInformation.jobName, { "filter_by_machine": false, "preferred_mimetypes": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml"}) + const args = { + "filter_by_machine": false, + "preferred_mimetypes": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml", + }; + UM.OutputDeviceManager.requestWriteToDevice("local_file", PrintInformation.jobName, args); } } @@ -113,7 +104,13 @@ Cura.Menu text: catalog.i18nc("@action:inmenu menubar:file", "Export Selection...") enabled: UM.Selection.hasSelection 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 { } From e99d5eb906c508b6cbfb4bf066c1843ffd0395a1 Mon Sep 17 00:00:00 2001 From: Saumya Jain Date: Thu, 22 Feb 2024 09:59:31 +0100 Subject: [PATCH 39/39] fixing comment CURA-11617 CURA-11624 --- plugins/3MFWriter/ThreeMFWriter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index 34ab4b5f58..6fda1742f8 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -178,7 +178,7 @@ class ThreeMFWriter(MeshWriter): # Load the icon png image icon_image = QImage(Resources.getPath(Resources.Images, "cura-share.png")) - # Resize icon_image to be 1/3 of primary_image size + # 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)