mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-06 22:47:29 -06:00
Merge branch 'master' into feature_xmlmaterials_cura_settings
# Conflicts: # plugins/XmlMaterialProfile/XmlMaterialProfile.py
This commit is contained in:
commit
266be74569
312 changed files with 46856 additions and 17766 deletions
|
@ -3,7 +3,10 @@
|
|||
|
||||
import copy
|
||||
import io
|
||||
from typing import List, Optional
|
||||
import json #To parse the product-to-id mapping file.
|
||||
import os.path #To find the product-to-id mapping.
|
||||
import sys
|
||||
from typing import Any, Dict, List, Optional
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from UM.Resources import Resources
|
||||
|
@ -14,7 +17,6 @@ import UM.Dictionary
|
|||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
|
||||
|
||||
## Handles serializing and deserializing material containers from an XML file
|
||||
class XmlMaterialProfile(InstanceContainer):
|
||||
CurrentFdmMaterialVersion = "1.3"
|
||||
|
@ -42,25 +44,18 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
def getInheritedFiles(self):
|
||||
return self._inherited_files
|
||||
|
||||
## Overridden from InstanceContainer
|
||||
def setReadOnly(self, read_only):
|
||||
super().setReadOnly(read_only)
|
||||
|
||||
basefile = self.getMetaDataEntry("base_file", self._id) # if basefile is self.id, this is a basefile.
|
||||
for container in ContainerRegistry.getInstance().findInstanceContainers(base_file = basefile):
|
||||
container._read_only = read_only # prevent loop instead of calling setReadOnly
|
||||
|
||||
## Overridden from InstanceContainer
|
||||
# set the meta data for all machine / variant combinations
|
||||
def setMetaDataEntry(self, key, value):
|
||||
if self.isReadOnly():
|
||||
registry = ContainerRegistry.getInstance()
|
||||
if registry.isReadOnly(self.getId()):
|
||||
return
|
||||
|
||||
super().setMetaDataEntry(key, value)
|
||||
|
||||
basefile = self.getMetaDataEntry("base_file", self._id) #if basefile is self.id, this is a basefile.
|
||||
basefile = self.getMetaDataEntry("base_file", self.getId()) #if basefile is self.getId, this is a basefile.
|
||||
# Update all containers that share basefile
|
||||
for container in ContainerRegistry.getInstance().findInstanceContainers(base_file = basefile):
|
||||
for container in registry.findInstanceContainers(base_file = basefile):
|
||||
if container.getMetaDataEntry(key, None) != value: # Prevent recursion
|
||||
container.setMetaDataEntry(key, value)
|
||||
|
||||
|
@ -68,7 +63,8 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
# without this function the setName would only set the name of the specific nozzle / material / machine combination container
|
||||
# The function is a bit tricky. It will not set the name of all containers if it has the correct name itself.
|
||||
def setName(self, new_name):
|
||||
if self.isReadOnly():
|
||||
registry = ContainerRegistry.getInstance()
|
||||
if registry.isReadOnly(self.getId()):
|
||||
return
|
||||
|
||||
# Not only is this faster, it also prevents a major loop that causes a stack overflow.
|
||||
|
@ -77,10 +73,10 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
|
||||
super().setName(new_name)
|
||||
|
||||
basefile = self.getMetaDataEntry("base_file", self._id) # if basefile is self.id, this is a basefile.
|
||||
basefile = self.getMetaDataEntry("base_file", self.getId()) # if basefile is self.getId, this is a basefile.
|
||||
# Update the basefile as well, this is actually what we're trying to do
|
||||
# Update all containers that share GUID and basefile
|
||||
containers = ContainerRegistry.getInstance().findInstanceContainers(base_file = basefile)
|
||||
containers = registry.findInstanceContainers(base_file = basefile)
|
||||
for container in containers:
|
||||
container.setName(new_name)
|
||||
|
||||
|
@ -88,33 +84,20 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
def setDirty(self, dirty):
|
||||
super().setDirty(dirty)
|
||||
base_file = self.getMetaDataEntry("base_file", None)
|
||||
if base_file is not None and base_file != self._id:
|
||||
containers = ContainerRegistry.getInstance().findContainers(id=base_file)
|
||||
registry = ContainerRegistry.getInstance()
|
||||
if base_file is not None and base_file != self.getId() and not registry.isReadOnly(base_file):
|
||||
containers = registry.findContainers(id = base_file)
|
||||
if containers:
|
||||
base_container = containers[0]
|
||||
if not base_container.isReadOnly():
|
||||
base_container.setDirty(dirty)
|
||||
|
||||
## Overridden from InstanceContainer
|
||||
# def setProperty(self, key, property_name, property_value, container = None):
|
||||
# if self.isReadOnly():
|
||||
# return
|
||||
#
|
||||
# super().setProperty(key, property_name, property_value)
|
||||
#
|
||||
# basefile = self.getMetaDataEntry("base_file", self._id) #if basefile is self.id, this is a basefile.
|
||||
# for container in UM.Settings.ContainerRegistry.ContainerRegistry.getInstance().findInstanceContainers(base_file = basefile):
|
||||
# if not container.isReadOnly():
|
||||
# container.setDirty(True)
|
||||
containers[0].setDirty(dirty)
|
||||
|
||||
## Overridden from InstanceContainer
|
||||
# base file: common settings + supported machines
|
||||
# machine / variant combination: only changes for itself.
|
||||
def serialize(self, ignored_metadata_keys: Optional[List] = None):
|
||||
def serialize(self, ignored_metadata_keys: Optional[set] = None):
|
||||
registry = ContainerRegistry.getInstance()
|
||||
|
||||
base_file = self.getMetaDataEntry("base_file", "")
|
||||
if base_file and self.id != base_file:
|
||||
if base_file and self.getId() != base_file:
|
||||
# Since we create an instance of XmlMaterialProfile for each machine and nozzle in the profile,
|
||||
# we should only serialize the "base" material definition, since that can then take care of
|
||||
# serializing the machine/nozzle specific profiles.
|
||||
|
@ -133,8 +116,8 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
metadata = copy.deepcopy(self.getMetaData())
|
||||
# setting_version is derived from the "version" tag in the schema, so don't serialize it into a file
|
||||
if ignored_metadata_keys is None:
|
||||
ignored_metadata_keys = []
|
||||
ignored_metadata_keys = ignored_metadata_keys + ["setting_version"]
|
||||
ignored_metadata_keys = set()
|
||||
ignored_metadata_keys |= {"setting_version"}
|
||||
# remove the keys that we want to ignore in the metadata
|
||||
for key in ignored_metadata_keys:
|
||||
if key in metadata:
|
||||
|
@ -147,6 +130,9 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
metadata.pop("type", "")
|
||||
metadata.pop("base_file", "")
|
||||
metadata.pop("approximate_diameter", "")
|
||||
metadata.pop("id", "")
|
||||
metadata.pop("container_type", "")
|
||||
metadata.pop("name", "")
|
||||
|
||||
## Begin Name Block
|
||||
builder.start("name")
|
||||
|
@ -164,7 +150,7 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
builder.end("color")
|
||||
|
||||
builder.start("label")
|
||||
builder.data(self._name)
|
||||
builder.data(self.getName())
|
||||
builder.end("label")
|
||||
|
||||
builder.end("name")
|
||||
|
@ -196,16 +182,16 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
## Begin Settings Block
|
||||
builder.start("settings")
|
||||
|
||||
if self.getDefinition().id == "fdmprinter":
|
||||
if self.getDefinition().getId() == "fdmprinter":
|
||||
for instance in self.findInstances():
|
||||
self._addSettingElement(builder, instance)
|
||||
|
||||
machine_container_map = {}
|
||||
machine_nozzle_map = {}
|
||||
|
||||
all_containers = registry.findInstanceContainers(GUID = self.getMetaDataEntry("GUID"), base_file = self._id)
|
||||
all_containers = registry.findInstanceContainers(GUID = self.getMetaDataEntry("GUID"), base_file = self.getId())
|
||||
for container in all_containers:
|
||||
definition_id = container.getDefinition().id
|
||||
definition_id = container.getDefinition().getId()
|
||||
if definition_id == "fdmprinter":
|
||||
continue
|
||||
|
||||
|
@ -223,17 +209,16 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
machine_container_map[definition_id] = container
|
||||
|
||||
# Map machine human-readable names to IDs
|
||||
product_id_map = {}
|
||||
for container in registry.findDefinitionContainers(type = "machine"):
|
||||
product_id_map[container.getName()] = container.getId()
|
||||
product_id_map = self.getProductIdMap()
|
||||
|
||||
for definition_id, container in machine_container_map.items():
|
||||
definition = container.getDefinition()
|
||||
try:
|
||||
product = UM.Dictionary.findKey(product_id_map, definition_id)
|
||||
except ValueError:
|
||||
# An unknown product id; export it anyway
|
||||
product = definition_id
|
||||
|
||||
product = definition_id
|
||||
for product_name, product_id_list in product_id_map.items():
|
||||
if definition_id in product_id_list:
|
||||
product = product_name
|
||||
break
|
||||
|
||||
builder.start("machine")
|
||||
builder.start("machine_identifier", {
|
||||
|
@ -243,7 +228,7 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
builder.end("machine_identifier")
|
||||
|
||||
for instance in container.findInstances():
|
||||
if self.getDefinition().id == "fdmprinter" and self.getInstance(instance.definition.key) and self.getProperty(instance.definition.key, "value") == instance.value:
|
||||
if self.getDefinition().getId() == "fdmprinter" and self.getInstance(instance.definition.key) and self.getProperty(instance.definition.key, "value") == instance.value:
|
||||
# If the settings match that of the base profile, just skip since we inherit the base profile.
|
||||
continue
|
||||
|
||||
|
@ -251,11 +236,12 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
|
||||
# Find all hotend sub-profiles corresponding to this material and machine and add them to this profile.
|
||||
for hotend_id, hotend in machine_nozzle_map[definition_id].items():
|
||||
variant_containers = registry.findInstanceContainers(id = hotend.getMetaDataEntry("variant"))
|
||||
variant_containers = registry.findInstanceContainersMetadata(id = hotend.getMetaDataEntry("variant"))
|
||||
if not variant_containers:
|
||||
continue
|
||||
|
||||
builder.start("hotend", {"id": variant_containers[0].getName()})
|
||||
# The hotend identifier is not the containers name, but its "name".
|
||||
builder.start("hotend", {"id": variant_containers[0]["name"]})
|
||||
|
||||
# Compatible is a special case, as it's added as a meta data entry (instead of an instance).
|
||||
compatible = hotend.getMetaDataEntry("compatible")
|
||||
|
@ -398,15 +384,18 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
first.append(element)
|
||||
|
||||
def clearData(self):
|
||||
self._metadata = {}
|
||||
self._name = ""
|
||||
self._metadata = {
|
||||
"id": self.getId(),
|
||||
"name": ""
|
||||
}
|
||||
self._definition = None
|
||||
self._instances = {}
|
||||
self._read_only = False
|
||||
self._dirty = False
|
||||
self._path = ""
|
||||
|
||||
def getConfigurationTypeFromSerialized(self, serialized: str) -> Optional[str]:
|
||||
@classmethod
|
||||
def getConfigurationTypeFromSerialized(cls, serialized: str) -> Optional[str]:
|
||||
return "materials"
|
||||
|
||||
@classmethod
|
||||
|
@ -416,31 +405,34 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
version = XmlMaterialProfile.Version
|
||||
# get setting version
|
||||
if "version" in data.attrib:
|
||||
setting_version = XmlMaterialProfile.xmlVersionToSettingVersion(data.attrib["version"])
|
||||
setting_version = cls.xmlVersionToSettingVersion(data.attrib["version"])
|
||||
else:
|
||||
setting_version = XmlMaterialProfile.xmlVersionToSettingVersion("1.2")
|
||||
setting_version = cls.xmlVersionToSettingVersion("1.2")
|
||||
|
||||
return version * 1000000 + setting_version
|
||||
|
||||
## Overridden from InstanceContainer
|
||||
def deserialize(self, serialized):
|
||||
def deserialize(self, serialized, file_name = None):
|
||||
containers_to_add = []
|
||||
# update the serialized data first
|
||||
from UM.Settings.Interfaces import ContainerInterface
|
||||
serialized = ContainerInterface.deserialize(self, serialized)
|
||||
serialized = ContainerInterface.deserialize(self, serialized, file_name)
|
||||
|
||||
try:
|
||||
data = ET.fromstring(serialized)
|
||||
except:
|
||||
Logger.logException("e", "An exception occured while parsing the material profile")
|
||||
Logger.logException("e", "An exception occurred while parsing the material profile")
|
||||
return
|
||||
|
||||
# Reset previous metadata
|
||||
old_id = self.getId()
|
||||
self.clearData() # Ensure any previous data is gone.
|
||||
meta_data = {}
|
||||
meta_data["type"] = "material"
|
||||
meta_data["base_file"] = self.id
|
||||
meta_data["base_file"] = self.getId()
|
||||
meta_data["status"] = "unknown" # TODO: Add material verification
|
||||
meta_data["id"] = old_id
|
||||
meta_data["container_type"] = XmlMaterialProfile
|
||||
|
||||
common_setting_values = {}
|
||||
|
||||
|
@ -455,8 +447,8 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
else:
|
||||
meta_data["setting_version"] = self.xmlVersionToSettingVersion("1.2") #1.2 and lower didn't have that version number there yet.
|
||||
|
||||
metadata = data.iterfind("./um:metadata/*", self.__namespaces)
|
||||
for entry in metadata:
|
||||
meta_data["name"] = "Unknown Material" #In case the name tag is missing.
|
||||
for entry in data.iterfind("./um:metadata/*", self.__namespaces):
|
||||
tag_name = _tag_without_namespace(entry)
|
||||
|
||||
if tag_name == "name":
|
||||
|
@ -466,9 +458,9 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
label = entry.find("./um:label", self.__namespaces)
|
||||
|
||||
if label is not None:
|
||||
self._name = label.text
|
||||
meta_data["name"] = label.text
|
||||
else:
|
||||
self._name = self._profile_name(material.text, color.text)
|
||||
meta_data["name"] = self._profile_name(material.text, color.text)
|
||||
meta_data["brand"] = brand.text
|
||||
meta_data["material"] = material.text
|
||||
meta_data["color_name"] = color.text
|
||||
|
@ -500,8 +492,7 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
|
||||
meta_data["approximate_diameter"] = str(round(float(property_values.get("diameter", 2.85)))) # In mm
|
||||
meta_data["properties"] = property_values
|
||||
|
||||
self.setDefinition(ContainerRegistry.getInstance().findDefinitionContainers(id = "fdmprinter")[0])
|
||||
meta_data["definition"] = "fdmprinter"
|
||||
|
||||
common_compatibility = True
|
||||
settings = data.iterfind("./um:settings/um:setting", self.__namespaces)
|
||||
|
@ -526,9 +517,7 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
self._dirty = False
|
||||
|
||||
# Map machine human-readable names to IDs
|
||||
product_id_map = {}
|
||||
for container in ContainerRegistry.getInstance().findDefinitionContainers(type = "machine"):
|
||||
product_id_map[container.getName()] = container.getId()
|
||||
product_id_map = self.getProductIdMap()
|
||||
|
||||
machines = data.iterfind("./um:settings/um:machine", self.__namespaces)
|
||||
for machine in machines:
|
||||
|
@ -556,77 +545,98 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
|
||||
identifiers = machine.iterfind("./um:machine_identifier", self.__namespaces)
|
||||
for identifier in identifiers:
|
||||
machine_id = product_id_map.get(identifier.get("product"), None)
|
||||
if machine_id is None:
|
||||
# Lets try again with some naive heuristics.
|
||||
machine_id = identifier.get("product").replace(" ", "").lower()
|
||||
machine_id_list = product_id_map.get(identifier.get("product"), [])
|
||||
if not machine_id_list:
|
||||
machine_id_list = self.getPossibleDefinitionIDsFromName(identifier.get("product"))
|
||||
|
||||
definitions = ContainerRegistry.getInstance().findDefinitionContainers(id = machine_id)
|
||||
if not definitions:
|
||||
Logger.log("w", "No definition found for machine ID %s", machine_id)
|
||||
continue
|
||||
|
||||
definition = definitions[0]
|
||||
|
||||
machine_manufacturer = identifier.get("manufacturer", definition.getMetaDataEntry("manufacturer", "Unknown")) #If the XML material doesn't specify a manufacturer, use the one in the actual printer definition.
|
||||
|
||||
if machine_compatibility:
|
||||
new_material_id = self.id + "_" + machine_id
|
||||
|
||||
# The child or derived material container may already exist. This can happen when a material in a
|
||||
# project file and the a material in Cura have the same ID.
|
||||
# In the case if a derived material already exists, override that material container because if
|
||||
# the data in the parent material has been changed, the derived ones should be updated too.
|
||||
found_materials = ContainerRegistry.getInstance().findInstanceContainers(id = new_material_id)
|
||||
is_new_material = False
|
||||
if found_materials:
|
||||
new_material = found_materials[0]
|
||||
else:
|
||||
new_material = XmlMaterialProfile(new_material_id)
|
||||
is_new_material = True
|
||||
|
||||
# Update the private directly, as we want to prevent the lookup that is done when using setName
|
||||
new_material._name = self.getName()
|
||||
new_material.setMetaData(copy.deepcopy(self.getMetaData()))
|
||||
new_material.setDefinition(definition)
|
||||
# Don't use setMetadata, as that overrides it for all materials with same base file
|
||||
new_material.getMetaData()["compatible"] = machine_compatibility
|
||||
new_material.getMetaData()["machine_manufacturer"] = machine_manufacturer
|
||||
|
||||
new_material.setCachedValues(cached_machine_setting_properties)
|
||||
|
||||
new_material._dirty = False
|
||||
|
||||
if is_new_material:
|
||||
containers_to_add.append(new_material)
|
||||
|
||||
hotends = machine.iterfind("./um:hotend", self.__namespaces)
|
||||
for hotend in hotends:
|
||||
hotend_id = hotend.get("id")
|
||||
if hotend_id is None:
|
||||
for machine_id in machine_id_list:
|
||||
definitions = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = machine_id)
|
||||
if not definitions:
|
||||
Logger.log("w", "No definition found for machine ID %s", machine_id)
|
||||
continue
|
||||
|
||||
variant_containers = ContainerRegistry.getInstance().findInstanceContainers(id = hotend_id)
|
||||
if not variant_containers:
|
||||
# It is not really properly defined what "ID" is so also search for variants by name.
|
||||
variant_containers = ContainerRegistry.getInstance().findInstanceContainers(definition = definition.id, name = hotend_id)
|
||||
Logger.log("d", "Found definition for machine ID %s", machine_id)
|
||||
definition = definitions[0]
|
||||
|
||||
if not variant_containers:
|
||||
#Logger.log("d", "No variants found with ID or name %s for machine %s", hotend_id, definition.id)
|
||||
continue
|
||||
machine_manufacturer = identifier.get("manufacturer", definition.get("manufacturer", "Unknown")) #If the XML material doesn't specify a manufacturer, use the one in the actual printer definition.
|
||||
|
||||
hotend_compatibility = machine_compatibility
|
||||
hotend_setting_values = {}
|
||||
settings = hotend.iterfind("./um:setting", self.__namespaces)
|
||||
for entry in settings:
|
||||
key = entry.get("key")
|
||||
if key in self.__material_settings_setting_map:
|
||||
hotend_setting_values[self.__material_settings_setting_map[key]] = entry.text
|
||||
elif key in self.__unmapped_settings:
|
||||
if key == "hardware compatible":
|
||||
hotend_compatibility = self._parseCompatibleValue(entry.text)
|
||||
if machine_compatibility:
|
||||
new_material_id = self.getId() + "_" + machine_id
|
||||
|
||||
# The child or derived material container may already exist. This can happen when a material in a
|
||||
# project file and the a material in Cura have the same ID.
|
||||
# In the case if a derived material already exists, override that material container because if
|
||||
# the data in the parent material has been changed, the derived ones should be updated too.
|
||||
if ContainerRegistry.getInstance().isLoaded(new_material_id):
|
||||
new_material = ContainerRegistry.getInstance().findContainers(id = new_material_id)[0]
|
||||
is_new_material = False
|
||||
else:
|
||||
Logger.log("d", "Unsupported material setting %s", key)
|
||||
new_material = XmlMaterialProfile(new_material_id)
|
||||
is_new_material = True
|
||||
|
||||
new_material.setMetaData(copy.deepcopy(self.getMetaData()))
|
||||
new_material.getMetaData()["id"] = new_material_id
|
||||
new_material.getMetaData()["name"] = self.getName()
|
||||
new_material.setDefinition(machine_id)
|
||||
# Don't use setMetadata, as that overrides it for all materials with same base file
|
||||
new_material.getMetaData()["compatible"] = machine_compatibility
|
||||
new_material.getMetaData()["machine_manufacturer"] = machine_manufacturer
|
||||
new_material.getMetaData()["definition"] = machine_id
|
||||
|
||||
new_material.setCachedValues(cached_machine_setting_properties)
|
||||
|
||||
new_material._dirty = False
|
||||
|
||||
if is_new_material:
|
||||
containers_to_add.append(new_material)
|
||||
|
||||
hotends = machine.iterfind("./um:hotend", self.__namespaces)
|
||||
for hotend in hotends:
|
||||
hotend_id = hotend.get("id")
|
||||
if hotend_id is None:
|
||||
continue
|
||||
|
||||
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = hotend_id)
|
||||
if not variant_containers:
|
||||
# It is not really properly defined what "ID" is so also search for variants by name.
|
||||
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(definition = machine_id, name = hotend_id)
|
||||
|
||||
if not variant_containers:
|
||||
continue
|
||||
|
||||
hotend_compatibility = machine_compatibility
|
||||
hotend_setting_values = {}
|
||||
settings = hotend.iterfind("./um:setting", self.__namespaces)
|
||||
for entry in settings:
|
||||
key = entry.get("key")
|
||||
if key in self.__material_settings_setting_map:
|
||||
hotend_setting_values[self.__material_settings_setting_map[key]] = entry.text
|
||||
elif key in self.__unmapped_settings:
|
||||
if key == "hardware compatible":
|
||||
hotend_compatibility = self._parseCompatibleValue(entry.text)
|
||||
else:
|
||||
Logger.log("d", "Unsupported material setting %s", key)
|
||||
|
||||
new_hotend_id = self.getId() + "_" + machine_id + "_" + hotend_id.replace(" ", "_")
|
||||
|
||||
# Same as machine compatibility, keep the derived material containers consistent with the parent
|
||||
# material
|
||||
if ContainerRegistry.getInstance().isLoaded(new_hotend_id):
|
||||
new_hotend_material = ContainerRegistry.getInstance().findContainers(id = new_hotend_id)[0]
|
||||
is_new_material = False
|
||||
else:
|
||||
new_hotend_material = XmlMaterialProfile(new_hotend_id)
|
||||
is_new_material = True
|
||||
|
||||
new_hotend_material.setMetaData(copy.deepcopy(self.getMetaData()))
|
||||
new_hotend_material.getMetaData()["id"] = new_hotend_id
|
||||
new_hotend_material.getMetaData()["name"] = self.getName()
|
||||
new_hotend_material.getMetaData()["variant"] = variant_containers[0]["id"]
|
||||
new_hotend_material.setDefinition(machine_id)
|
||||
# Don't use setMetadata, as that overrides it for all materials with same base file
|
||||
new_hotend_material.getMetaData()["compatible"] = hotend_compatibility
|
||||
new_hotend_material.getMetaData()["machine_manufacturer"] = machine_manufacturer
|
||||
new_hotend_material.getMetaData()["definition"] = machine_id
|
||||
|
||||
# Add namespaced Cura-specific settings
|
||||
settings = hotend.iterfind("./cura:setting", self.__namespaces)
|
||||
|
@ -636,38 +646,192 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
|
||||
new_hotend_id = self.id + "_" + machine_id + "_" + hotend_id.replace(" ", "_")
|
||||
|
||||
# Same as machine compatibility, keep the derived material containers consistent with the parent
|
||||
# material
|
||||
found_materials = ContainerRegistry.getInstance().findInstanceContainers(id = new_hotend_id)
|
||||
is_new_material = False
|
||||
if found_materials:
|
||||
new_hotend_material = found_materials[0]
|
||||
else:
|
||||
new_hotend_material = XmlMaterialProfile(new_hotend_id)
|
||||
is_new_material = True
|
||||
cached_hotend_setting_properties = cached_machine_setting_properties.copy()
|
||||
cached_hotend_setting_properties.update(hotend_setting_values)
|
||||
|
||||
# Update the private directly, as we want to prevent the lookup that is done when using setName
|
||||
new_hotend_material._name = self.getName()
|
||||
new_hotend_material.setMetaData(copy.deepcopy(self.getMetaData()))
|
||||
new_hotend_material.setDefinition(definition)
|
||||
new_hotend_material.addMetaDataEntry("variant", variant_containers[0].id)
|
||||
# Don't use setMetadata, as that overrides it for all materials with same base file
|
||||
new_hotend_material.getMetaData()["compatible"] = hotend_compatibility
|
||||
new_hotend_material.getMetaData()["machine_manufacturer"] = machine_manufacturer
|
||||
new_hotend_material.setCachedValues(cached_hotend_setting_properties)
|
||||
|
||||
cached_hotend_setting_properties = cached_machine_setting_properties.copy()
|
||||
cached_hotend_setting_properties.update(hotend_setting_values)
|
||||
new_hotend_material._dirty = False
|
||||
|
||||
new_hotend_material.setCachedValues(cached_hotend_setting_properties)
|
||||
if is_new_material:
|
||||
containers_to_add.append(new_hotend_material)
|
||||
|
||||
new_hotend_material._dirty = False
|
||||
|
||||
if is_new_material:
|
||||
containers_to_add.append(new_hotend_material)
|
||||
# there is only one ID for a machine. Once we have reached here, it means we have already found
|
||||
# a workable ID for that machine, so there is no need to continue
|
||||
break
|
||||
|
||||
for container_to_add in containers_to_add:
|
||||
ContainerRegistry.getInstance().addContainer(container_to_add)
|
||||
|
||||
@classmethod
|
||||
def deserializeMetadata(cls, serialized: str, container_id: str) -> List[Dict[str, Any]]:
|
||||
result_metadata = [] #All the metadata that we found except the base (because the base is returned).
|
||||
|
||||
#Update the serialized data to the latest version.
|
||||
serialized = cls._updateSerialized(serialized)
|
||||
|
||||
base_metadata = {
|
||||
"type": "material",
|
||||
"status": "unknown", #TODO: Add material verification.
|
||||
"container_type": XmlMaterialProfile,
|
||||
"id": container_id,
|
||||
"base_file": container_id
|
||||
}
|
||||
|
||||
try:
|
||||
data = ET.fromstring(serialized)
|
||||
except:
|
||||
Logger.logException("e", "An exception occurred while parsing the material profile")
|
||||
return []
|
||||
|
||||
#TODO: Implement the <inherits> tag. It's unused at the moment though.
|
||||
|
||||
if "version" in data.attrib:
|
||||
base_metadata["setting_version"] = cls.xmlVersionToSettingVersion(data.attrib["version"])
|
||||
else:
|
||||
base_metadata["setting_version"] = cls.xmlVersionToSettingVersion("1.2") #1.2 and lower didn't have that version number there yet.
|
||||
|
||||
for entry in data.iterfind("./um:metadata/*", cls.__namespaces):
|
||||
tag_name = _tag_without_namespace(entry)
|
||||
|
||||
if tag_name == "name":
|
||||
brand = entry.find("./um:brand", cls.__namespaces)
|
||||
material = entry.find("./um:material", cls.__namespaces)
|
||||
color = entry.find("./um:color", cls.__namespaces)
|
||||
label = entry.find("./um:label", cls.__namespaces)
|
||||
|
||||
if label is not None:
|
||||
base_metadata["name"] = label.text
|
||||
else:
|
||||
base_metadata["name"] = cls._profile_name(material.text, color.text)
|
||||
base_metadata["brand"] = brand.text
|
||||
base_metadata["material"] = material.text
|
||||
base_metadata["color_name"] = color.text
|
||||
continue
|
||||
|
||||
#Setting_version is derived from the "version" tag in the schema earlier, so don't set it here.
|
||||
if tag_name == "setting_version":
|
||||
continue
|
||||
|
||||
base_metadata[tag_name] = entry.text
|
||||
|
||||
if "description" not in base_metadata:
|
||||
base_metadata["description"] = ""
|
||||
if "adhesion_info" not in base_metadata:
|
||||
base_metadata["adhesion_info"] = ""
|
||||
|
||||
property_values = {}
|
||||
properties = data.iterfind("./um:properties/*", cls.__namespaces)
|
||||
for entry in properties:
|
||||
tag_name = _tag_without_namespace(entry)
|
||||
property_values[tag_name] = entry.text
|
||||
|
||||
base_metadata["approximate_diameter"] = str(round(float(property_values.get("diameter", 2.85)))) # In mm
|
||||
base_metadata["properties"] = property_values
|
||||
base_metadata["definition"] = "fdmprinter"
|
||||
|
||||
compatible_entries = data.iterfind("./um:settings/um:setting[@key='hardware compatible']", cls.__namespaces)
|
||||
try:
|
||||
common_compatibility = cls._parseCompatibleValue(next(compatible_entries).text)
|
||||
except StopIteration: #No 'hardware compatible' setting.
|
||||
common_compatibility = True
|
||||
base_metadata["compatible"] = common_compatibility
|
||||
result_metadata.append(base_metadata)
|
||||
|
||||
# Map machine human-readable names to IDs
|
||||
product_id_map = cls.getProductIdMap()
|
||||
|
||||
for machine in data.iterfind("./um:settings/um:machine", cls.__namespaces):
|
||||
machine_compatibility = common_compatibility
|
||||
for entry in machine.iterfind("./um:setting", cls.__namespaces):
|
||||
key = entry.get("key")
|
||||
if key == "hardware compatible":
|
||||
machine_compatibility = cls._parseCompatibleValue(entry.text)
|
||||
|
||||
for identifier in machine.iterfind("./um:machine_identifier", cls.__namespaces):
|
||||
machine_id_list = product_id_map.get(identifier.get("product"), [])
|
||||
if not machine_id_list:
|
||||
machine_id_list = cls.getPossibleDefinitionIDsFromName(identifier.get("product"))
|
||||
|
||||
for machine_id in machine_id_list:
|
||||
definition_metadata = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = machine_id)
|
||||
if not definition_metadata:
|
||||
Logger.log("w", "No definition found for machine ID %s", machine_id)
|
||||
continue
|
||||
|
||||
Logger.log("d", "Found def for machine [%s]", machine_id)
|
||||
definition_metadata = definition_metadata[0]
|
||||
|
||||
machine_manufacturer = identifier.get("manufacturer", definition_metadata.get("manufacturer", "Unknown")) #If the XML material doesn't specify a manufacturer, use the one in the actual printer definition.
|
||||
|
||||
if machine_compatibility:
|
||||
new_material_id = container_id + "_" + machine_id
|
||||
|
||||
# The child or derived material container may already exist. This can happen when a material in a
|
||||
# project file and the a material in Cura have the same ID.
|
||||
# In the case if a derived material already exists, override that material container because if
|
||||
# the data in the parent material has been changed, the derived ones should be updated too.
|
||||
found_materials = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = new_material_id)
|
||||
if found_materials:
|
||||
new_material_metadata = found_materials[0]
|
||||
else:
|
||||
new_material_metadata = {}
|
||||
|
||||
new_material_metadata.update(base_metadata)
|
||||
new_material_metadata["id"] = new_material_id
|
||||
new_material_metadata["compatible"] = machine_compatibility
|
||||
new_material_metadata["machine_manufacturer"] = machine_manufacturer
|
||||
new_material_metadata["definition"] = machine_id
|
||||
|
||||
if len(found_materials) == 0: #This is a new material.
|
||||
result_metadata.append(new_material_metadata)
|
||||
|
||||
for hotend in machine.iterfind("./um:hotend", cls.__namespaces):
|
||||
hotend_id = hotend.get("id")
|
||||
if hotend_id is None:
|
||||
continue
|
||||
|
||||
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = hotend_id)
|
||||
if not variant_containers:
|
||||
# It is not really properly defined what "ID" is so also search for variants by name.
|
||||
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(definition = machine_id, name = hotend_id)
|
||||
|
||||
hotend_compatibility = machine_compatibility
|
||||
for entry in hotend.iterfind("./um:setting", cls.__namespaces):
|
||||
key = entry.get("key")
|
||||
if key == "hardware compatible":
|
||||
hotend_compatibility = cls._parseCompatibleValue(entry.text)
|
||||
|
||||
new_hotend_id = container_id + "_" + machine_id + "_" + hotend_id.replace(" ", "_")
|
||||
|
||||
# Same as machine compatibility, keep the derived material containers consistent with the parent
|
||||
# material
|
||||
found_materials = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = new_hotend_id)
|
||||
if found_materials:
|
||||
new_hotend_material_metadata = found_materials[0]
|
||||
else:
|
||||
new_hotend_material_metadata = {}
|
||||
|
||||
new_hotend_material_metadata.update(base_metadata)
|
||||
if variant_containers:
|
||||
new_hotend_material_metadata["variant"] = variant_containers[0]["id"]
|
||||
else:
|
||||
new_hotend_material_metadata["variant"] = hotend_id
|
||||
_with_missing_variants.append(new_hotend_material_metadata)
|
||||
new_hotend_material_metadata["compatible"] = hotend_compatibility
|
||||
new_hotend_material_metadata["machine_manufacturer"] = machine_manufacturer
|
||||
new_hotend_material_metadata["id"] = new_hotend_id
|
||||
new_hotend_material_metadata["definition"] = machine_id
|
||||
|
||||
if len(found_materials) == 0:
|
||||
result_metadata.append(new_hotend_material_metadata)
|
||||
|
||||
# there is only one ID for a machine. Once we have reached here, it means we have already found
|
||||
# a workable ID for that machine, so there is no need to continue
|
||||
break
|
||||
|
||||
return result_metadata
|
||||
|
||||
def _addSettingElement(self, builder, instance):
|
||||
key = instance.definition.key
|
||||
if key in self.__material_settings_setting_map.values():
|
||||
|
@ -685,16 +849,58 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
builder.data(str(instance.value))
|
||||
builder.end(tag_name)
|
||||
|
||||
def _profile_name(self, material_name, color_name):
|
||||
@classmethod
|
||||
def _profile_name(cls, material_name, color_name):
|
||||
if color_name != "Generic":
|
||||
return "%s %s" % (color_name, material_name)
|
||||
else:
|
||||
return material_name
|
||||
|
||||
@classmethod
|
||||
def getPossibleDefinitionIDsFromName(cls, name):
|
||||
name_parts = name.lower().split(" ")
|
||||
merged_name_parts = []
|
||||
for part in name_parts:
|
||||
if len(part) == 0:
|
||||
continue
|
||||
if len(merged_name_parts) == 0:
|
||||
merged_name_parts.append(part)
|
||||
continue
|
||||
if part.isdigit():
|
||||
# for names with digit(s) such as Ultimaker 3 Extended, we generate an ID like
|
||||
# "ultimaker3_extended", ignoring the space between "Ultimaker" and "3".
|
||||
merged_name_parts[-1] = merged_name_parts[-1] + part
|
||||
else:
|
||||
merged_name_parts.append(part)
|
||||
|
||||
id_list = [name.lower().replace(" ", ""), # simply removing all spaces
|
||||
name.lower().replace(" ", "_"), # simply replacing all spaces with underscores
|
||||
"_".join(merged_name_parts),
|
||||
]
|
||||
|
||||
return id_list
|
||||
|
||||
## Gets a mapping from product names in the XML files to their definition
|
||||
# IDs.
|
||||
#
|
||||
# This loads the mapping from a file.
|
||||
@classmethod
|
||||
def getProductIdMap(cls) -> Dict[str, List[str]]:
|
||||
product_to_id_file = os.path.join(os.path.dirname(sys.modules[cls.__module__].__file__), "product_to_id.json")
|
||||
with open(product_to_id_file) as f:
|
||||
product_to_id_map = json.load(f)
|
||||
product_to_id_map = {key: [value] for key, value in product_to_id_map.items()}
|
||||
return product_to_id_map
|
||||
|
||||
## Parse the value of the "material compatible" property.
|
||||
def _parseCompatibleValue(self, value: str):
|
||||
@classmethod
|
||||
def _parseCompatibleValue(cls, value: str):
|
||||
return value in {"yes", "unknown"}
|
||||
|
||||
## Small string representation for debugging.
|
||||
def __str__(self):
|
||||
return "<XmlMaterialProfile '{my_id}' ('{name}') from base file '{base_file}'>".format(my_id = self.getId(), name = self.getName(), base_file = self.getMetaDataEntry("base_file"))
|
||||
|
||||
# Map XML file setting names to internal names
|
||||
__material_settings_setting_map = {
|
||||
"print temperature": "default_material_print_temperature",
|
||||
|
@ -744,4 +950,22 @@ def _indent(elem, level = 0):
|
|||
# We are only interested in the actual tag name, so discard everything
|
||||
# before the last }
|
||||
def _tag_without_namespace(element):
|
||||
return element.tag[element.tag.rfind("}") + 1:]
|
||||
return element.tag[element.tag.rfind("}") + 1:]
|
||||
|
||||
#While loading XML profiles, some of these profiles don't know what variant
|
||||
#they belong to. We'd like to search by the machine ID and the variant's
|
||||
#name, but we don't know the variant's ID. Not all variants have been loaded
|
||||
#yet so we can't run a filter on the name and machine. The ID is unknown
|
||||
#so we can't lazily load the variant either. So we have to wait until all
|
||||
#the rest is loaded properly and then assign the correct variant to the
|
||||
#material files that were missing it.
|
||||
_with_missing_variants = []
|
||||
def _fillMissingVariants():
|
||||
registry = ContainerRegistry.getInstance()
|
||||
for variant_metadata in _with_missing_variants:
|
||||
variants = registry.findContainersMetadata(definition = variant_metadata["definition"], name = variant_metadata["variant"])
|
||||
if not variants:
|
||||
Logger.log("w", "Could not find variant for variant-specific material {material_id}.".format(material_id = variant_metadata["id"]))
|
||||
continue
|
||||
variant_metadata["variant"] = variants[0]["id"]
|
||||
ContainerRegistry.allMetadataLoaded.connect(_fillMissingVariants)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue