Browse Source

Add used backend plugins to 3mf metadata

These are appended to project metadata. A warning can now be shown to the user when trying to load a project file while the correct plugins are not installed. These missing plugins can concequently be downloaded from the marketplace. To show the warning and install missing package dialog the same system we use to install missing materials is used.

CURA-10719
c.lamboo 1 year ago
parent
commit
a1f02154f1
1 changed files with 77 additions and 11 deletions
  1. 77 11
      plugins/3MFWriter/ThreeMFWriter.py

+ 77 - 11
plugins/3MFWriter/ThreeMFWriter.py

@@ -1,8 +1,9 @@
 #  Copyright (c) 2015-2022 Ultimaker B.V.
 #  Cura is released under the terms of the LGPLv3 or higher.
 import json
+import re
 
-from typing import Optional, cast, List, Dict
+from typing import Optional, cast, List, Dict, Pattern, Set
 
 from UM.Mesh.MeshWriter import MeshWriter
 from UM.Math.Vector import Vector
@@ -17,6 +18,7 @@ from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer
 
 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
 
@@ -175,13 +177,15 @@ class ThreeMFWriter(MeshWriter):
                 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")
+                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")
+                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
-            material_metadata = self._getMaterialPackageMetadata()
-            self._storeMetadataJson({"packages": material_metadata}, archive, PACKAGE_METADATA_PATH)
+            packages_metadata = self._getMaterialPackageMetadata() + self._getPluginPackageMetadata()
+            self._storeMetadataJson({"packages": packages_metadata}, archive, PACKAGE_METADATA_PATH)
 
             savitar_scene = Savitar.Scene()
 
@@ -253,7 +257,64 @@ class ThreeMFWriter(MeshWriter):
         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))
+        archive.writestr(metadata_file,
+                         json.dumps(metadata, separators=(", ", ": "), indent=4, skipkeys=True, ensure_ascii=False))
+
+    @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<plugin_id>\w+)@(?P<version>\d+.\d+.\d+)::(?P<value>\w+)")
+        # This regex parses enum values to find if they contain custom
+        # backend engine values. These custom enum values are in the format
+        #      PLUGIN::<plugin_id>@<version>::<value>
+        # 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 add_plugin_ids_in_stack(stack: CuraContainerStack):
+            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 = Application.getInstance().getMachineManager().activeMachine
+        add_plugin_ids_in_stack(global_stack)
+
+        for container in global_stack.getContainers():
+            add_plugin_ids_in_stack(container)
+
+        for extruder_stack in global_stack.extruderList:
+            add_plugin_ids_in_stack(extruder_stack)
+
+            for container in extruder_stack.getContainers():
+                add_plugin_ids_in_stack(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": "backend_plugin",
+            }
+
+        # Storing in a dict and fetching values to avoid duplicates
+        return list(metadata.values())
 
     @staticmethod
     def _getMaterialPackageMetadata() -> List[Dict[str, str]]:
@@ -278,7 +339,8 @@ class ThreeMFWriter(MeshWriter):
                 # Don't export bundled materials
                 continue
 
-            package_id = package_manager.getMaterialFilePackageId(extruder.material.getFileName(), extruder.material.getMetaDataEntry("GUID"))
+            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
@@ -286,10 +348,14 @@ class ThreeMFWriter(MeshWriter):
                 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 ""}
+            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