ThreeMFWorkspaceReader.py 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876
  1. # Copyright (c) 2017 Ultimaker B.V.
  2. # Cura is released under the terms of the AGPLv3 or higher.
  3. from UM.Workspace.WorkspaceReader import WorkspaceReader
  4. from UM.Application import Application
  5. from UM.Logger import Logger
  6. from UM.i18n import i18nCatalog
  7. from UM.Settings.ContainerStack import ContainerStack
  8. from UM.Settings.DefinitionContainer import DefinitionContainer
  9. from UM.Settings.InstanceContainer import InstanceContainer
  10. from UM.Settings.ContainerRegistry import ContainerRegistry
  11. from UM.MimeTypeDatabase import MimeTypeDatabase
  12. from UM.Job import Job
  13. from UM.Preferences import Preferences
  14. from .WorkspaceDialog import WorkspaceDialog
  15. import xml.etree.ElementTree as ET
  16. from cura.Settings.ExtruderManager import ExtruderManager
  17. from cura.Settings.ExtruderStack import ExtruderStack
  18. from cura.Settings.GlobalStack import GlobalStack
  19. from configparser import ConfigParser
  20. import zipfile
  21. import io
  22. import configparser
  23. i18n_catalog = i18nCatalog("cura")
  24. ## Base implementation for reading 3MF workspace files.
  25. class ThreeMFWorkspaceReader(WorkspaceReader):
  26. def __init__(self):
  27. super().__init__()
  28. self._supported_extensions = [".3mf"]
  29. self._dialog = WorkspaceDialog()
  30. self._3mf_mesh_reader = None
  31. self._container_registry = ContainerRegistry.getInstance()
  32. # suffixes registered with the MineTypes don't start with a dot '.'
  33. self._definition_container_suffix = "." + ContainerRegistry.getMimeTypeForContainer(DefinitionContainer).preferredSuffix
  34. self._material_container_suffix = None # We have to wait until all other plugins are loaded before we can set it
  35. self._instance_container_suffix = "." + ContainerRegistry.getMimeTypeForContainer(InstanceContainer).preferredSuffix
  36. self._container_stack_suffix = "." + ContainerRegistry.getMimeTypeForContainer(ContainerStack).preferredSuffix
  37. self._extruder_stack_suffix = "." + ContainerRegistry.getMimeTypeForContainer(ExtruderStack).preferredSuffix
  38. self._global_stack_suffix = "." + ContainerRegistry.getMimeTypeForContainer(GlobalStack).preferredSuffix
  39. # Certain instance container types are ignored because we make the assumption that only we make those types
  40. # of containers. They are:
  41. # - quality
  42. # - variant
  43. self._ignored_instance_container_types = {"quality", "variant"}
  44. self._resolve_strategies = {}
  45. self._id_mapping = {}
  46. ## Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results.
  47. # This has nothing to do with speed, but with getting consistent new naming for instances & objects.
  48. def getNewId(self, old_id):
  49. if old_id not in self._id_mapping:
  50. self._id_mapping[old_id] = self._container_registry.uniqueName(old_id)
  51. return self._id_mapping[old_id]
  52. ## Separates the given file list into a list of GlobalStack files and a list of ExtruderStack files.
  53. #
  54. # In old versions, extruder stack files have the same suffix as container stack files ".stack.cfg".
  55. #
  56. def _determineGlobalAndExtruderStackFiles(self, project_file_name, file_list):
  57. archive = zipfile.ZipFile(project_file_name, "r")
  58. global_stack_file_list = [name for name in file_list if name.endswith(self._global_stack_suffix)]
  59. extruder_stack_file_list = [name for name in file_list if name.endswith(self._extruder_stack_suffix)]
  60. # separate container stack files and extruder stack files
  61. files_to_determine = [name for name in file_list if name.endswith(self._container_stack_suffix)]
  62. for file_name in files_to_determine:
  63. # FIXME: HACK!
  64. # We need to know the type of the stack file, but we can only know it if we deserialize it.
  65. # The default ContainerStack.deserialize() will connect signals, which is not desired in this case.
  66. # Since we know that the stack files are INI files, so we directly use the ConfigParser to parse them.
  67. serialized = archive.open(file_name).read().decode("utf-8")
  68. stack_config = ConfigParser()
  69. stack_config.read_string(serialized)
  70. # sanity check
  71. if not stack_config.has_option("metadata", "type"):
  72. Logger.log("e", "%s in %s doesn't seem to be valid stack file", file_name, project_file_name)
  73. continue
  74. stack_type = stack_config.get("metadata", "type")
  75. if stack_type == "extruder_train":
  76. extruder_stack_file_list.append(file_name)
  77. elif stack_type == "machine":
  78. global_stack_file_list.append(file_name)
  79. else:
  80. Logger.log("w", "Unknown container stack type '%s' from %s in %s",
  81. stack_type, file_name, project_file_name)
  82. if len(global_stack_file_list) != 1:
  83. raise RuntimeError("More than one global stack file found: [%s]" % str(global_stack_file_list))
  84. return global_stack_file_list[0], extruder_stack_file_list
  85. ## read some info so we can make decisions
  86. # \param file_name
  87. # \param show_dialog In case we use preRead() to check if a file is a valid project file, we don't want to show a dialog.
  88. def preRead(self, file_name, show_dialog=True, *args, **kwargs):
  89. self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name)
  90. if self._3mf_mesh_reader and self._3mf_mesh_reader.preRead(file_name) == WorkspaceReader.PreReadResult.accepted:
  91. pass
  92. else:
  93. Logger.log("w", "Could not find reader that was able to read the scene data for 3MF workspace")
  94. return WorkspaceReader.PreReadResult.failed
  95. machine_name = ""
  96. machine_type = ""
  97. variant_type_name = i18n_catalog.i18nc("@label", "Nozzle")
  98. # Check if there are any conflicts, so we can ask the user.
  99. archive = zipfile.ZipFile(file_name, "r")
  100. cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")]
  101. # A few lists of containers in this project files.
  102. # When loading the global stack file, it may be associated with those containers, which may or may not be
  103. # in Cura already, so we need to provide them as alternative search lists.
  104. definition_container_list = []
  105. instance_container_list = []
  106. material_container_list = []
  107. #
  108. # Read definition containers
  109. #
  110. machine_definition_container_count = 0
  111. extruder_definition_container_count = 0
  112. definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
  113. for each_definition_container_file in definition_container_files:
  114. container_id = self._stripFileToId(each_definition_container_file)
  115. definitions = self._container_registry.findDefinitionContainers(id=container_id)
  116. if not definitions:
  117. definition_container = DefinitionContainer(container_id)
  118. definition_container.deserialize(archive.open(each_definition_container_file).read().decode("utf-8"))
  119. else:
  120. definition_container = definitions[0]
  121. definition_container_list.append(definition_container)
  122. definition_container_type = definition_container.getMetaDataEntry("type")
  123. if definition_container_type == "machine":
  124. machine_type = definition_container.getName()
  125. variant_type_name = definition_container.getMetaDataEntry("variants_name", variant_type_name)
  126. machine_definition_container_count += 1
  127. elif definition_container_type == "extruder":
  128. extruder_definition_container_count += 1
  129. else:
  130. Logger.log("w", "Unknown definition container type %s for %s",
  131. definition_container_type, each_definition_container_file)
  132. Job.yieldThread()
  133. # sanity check
  134. if machine_definition_container_count != 1:
  135. msg = "Expecting one machine definition container but got %s" % machine_definition_container_count
  136. Logger.log("e", msg)
  137. raise RuntimeError(msg)
  138. material_labels = []
  139. material_conflict = False
  140. xml_material_profile = self._getXmlProfileClass()
  141. if self._material_container_suffix is None:
  142. self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).preferredSuffix
  143. if xml_material_profile:
  144. material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
  145. for material_container_file in material_container_files:
  146. container_id = self._stripFileToId(material_container_file)
  147. materials = self._container_registry.findInstanceContainers(id=container_id)
  148. material_labels.append(self._getMaterialLabelFromSerialized(archive.open(material_container_file).read().decode("utf-8")))
  149. if materials and not materials[0].isReadOnly(): # Only non readonly materials can be in conflict
  150. material_conflict = True
  151. Job.yieldThread()
  152. # Check if any quality_changes instance container is in conflict.
  153. instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)]
  154. quality_name = ""
  155. quality_type = ""
  156. num_settings_overriden_by_quality_changes = 0 # How many settings are changed by the quality changes
  157. num_settings_overriden_by_definition_changes = 0 # How many settings are changed by the definition changes
  158. num_user_settings = 0
  159. quality_changes_conflict = False
  160. definition_changes_conflict = False
  161. for each_instance_container_file in instance_container_files:
  162. container_id = self._stripFileToId(each_instance_container_file)
  163. instance_container = InstanceContainer(container_id)
  164. # Deserialize InstanceContainer by converting read data from bytes to string
  165. instance_container.deserialize(archive.open(each_instance_container_file).read().decode("utf-8"))
  166. instance_container_list.append(instance_container)
  167. container_type = instance_container.getMetaDataEntry("type")
  168. if container_type == "quality_changes":
  169. quality_name = instance_container.getName()
  170. num_settings_overriden_by_quality_changes += len(instance_container._instances)
  171. # Check if quality changes already exists.
  172. quality_changes = self._container_registry.findInstanceContainers(id = container_id)
  173. if quality_changes:
  174. # Check if there really is a conflict by comparing the values
  175. if quality_changes[0] != instance_container:
  176. quality_changes_conflict = True
  177. elif container_type == "definition_changes":
  178. definition_name = instance_container.getName()
  179. num_settings_overriden_by_definition_changes += len(instance_container._instances)
  180. definition_changes = self._container_registry.findDefinitionContainers(id = container_id)
  181. if definition_changes:
  182. if definition_changes[0] != instance_container:
  183. definition_changes_conflict = True
  184. elif container_type == "user":
  185. num_user_settings += len(instance_container._instances)
  186. elif container_type in self._ignored_instance_container_types:
  187. # Ignore certain instance container types
  188. Logger.log("w", "Ignoring instance container [%s] with type [%s]", container_id, container_type)
  189. continue
  190. Job.yieldThread()
  191. # Load ContainerStack files and ExtruderStack files
  192. global_stack_file, extruder_stack_files = self._determineGlobalAndExtruderStackFiles(
  193. file_name, cura_file_names)
  194. self._resolve_strategies = {"machine": None, "quality_changes": None, "material": None}
  195. machine_conflict = False
  196. for container_stack_file in [global_stack_file] + extruder_stack_files:
  197. container_id = self._stripFileToId(container_stack_file)
  198. serialized = archive.open(container_stack_file).read().decode("utf-8")
  199. if machine_name == "":
  200. machine_name = self._getMachineNameFromSerializedStack(serialized)
  201. stacks = self._container_registry.findContainerStacks(id = container_id)
  202. if stacks:
  203. # Check if there are any changes at all in any of the container stacks.
  204. id_list = self._getContainerIdListFromSerialized(serialized)
  205. for index, container_id in enumerate(id_list):
  206. if stacks[0].getContainer(index).getId() != container_id:
  207. machine_conflict = True
  208. Job.yieldThread()
  209. num_visible_settings = 0
  210. try:
  211. temp_preferences = Preferences()
  212. temp_preferences.readFromFile(io.TextIOWrapper(archive.open("Cura/preferences.cfg"))) # We need to wrap it, else the archive parser breaks.
  213. visible_settings_string = temp_preferences.getValue("general/visible_settings")
  214. if visible_settings_string is not None:
  215. num_visible_settings = len(visible_settings_string.split(";"))
  216. active_mode = temp_preferences.getValue("cura/active_mode")
  217. if not active_mode:
  218. active_mode = Preferences.getInstance().getValue("cura/active_mode")
  219. except KeyError:
  220. # If there is no preferences file, it's not a workspace, so notify user of failure.
  221. Logger.log("w", "File %s is not a valid workspace.", file_name)
  222. return WorkspaceReader.PreReadResult.failed
  223. # In case we use preRead() to check if a file is a valid project file, we don't want to show a dialog.
  224. if not show_dialog:
  225. return WorkspaceReader.PreReadResult.accepted
  226. # prepare data for the dialog
  227. num_extruders = extruder_definition_container_count
  228. if num_extruders == 0:
  229. num_extruders = 1 # No extruder stacks found, which means there is one extruder
  230. extruders = num_extruders * [""]
  231. # Show the dialog, informing the user what is about to happen.
  232. self._dialog.setMachineConflict(machine_conflict)
  233. self._dialog.setQualityChangesConflict(quality_changes_conflict)
  234. self._dialog.setDefinitionChangesConflict(definition_changes_conflict)
  235. self._dialog.setMaterialConflict(material_conflict)
  236. self._dialog.setNumVisibleSettings(num_visible_settings)
  237. self._dialog.setQualityName(quality_name)
  238. self._dialog.setQualityType(quality_type)
  239. self._dialog.setNumSettingsOverridenByQualityChanges(num_settings_overriden_by_quality_changes)
  240. self._dialog.setNumUserSettings(num_user_settings)
  241. self._dialog.setActiveMode(active_mode)
  242. self._dialog.setMachineName(machine_name)
  243. self._dialog.setMaterialLabels(material_labels)
  244. self._dialog.setMachineType(machine_type)
  245. self._dialog.setExtruders(extruders)
  246. self._dialog.setVariantType(variant_type_name)
  247. self._dialog.setHasObjectsOnPlate(Application.getInstance().platformActivity)
  248. self._dialog.show()
  249. # Block until the dialog is closed.
  250. self._dialog.waitForClose()
  251. if self._dialog.getResult() == {}:
  252. return WorkspaceReader.PreReadResult.cancelled
  253. self._resolve_strategies = self._dialog.getResult()
  254. #
  255. # There can be 3 resolve strategies coming from the dialog:
  256. # - new: create a new container
  257. # - override: override the existing container
  258. # - None: There is no conflict, which means containers with the same IDs may or may not be there already.
  259. # If they are there, there is no conflict between the them.
  260. # In this case, you can either create a new one, or safely override the existing one.
  261. #
  262. # Default values
  263. for k, v in self._resolve_strategies.items():
  264. if v is None:
  265. self._resolve_strategies[k] = "new"
  266. return WorkspaceReader.PreReadResult.accepted
  267. ## Overrides an ExtruderStack in the given GlobalStack and returns the new ExtruderStack.
  268. def _overrideExtruderStack(self, global_stack, extruder_index, extruder_file_content):
  269. extruder_stack = global_stack.extruders[extruder_index]
  270. machine_extruder_count = len(global_stack.extruders)
  271. old_extruder_stack_id = extruder_stack.getId()
  272. # HACK: There are two cases:
  273. # - the new ExtruderStack has the same ID as the one we are overriding
  274. # - they don't have the same ID
  275. # In the second case, directly overriding the existing ExtruderStack will leave the old stack file
  276. # in the Cura directory, and this will cause a problem when we restart Cura. So, we always delete
  277. # the existing file first.
  278. self._container_registry._deleteFiles(extruder_stack)
  279. # override the given extruder stack
  280. extruder_stack.deserialize(extruder_file_content)
  281. # HACK: The deserialize() of ExtruderStack will add itself to the GlobalStack, which is redundant here.
  282. # So we need to remove the new entries in the GlobalStack.
  283. global_stack._extruders = global_stack._extruders[:machine_extruder_count]
  284. # HACK: clean and fill the container query cache again
  285. if old_extruder_stack_id in self._container_registry._id_container_cache:
  286. del self._container_registry._id_container_cache[old_extruder_stack_id]
  287. new_extruder_stack_id = extruder_stack.getId()
  288. self._container_registry._id_container_cache[new_extruder_stack_id] = extruder_stack
  289. # return the new ExtruderStack
  290. return extruder_stack
  291. ## Read the project file
  292. # Add all the definitions / materials / quality changes that do not exist yet. Then it loads
  293. # all the stacks into the container registry. In some cases it will reuse the container for the global stack.
  294. # It handles old style project files containing .stack.cfg as well as new style project files
  295. # containing global.cfg / extruder.cfg
  296. #
  297. # \param file_name
  298. def read(self, file_name):
  299. archive = zipfile.ZipFile(file_name, "r")
  300. cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")]
  301. # Create a shadow copy of the preferences (we don't want all of the preferences, but we do want to re-use its
  302. # parsing code.
  303. temp_preferences = Preferences()
  304. temp_preferences.readFromFile(io.TextIOWrapper(archive.open("Cura/preferences.cfg"))) # We need to wrap it, else the archive parser breaks.
  305. # Copy a number of settings from the temp preferences to the global
  306. global_preferences = Preferences.getInstance()
  307. visible_settings = temp_preferences.getValue("general/visible_settings")
  308. if visible_settings is None:
  309. Logger.log("w", "Workspace did not contain visible settings. Leaving visibility unchanged")
  310. else:
  311. global_preferences.setValue("general/visible_settings", visible_settings)
  312. categories_expanded = temp_preferences.getValue("cura/categories_expanded")
  313. if categories_expanded is None:
  314. Logger.log("w", "Workspace did not contain expanded categories. Leaving them unchanged")
  315. else:
  316. global_preferences.setValue("cura/categories_expanded", categories_expanded)
  317. Application.getInstance().expandedCategoriesChanged.emit() # Notify the GUI of the change
  318. self._id_mapping = {}
  319. # We don't add containers right away, but wait right until right before the stack serialization.
  320. # We do this so that if something goes wrong, it's easier to clean up.
  321. containers_to_add = []
  322. global_stack_file, extruder_stack_files = self._determineGlobalAndExtruderStackFiles(file_name, cura_file_names)
  323. global_stack = None
  324. extruder_stacks = []
  325. extruder_stacks_added = []
  326. container_stacks_added = []
  327. containers_added = []
  328. global_stack_id_original = self._stripFileToId(global_stack_file)
  329. global_stack_id_new = global_stack_id_original
  330. global_stack_need_rename = False
  331. extruder_stack_id_map = {} # new and old ExtruderStack IDs map
  332. if self._resolve_strategies["machine"] == "new":
  333. # We need a new id if the id already exists
  334. if self._container_registry.findContainerStacks(id = global_stack_id_original):
  335. global_stack_id_new = self.getNewId(global_stack_id_original)
  336. global_stack_need_rename = True
  337. for each_extruder_stack_file in extruder_stack_files:
  338. old_container_id = self._stripFileToId(each_extruder_stack_file)
  339. new_container_id = old_container_id
  340. if self._container_registry.findContainerStacks(id = old_container_id):
  341. # get a new name for this extruder
  342. new_container_id = self.getNewId(old_container_id)
  343. extruder_stack_id_map[old_container_id] = new_container_id
  344. # TODO: For the moment we use pretty naive existence checking. If the ID is the same, we assume in quite a few
  345. # TODO: cases that the container loaded is the same (most notable in materials & definitions).
  346. # TODO: It might be possible that we need to add smarter checking in the future.
  347. Logger.log("d", "Workspace loading is checking definitions...")
  348. # Get all the definition files & check if they exist. If not, add them.
  349. definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
  350. for definition_container_file in definition_container_files:
  351. container_id = self._stripFileToId(definition_container_file)
  352. definitions = self._container_registry.findDefinitionContainers(id = container_id)
  353. if not definitions:
  354. definition_container = DefinitionContainer(container_id)
  355. definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"))
  356. self._container_registry.addContainer(definition_container)
  357. Job.yieldThread()
  358. Logger.log("d", "Workspace loading is checking materials...")
  359. material_containers = []
  360. # Get all the material files and check if they exist. If not, add them.
  361. xml_material_profile = self._getXmlProfileClass()
  362. if self._material_container_suffix is None:
  363. self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0]
  364. if xml_material_profile:
  365. material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
  366. for material_container_file in material_container_files:
  367. container_id = self._stripFileToId(material_container_file)
  368. materials = self._container_registry.findInstanceContainers(id = container_id)
  369. if not materials:
  370. material_container = xml_material_profile(container_id)
  371. material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"))
  372. containers_to_add.append(material_container)
  373. else:
  374. material_container = materials[0]
  375. if not material_container.isReadOnly(): # Only create new materials if they are not read only.
  376. if self._resolve_strategies["material"] == "override":
  377. material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"))
  378. elif self._resolve_strategies["material"] == "new":
  379. # Note that we *must* deserialize it with a new ID, as multiple containers will be
  380. # auto created & added.
  381. material_container = xml_material_profile(self.getNewId(container_id))
  382. material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"))
  383. containers_to_add.append(material_container)
  384. material_containers.append(material_container)
  385. Job.yieldThread()
  386. Logger.log("d", "Workspace loading is checking instance containers...")
  387. # Get quality_changes and user profiles saved in the workspace
  388. instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)]
  389. user_instance_containers = []
  390. quality_and_definition_changes_instance_containers = []
  391. for instance_container_file in instance_container_files:
  392. container_id = self._stripFileToId(instance_container_file)
  393. serialized = archive.open(instance_container_file).read().decode("utf-8")
  394. # HACK! we ignore the "metadata/type = quality" instance containers!
  395. parser = configparser.ConfigParser()
  396. parser.read_string(serialized)
  397. if not parser.has_option("metadata", "type"):
  398. Logger.log("w", "Cannot find metadata/type in %s, ignoring it", instance_container_file)
  399. continue
  400. if parser.get("metadata", "type") == "quality":
  401. continue
  402. instance_container = InstanceContainer(container_id)
  403. # Deserialize InstanceContainer by converting read data from bytes to string
  404. instance_container.deserialize(serialized)
  405. container_type = instance_container.getMetaDataEntry("type")
  406. Job.yieldThread()
  407. #
  408. # IMPORTANT:
  409. # If an instance container (or maybe other type of container) exists, and user chooses "Create New",
  410. # we need to rename this container and all references to it, and changing those references are VERY
  411. # HARD.
  412. #
  413. if container_type in self._ignored_instance_container_types:
  414. # Ignore certain instance container types
  415. Logger.log("w", "Ignoring instance container [%s] with type [%s]", container_id, container_type)
  416. continue
  417. elif container_type == "user":
  418. # Check if quality changes already exists.
  419. user_containers = self._container_registry.findInstanceContainers(id = container_id)
  420. if not user_containers:
  421. containers_to_add.append(instance_container)
  422. else:
  423. if self._resolve_strategies["machine"] == "override" or self._resolve_strategies["machine"] is None:
  424. instance_container = user_containers[0]
  425. instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8"))
  426. instance_container.setDirty(True)
  427. elif self._resolve_strategies["machine"] == "new":
  428. # The machine is going to get a spiffy new name, so ensure that the id's of user settings match.
  429. old_extruder_id = instance_container.getMetaDataEntry("extruder", None)
  430. if old_extruder_id:
  431. new_extruder_id = extruder_stack_id_map[old_extruder_id]
  432. new_id = new_extruder_id + "_current_settings"
  433. instance_container._id = new_id
  434. instance_container.setName(new_id)
  435. instance_container.setMetaDataEntry("extruder", new_extruder_id)
  436. containers_to_add.append(instance_container)
  437. machine_id = instance_container.getMetaDataEntry("machine", None)
  438. if machine_id:
  439. new_machine_id = self.getNewId(machine_id)
  440. new_id = new_machine_id + "_current_settings"
  441. instance_container._id = new_id
  442. instance_container.setName(new_id)
  443. instance_container.setMetaDataEntry("machine", new_machine_id)
  444. containers_to_add.append(instance_container)
  445. user_instance_containers.append(instance_container)
  446. elif container_type in ("quality_changes", "definition_changes"):
  447. # Check if quality changes already exists.
  448. changes_containers = self._container_registry.findInstanceContainers(id = container_id)
  449. if not changes_containers:
  450. # no existing containers with the same ID, so we can safely add the new one
  451. containers_to_add.append(instance_container)
  452. else:
  453. # we have found existing container with the same ID, so we need to resolve according to the
  454. # selected strategy.
  455. if self._resolve_strategies[container_type] == "override":
  456. instance_container = changes_containers[0]
  457. instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8"))
  458. instance_container.setDirty(True)
  459. elif self._resolve_strategies[container_type] == "new":
  460. # TODO: how should we handle the case "new" for quality_changes and definition_changes?
  461. instance_container.setName(self._container_registry.uniqueName(instance_container.getName()))
  462. new_changes_container_id = self.getNewId(instance_container.getId())
  463. instance_container._id = new_changes_container_id
  464. # TODO: we don't know the following is correct or not, need to verify
  465. # AND REFACTOR!!!
  466. if self._resolve_strategies["machine"] == "new":
  467. # The machine is going to get a spiffy new name, so ensure that the id's of user settings match.
  468. old_extruder_id = instance_container.getMetaDataEntry("extruder", None)
  469. if old_extruder_id:
  470. new_extruder_id = extruder_stack_id_map[old_extruder_id]
  471. instance_container.setMetaDataEntry("extruder", new_extruder_id)
  472. machine_id = instance_container.getMetaDataEntry("machine", None)
  473. if machine_id:
  474. new_machine_id = self.getNewId(machine_id)
  475. instance_container.setMetaDataEntry("machine", new_machine_id)
  476. containers_to_add.append(instance_container)
  477. elif self._resolve_strategies[container_type] is None:
  478. # The ID already exists, but nothing in the values changed, so do nothing.
  479. pass
  480. quality_and_definition_changes_instance_containers.append(instance_container)
  481. else:
  482. existing_container = self._container_registry.findInstanceContainers(id = container_id)
  483. if not existing_container:
  484. containers_to_add.append(instance_container)
  485. if global_stack_need_rename:
  486. if instance_container.getMetaDataEntry("machine"):
  487. instance_container.setMetaDataEntry("machine", global_stack_id_new)
  488. # Add all the containers right before we try to add / serialize the stack
  489. for container in containers_to_add:
  490. self._container_registry.addContainer(container)
  491. container.setDirty(True)
  492. containers_added.append(container)
  493. # Get the stack(s) saved in the workspace.
  494. Logger.log("d", "Workspace loading is checking stacks containers...")
  495. # --
  496. # load global stack file
  497. try:
  498. # Check if a stack by this ID already exists;
  499. container_stacks = self._container_registry.findContainerStacks(id = global_stack_id_original)
  500. if container_stacks:
  501. stack = container_stacks[0]
  502. if self._resolve_strategies["machine"] == "override":
  503. # TODO: HACK
  504. # There is a machine, check if it has authentication data. If so, keep that data.
  505. network_authentication_id = container_stacks[0].getMetaDataEntry("network_authentication_id")
  506. network_authentication_key = container_stacks[0].getMetaDataEntry("network_authentication_key")
  507. container_stacks[0].deserialize(archive.open(global_stack_file).read().decode("utf-8"))
  508. if network_authentication_id:
  509. container_stacks[0].addMetaDataEntry("network_authentication_id", network_authentication_id)
  510. if network_authentication_key:
  511. container_stacks[0].addMetaDataEntry("network_authentication_key", network_authentication_key)
  512. elif self._resolve_strategies["machine"] == "new":
  513. stack = GlobalStack(global_stack_id_new)
  514. stack.deserialize(archive.open(global_stack_file).read().decode("utf-8"))
  515. # Ensure a unique ID and name
  516. stack._id = global_stack_id_new
  517. # Extruder stacks are "bound" to a machine. If we add the machine as a new one, the id of the
  518. # bound machine also needs to change.
  519. if stack.getMetaDataEntry("machine", None):
  520. stack.setMetaDataEntry("machine", global_stack_id_new)
  521. # Only machines need a new name, stacks may be non-unique
  522. stack.setName(self._container_registry.uniqueName(stack.getName()))
  523. container_stacks_added.append(stack)
  524. self._container_registry.addContainer(stack)
  525. else:
  526. Logger.log("w", "Resolve strategy of %s for machine is not supported", self._resolve_strategies["machine"])
  527. else:
  528. # no existing container stack, so we create a new one
  529. stack = GlobalStack(global_stack_id_new)
  530. # Deserialize stack by converting read data from bytes to string
  531. stack.deserialize(archive.open(global_stack_file).read().decode("utf-8"))
  532. container_stacks_added.append(stack)
  533. self._container_registry.addContainer(stack)
  534. containers_added.append(stack)
  535. global_stack = stack
  536. Job.yieldThread()
  537. except:
  538. Logger.logException("w", "We failed to serialize the stack. Trying to clean up.")
  539. # Something went really wrong. Try to remove any data that we added.
  540. for container in containers_added:
  541. self._container_registry.removeContainer(container.getId())
  542. return
  543. # --
  544. # load extruder stack files
  545. try:
  546. for index, extruder_stack_file in enumerate(extruder_stack_files):
  547. container_id = self._stripFileToId(extruder_stack_file)
  548. extruder_file_content = archive.open(extruder_stack_file, "r").read().decode("utf-8")
  549. container_stacks = self._container_registry.findContainerStacks(id = container_id)
  550. if container_stacks:
  551. # this container stack already exists, try to resolve
  552. stack = container_stacks[0]
  553. if self._resolve_strategies["machine"] == "override":
  554. # NOTE: This is the same code as those in the lower part
  555. # deserialize new extruder stack over the current ones
  556. stack = self._overrideExtruderStack(global_stack, index, extruder_file_content)
  557. elif self._resolve_strategies["machine"] == "new":
  558. # create a new extruder stack from this one
  559. new_id = extruder_stack_id_map[container_id]
  560. stack = ExtruderStack(new_id)
  561. # HACK: the global stack can have a new name, so we need to make sure that this extruder stack
  562. # references to the new name instead of the old one. Normally, this can be done after
  563. # deserialize() by setting the metadata, but in the case of ExtruderStack, deserialize()
  564. # also does addExtruder() to its machine stack, so we have to make sure that it's pointing
  565. # to the right machine BEFORE deserialization.
  566. extruder_config = configparser.ConfigParser()
  567. extruder_config.read_string(extruder_file_content)
  568. extruder_config.set("metadata", "machine", global_stack_id_new)
  569. tmp_string_io = io.StringIO()
  570. extruder_config.write(tmp_string_io)
  571. extruder_file_content = tmp_string_io.getvalue()
  572. stack.deserialize(extruder_file_content)
  573. # Ensure a unique ID and name
  574. stack._id = new_id
  575. self._container_registry.addContainer(stack)
  576. extruder_stacks_added.append(stack)
  577. containers_added.append(stack)
  578. else:
  579. # No extruder stack with the same ID can be found
  580. if self._resolve_strategies["machine"] == "override":
  581. # deserialize new extruder stack over the current ones
  582. stack = self._overrideExtruderStack(global_stack, index, extruder_file_content)
  583. elif self._resolve_strategies["machine"] == "new":
  584. # container not found, create a new one
  585. stack = ExtruderStack(container_id)
  586. # HACK: the global stack can have a new name, so we need to make sure that this extruder stack
  587. # references to the new name instead of the old one. Normally, this can be done after
  588. # deserialize() by setting the metadata, but in the case of ExtruderStack, deserialize()
  589. # also does addExtruder() to its machine stack, so we have to make sure that it's pointing
  590. # to the right machine BEFORE deserialization.
  591. extruder_config = configparser.ConfigParser()
  592. extruder_config.read_string(extruder_file_content)
  593. extruder_config.set("metadata", "machine", global_stack_id_new)
  594. tmp_string_io = io.StringIO()
  595. extruder_config.write(tmp_string_io)
  596. extruder_file_content = tmp_string_io.getvalue()
  597. stack.deserialize(extruder_file_content)
  598. self._container_registry.addContainer(stack)
  599. extruder_stacks_added.append(stack)
  600. containers_added.append(stack)
  601. else:
  602. Logger.log("w", "Unknown resolve strategy: %s" % str(self._resolve_strategies["machine"]))
  603. extruder_stacks.append(stack)
  604. except:
  605. Logger.logException("w", "We failed to serialize the stack. Trying to clean up.")
  606. # Something went really wrong. Try to remove any data that we added.
  607. for container in containers_added:
  608. self._container_registry.removeContainer(container.getId())
  609. return
  610. #
  611. # Replacing the old containers if resolve is "new".
  612. # When resolve is "new", some containers will get renamed, so all the other containers that reference to those
  613. # MUST get updated too.
  614. #
  615. if self._resolve_strategies["machine"] == "new":
  616. # A new machine was made, but it was serialized with the wrong user container. Fix that now.
  617. for container in user_instance_containers:
  618. # replacing the container ID for user instance containers for the extruders
  619. extruder_id = container.getMetaDataEntry("extruder", None)
  620. if extruder_id:
  621. for extruder in extruder_stacks:
  622. if extruder.getId() == extruder_id:
  623. extruder.userChanges = container
  624. continue
  625. # replacing the container ID for user instance containers for the machine
  626. machine_id = container.getMetaDataEntry("machine", None)
  627. if machine_id:
  628. if global_stack.getId() == machine_id:
  629. global_stack.userChanges = container
  630. continue
  631. for changes_container_type in ("quality_changes", "definition_changes"):
  632. if self._resolve_strategies[changes_container_type] == "new":
  633. # Quality changes needs to get a new ID, added to registry and to the right stacks
  634. for each_changes_container in quality_and_definition_changes_instance_containers:
  635. # NOTE: The renaming and giving new IDs are possibly redundant because they are done in the
  636. # instance container loading part.
  637. new_id = each_changes_container.getId()
  638. # Find the old (current) changes container in the global stack
  639. if changes_container_type == "quality_changes":
  640. old_container = global_stack.qualityChanges
  641. elif changes_container_type == "definition_changes":
  642. old_container = global_stack.definitionChanges
  643. # sanity checks
  644. # NOTE: The following cases SHOULD NOT happen!!!!
  645. if not old_container:
  646. Logger.log("e", "We try to get [%s] from the global stack [%s] but we got None instead!",
  647. changes_container_type, global_stack.getId())
  648. # Replace the quality/definition changes container if it's in the GlobalStack
  649. # NOTE: we can get an empty container here, but the IDs will not match,
  650. # so this comparison is fine.
  651. if self._id_mapping.get(old_container.getId()) == new_id:
  652. if changes_container_type == "quality_changes":
  653. global_stack.qualityChanges = each_changes_container
  654. elif changes_container_type == "definition_changes":
  655. global_stack.definitionChanges = each_changes_container
  656. continue
  657. # Replace the quality/definition changes container if it's in one of the ExtruderStacks
  658. for each_extruder_stack in extruder_stacks:
  659. changes_container = None
  660. if changes_container_type == "quality_changes":
  661. changes_container = each_extruder_stack.qualityChanges
  662. elif changes_container_type == "definition_changes":
  663. changes_container = each_extruder_stack.definitionChanges
  664. # sanity checks
  665. # NOTE: The following cases SHOULD NOT happen!!!!
  666. if not changes_container:
  667. Logger.log("e", "We try to get [%s] from the extruder stack [%s] but we got None instead!",
  668. changes_container_type, each_extruder_stack.getId())
  669. # NOTE: we can get an empty container here, but the IDs will not match,
  670. # so this comparison is fine.
  671. if self._id_mapping.get(changes_container.getId()) == new_id:
  672. if changes_container_type == "quality_changes":
  673. each_extruder_stack.qualityChanges = each_changes_container
  674. elif changes_container_type == "definition_changes":
  675. each_extruder_stack.definitionChanges = each_changes_container
  676. if self._resolve_strategies["material"] == "new":
  677. for each_material in material_containers:
  678. old_material = global_stack.material
  679. # check if the old material container has been renamed to this material container ID
  680. # if the container hasn't been renamed, we do nothing.
  681. new_id = self._id_mapping.get(old_material.getId())
  682. if new_id is None or new_id != each_material.getId():
  683. continue
  684. if old_material.getId() in self._id_mapping:
  685. global_stack.material = each_material
  686. for each_extruder_stack in extruder_stacks:
  687. old_material = each_extruder_stack.material
  688. # check if the old material container has been renamed to this material container ID
  689. # if the container hasn't been renamed, we do nothing.
  690. new_id = self._id_mapping.get(old_material.getId())
  691. if new_id is None or new_id != each_material.getId():
  692. continue
  693. if old_material.getId() in self._id_mapping:
  694. each_extruder_stack.material = each_material
  695. if extruder_stacks:
  696. for stack in extruder_stacks:
  697. ExtruderManager.getInstance().registerExtruder(stack, global_stack.getId())
  698. else:
  699. # Machine has no extruders, but it needs to be registered with the extruder manager.
  700. ExtruderManager.getInstance().registerExtruder(None, global_stack.getId())
  701. Logger.log("d", "Workspace loading is notifying rest of the code of changes...")
  702. if self._resolve_strategies["machine"] == "new":
  703. for stack in extruder_stacks:
  704. stack.setNextStack(global_stack)
  705. stack.containersChanged.emit(stack.getTop())
  706. # Actually change the active machine.
  707. Application.getInstance().setGlobalContainerStack(global_stack)
  708. # Notify everything/one that is to notify about changes.
  709. global_stack.containersChanged.emit(global_stack.getTop())
  710. # Load all the nodes / meshdata of the workspace
  711. nodes = self._3mf_mesh_reader.read(file_name)
  712. if nodes is None:
  713. nodes = []
  714. return nodes
  715. def _stripFileToId(self, file):
  716. mime_type = MimeTypeDatabase.getMimeTypeForFile(file)
  717. file = mime_type.stripExtension(file)
  718. return file.replace("Cura/", "")
  719. def _getXmlProfileClass(self):
  720. return self._container_registry.getContainerForMimeType(MimeTypeDatabase.getMimeType("application/x-ultimaker-material-profile"))
  721. ## Get the list of ID's of all containers in a container stack by partially parsing it's serialized data.
  722. def _getContainerIdListFromSerialized(self, serialized):
  723. parser = configparser.ConfigParser(interpolation=None, empty_lines_in_values=False)
  724. parser.read_string(serialized)
  725. container_ids = []
  726. if "containers" in parser:
  727. for index, container_id in parser.items("containers"):
  728. container_ids.append(container_id)
  729. elif parser.has_option("general", "containers"):
  730. container_string = parser["general"].get("containers", "")
  731. container_list = container_string.split(",")
  732. container_ids = [container_id for container_id in container_list if container_id != ""]
  733. return container_ids
  734. def _getMachineNameFromSerializedStack(self, serialized):
  735. parser = configparser.ConfigParser(interpolation=None, empty_lines_in_values=False)
  736. parser.read_string(serialized)
  737. return parser["general"].get("name", "")
  738. def _getMaterialLabelFromSerialized(self, serialized):
  739. data = ET.fromstring(serialized)
  740. metadata = data.iterfind("./um:metadata/um:name/um:label", {"um": "http://www.ultimaker.com/material"})
  741. for entry in metadata:
  742. return entry.text
  743. pass