ThreeMFWorkspaceReader.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416
  1. from UM.Workspace.WorkspaceReader import WorkspaceReader
  2. from UM.Application import Application
  3. from UM.Logger import Logger
  4. from UM.i18n import i18nCatalog
  5. from UM.Settings.ContainerStack import ContainerStack
  6. from UM.Settings.DefinitionContainer import DefinitionContainer
  7. from UM.Settings.InstanceContainer import InstanceContainer
  8. from UM.Settings.ContainerRegistry import ContainerRegistry
  9. from UM.MimeTypeDatabase import MimeTypeDatabase
  10. from UM.Job import Job
  11. from UM.Preferences import Preferences
  12. from .WorkspaceDialog import WorkspaceDialog
  13. from cura.Settings.ExtruderManager import ExtruderManager
  14. import zipfile
  15. import io
  16. import configparser
  17. i18n_catalog = i18nCatalog("cura")
  18. ## Base implementation for reading 3MF workspace files.
  19. class ThreeMFWorkspaceReader(WorkspaceReader):
  20. def __init__(self):
  21. super().__init__()
  22. self._supported_extensions = [".3mf"]
  23. self._dialog = WorkspaceDialog()
  24. self._3mf_mesh_reader = None
  25. self._container_registry = ContainerRegistry.getInstance()
  26. self._definition_container_suffix = ContainerRegistry.getMimeTypeForContainer(DefinitionContainer).preferredSuffix
  27. self._material_container_suffix = None # We have to wait until all other plugins are loaded before we can set it
  28. self._instance_container_suffix = ContainerRegistry.getMimeTypeForContainer(InstanceContainer).preferredSuffix
  29. self._container_stack_suffix = ContainerRegistry.getMimeTypeForContainer(ContainerStack).preferredSuffix
  30. self._resolve_strategies = {}
  31. self._id_mapping = {}
  32. ## Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results.
  33. # This has nothing to do with speed, but with getting consistent new naming for instances & objects.
  34. def getNewId(self, old_id):
  35. if old_id not in self._id_mapping:
  36. self._id_mapping[old_id] = self._container_registry.uniqueName(old_id)
  37. return self._id_mapping[old_id]
  38. def preRead(self, file_name):
  39. self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name)
  40. if self._3mf_mesh_reader and self._3mf_mesh_reader.preRead(file_name) == WorkspaceReader.PreReadResult.accepted:
  41. pass
  42. else:
  43. Logger.log("w", "Could not find reader that was able to read the scene data for 3MF workspace")
  44. return WorkspaceReader.PreReadResult.failed
  45. # Check if there are any conflicts, so we can ask the user.
  46. archive = zipfile.ZipFile(file_name, "r")
  47. cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")]
  48. container_stack_files = [name for name in cura_file_names if name.endswith(self._container_stack_suffix)]
  49. self._resolve_strategies = {"machine": None, "quality_changes": None, "material": None}
  50. machine_conflict = False
  51. quality_changes_conflict = False
  52. for container_stack_file in container_stack_files:
  53. container_id = self._stripFileToId(container_stack_file)
  54. stacks = self._container_registry.findContainerStacks(id=container_id)
  55. if stacks:
  56. # Check if there are any changes at all in any of the container stacks.
  57. id_list = self._getContainerIdListFromSerialized(archive.open(container_stack_file).read().decode("utf-8"))
  58. for index, container_id in enumerate(id_list):
  59. if stacks[0].getContainer(index).getId() != container_id:
  60. machine_conflict = True
  61. break
  62. Job.yieldThread()
  63. material_conflict = False
  64. xml_material_profile = self._getXmlProfileClass()
  65. if self._material_container_suffix is None:
  66. self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).preferredSuffix
  67. if xml_material_profile:
  68. material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
  69. for material_container_file in material_container_files:
  70. container_id = self._stripFileToId(material_container_file)
  71. materials = self._container_registry.findInstanceContainers(id=container_id)
  72. if materials and not materials[0].isReadOnly(): # Only non readonly materials can be in conflict
  73. material_conflict = True
  74. Job.yieldThread()
  75. # Check if any quality_changes instance container is in conflict.
  76. instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)]
  77. for instance_container_file in instance_container_files:
  78. container_id = self._stripFileToId(instance_container_file)
  79. instance_container = InstanceContainer(container_id)
  80. # Deserialize InstanceContainer by converting read data from bytes to string
  81. instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8"))
  82. container_type = instance_container.getMetaDataEntry("type")
  83. if container_type == "quality_changes":
  84. # Check if quality changes already exists.
  85. quality_changes = self._container_registry.findInstanceContainers(id = container_id)
  86. if quality_changes:
  87. # Check if there really is a conflict by comparing the values
  88. if quality_changes[0] != instance_container:
  89. quality_changes_conflict = True
  90. break
  91. Job.yieldThread()
  92. try:
  93. archive.open("Cura/preferences.cfg")
  94. except KeyError:
  95. # If there is no preferences file, it's not a workspace, so notify user of failure.
  96. Logger.log("w", "File %s is not a valid workspace.", file_name)
  97. return WorkspaceReader.PreReadResult.failed
  98. if machine_conflict or quality_changes_conflict or material_conflict:
  99. # There is a conflict; User should choose to either update the existing data, add everything as new data or abort
  100. self._dialog.setMachineConflict(machine_conflict)
  101. self._dialog.setQualityChangesConflict(quality_changes_conflict)
  102. self._dialog.setMaterialConflict(material_conflict)
  103. self._dialog.show()
  104. # Block until the dialog is closed.
  105. self._dialog.waitForClose()
  106. if self._dialog.getResult() == {}:
  107. return WorkspaceReader.PreReadResult.cancelled
  108. self._resolve_strategies = self._dialog.getResult()
  109. return WorkspaceReader.PreReadResult.accepted
  110. def read(self, file_name):
  111. # Load all the nodes / meshdata of the workspace
  112. nodes = self._3mf_mesh_reader.read(file_name)
  113. if nodes is None:
  114. nodes = []
  115. archive = zipfile.ZipFile(file_name, "r")
  116. cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")]
  117. # Create a shadow copy of the preferences (we don't want all of the preferences, but we do want to re-use its
  118. # parsing code.
  119. temp_preferences = Preferences()
  120. temp_preferences.readFromFile(io.TextIOWrapper(archive.open("Cura/preferences.cfg"))) # We need to wrap it, else the archive parser breaks.
  121. # Copy a number of settings from the temp preferences to the global
  122. global_preferences = Preferences.getInstance()
  123. visible_settings = temp_preferences.getValue("general/visible_settings")
  124. if visible_settings is None:
  125. Logger.log("w", "Workspace did not contain visible settings. Leaving visibility unchanged")
  126. else:
  127. global_preferences.setValue("general/visible_settings", visible_settings)
  128. categories_expanded = temp_preferences.getValue("cura/categories_expanded")
  129. if categories_expanded is None:
  130. Logger.log("w", "Workspace did not contain expanded categories. Leaving them unchanged")
  131. else:
  132. global_preferences.setValue("cura/categories_expanded", categories_expanded)
  133. Application.getInstance().expandedCategoriesChanged.emit() # Notify the GUI of the change
  134. self._id_mapping = {}
  135. # We don't add containers right away, but wait right until right before the stack serialization.
  136. # We do this so that if something goes wrong, it's easier to clean up.
  137. containers_to_add = []
  138. # TODO: For the moment we use pretty naive existence checking. If the ID is the same, we assume in quite a few
  139. # TODO: cases that the container loaded is the same (most notable in materials & definitions).
  140. # TODO: It might be possible that we need to add smarter checking in the future.
  141. Logger.log("d", "Workspace loading is checking definitions...")
  142. # Get all the definition files & check if they exist. If not, add them.
  143. definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
  144. for definition_container_file in definition_container_files:
  145. container_id = self._stripFileToId(definition_container_file)
  146. definitions = self._container_registry.findDefinitionContainers(id=container_id)
  147. if not definitions:
  148. definition_container = DefinitionContainer(container_id)
  149. definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"))
  150. self._container_registry.addContainer(definition_container)
  151. Job.yieldThread()
  152. Logger.log("d", "Workspace loading is checking materials...")
  153. material_containers = []
  154. # Get all the material files and check if they exist. If not, add them.
  155. xml_material_profile = self._getXmlProfileClass()
  156. if self._material_container_suffix is None:
  157. self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0]
  158. if xml_material_profile:
  159. material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
  160. for material_container_file in material_container_files:
  161. container_id = self._stripFileToId(material_container_file)
  162. materials = self._container_registry.findInstanceContainers(id=container_id)
  163. if not materials:
  164. material_container = xml_material_profile(container_id)
  165. material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"))
  166. containers_to_add.append(material_container)
  167. else:
  168. if not materials[0].isReadOnly(): # Only create new materials if they are not read only.
  169. if self._resolve_strategies["material"] == "override":
  170. materials[0].deserialize(archive.open(material_container_file).read().decode("utf-8"))
  171. elif self._resolve_strategies["material"] == "new":
  172. # Note that we *must* deserialize it with a new ID, as multiple containers will be
  173. # auto created & added.
  174. material_container = xml_material_profile(self.getNewId(container_id))
  175. material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"))
  176. containers_to_add.append(material_container)
  177. material_containers.append(material_container)
  178. Job.yieldThread()
  179. Logger.log("d", "Workspace loading is checking instance containers...")
  180. # Get quality_changes and user profiles saved in the workspace
  181. instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)]
  182. user_instance_containers = []
  183. quality_changes_instance_containers = []
  184. for instance_container_file in instance_container_files:
  185. container_id = self._stripFileToId(instance_container_file)
  186. instance_container = InstanceContainer(container_id)
  187. # Deserialize InstanceContainer by converting read data from bytes to string
  188. instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8"))
  189. container_type = instance_container.getMetaDataEntry("type")
  190. Job.yieldThread()
  191. if container_type == "user":
  192. # Check if quality changes already exists.
  193. user_containers = self._container_registry.findInstanceContainers(id=container_id)
  194. if not user_containers:
  195. containers_to_add.append(instance_container)
  196. else:
  197. if self._resolve_strategies["machine"] == "override" or self._resolve_strategies["machine"] is None:
  198. user_containers[0].deserialize(archive.open(instance_container_file).read().decode("utf-8"))
  199. elif self._resolve_strategies["machine"] == "new":
  200. # The machine is going to get a spiffy new name, so ensure that the id's of user settings match.
  201. extruder_id = instance_container.getMetaDataEntry("extruder", None)
  202. if extruder_id:
  203. new_id = self.getNewId(extruder_id) + "_current_settings"
  204. instance_container._id = new_id
  205. instance_container.setName(new_id)
  206. instance_container.setMetaDataEntry("extruder", self.getNewId(extruder_id))
  207. containers_to_add.append(instance_container)
  208. machine_id = instance_container.getMetaDataEntry("machine", None)
  209. if machine_id:
  210. new_id = self.getNewId(machine_id) + "_current_settings"
  211. instance_container._id = new_id
  212. instance_container.setName(new_id)
  213. instance_container.setMetaDataEntry("machine", self.getNewId(machine_id))
  214. containers_to_add.append(instance_container)
  215. user_instance_containers.append(instance_container)
  216. elif container_type == "quality_changes":
  217. # Check if quality changes already exists.
  218. quality_changes = self._container_registry.findInstanceContainers(id = container_id)
  219. if not quality_changes:
  220. containers_to_add.append(instance_container)
  221. else:
  222. if self._resolve_strategies["quality_changes"] == "override":
  223. quality_changes[0].deserialize(archive.open(instance_container_file).read().decode("utf-8"))
  224. elif self._resolve_strategies["quality_changes"] is None:
  225. # The ID already exists, but nothing in the values changed, so do nothing.
  226. pass
  227. quality_changes_instance_containers.append(instance_container)
  228. else:
  229. continue
  230. # Add all the containers right before we try to add / serialize the stack
  231. for container in containers_to_add:
  232. self._container_registry.addContainer(container)
  233. # Get the stack(s) saved in the workspace.
  234. Logger.log("d", "Workspace loading is checking stacks containers...")
  235. container_stack_files = [name for name in cura_file_names if name.endswith(self._container_stack_suffix)]
  236. global_stack = None
  237. extruder_stacks = []
  238. container_stacks_added = []
  239. try:
  240. for container_stack_file in container_stack_files:
  241. container_id = self._stripFileToId(container_stack_file)
  242. # Check if a stack by this ID already exists;
  243. container_stacks = self._container_registry.findContainerStacks(id=container_id)
  244. if container_stacks:
  245. stack = container_stacks[0]
  246. if self._resolve_strategies["machine"] == "override":
  247. container_stacks[0].deserialize(archive.open(container_stack_file).read().decode("utf-8"))
  248. elif self._resolve_strategies["machine"] == "new":
  249. new_id = self.getNewId(container_id)
  250. stack = ContainerStack(new_id)
  251. stack.deserialize(archive.open(container_stack_file).read().decode("utf-8"))
  252. # Ensure a unique ID and name
  253. stack._id = new_id
  254. # Extruder stacks are "bound" to a machine. If we add the machine as a new one, the id of the
  255. # bound machine also needs to change.
  256. if stack.getMetaDataEntry("machine", None):
  257. stack.setMetaDataEntry("machine", self.getNewId(stack.getMetaDataEntry("machine")))
  258. if stack.getMetaDataEntry("type") != "extruder_train":
  259. # Only machines need a new name, stacks may be non-unique
  260. stack.setName(self._container_registry.uniqueName(stack.getName()))
  261. container_stacks_added.append(stack)
  262. self._container_registry.addContainer(stack)
  263. else:
  264. Logger.log("w", "Resolve strategy of %s for machine is not supported", self._resolve_strategies["machine"])
  265. else:
  266. stack = ContainerStack(container_id)
  267. # Deserialize stack by converting read data from bytes to string
  268. stack.deserialize(archive.open(container_stack_file).read().decode("utf-8"))
  269. container_stacks_added.append(stack)
  270. self._container_registry.addContainer(stack)
  271. if stack.getMetaDataEntry("type") == "extruder_train":
  272. extruder_stacks.append(stack)
  273. else:
  274. global_stack = stack
  275. Job.yieldThread()
  276. except:
  277. Logger.log("W", "We failed to serialize the stack. Trying to clean up.")
  278. # Something went really wrong. Try to remove any data that we added.
  279. for container in containers_to_add:
  280. self._container_registry.getInstance().removeContainer(container.getId())
  281. for container in container_stacks_added:
  282. self._container_registry.getInstance().removeContainer(container.getId())
  283. return None
  284. if self._resolve_strategies["machine"] == "new":
  285. # A new machine was made, but it was serialized with the wrong user container. Fix that now.
  286. for container in user_instance_containers:
  287. extruder_id = container.getMetaDataEntry("extruder", None)
  288. if extruder_id:
  289. for extruder in extruder_stacks:
  290. if extruder.getId() == extruder_id:
  291. extruder.replaceContainer(0, container)
  292. continue
  293. machine_id = container.getMetaDataEntry("machine", None)
  294. if machine_id:
  295. if global_stack.getId() == machine_id:
  296. global_stack.replaceContainer(0, container)
  297. continue
  298. if self._resolve_strategies["quality_changes"] == "new":
  299. # Quality changes needs to get a new ID, added to registry and to the right stacks
  300. for container in quality_changes_instance_containers:
  301. old_id = container.getId()
  302. container.setName(self._container_registry.uniqueName(container.getName()))
  303. # We're not really supposed to change the ID in normal cases, but this is an exception.
  304. container._id = self.getNewId(container.getId())
  305. # The container was not added yet, as it didn't have an unique ID. It does now, so add it.
  306. self._container_registry.addContainer(container)
  307. # Replace the quality changes container
  308. old_container = global_stack.findContainer({"type": "quality_changes"})
  309. if old_container.getId() == old_id:
  310. quality_changes_index = global_stack.getContainerIndex(old_container)
  311. global_stack.replaceContainer(quality_changes_index, container)
  312. continue
  313. for stack in extruder_stacks:
  314. old_container = stack.findContainer({"type": "quality_changes"})
  315. if old_container.getId() == old_id:
  316. quality_changes_index = stack.getContainerIndex(old_container)
  317. stack.replaceContainer(quality_changes_index, container)
  318. if self._resolve_strategies["material"] == "new":
  319. for material in material_containers:
  320. old_material = global_stack.findContainer({"type": "material"})
  321. if old_material.getId() in self._id_mapping:
  322. material_index = global_stack.getContainerIndex(old_material)
  323. global_stack.replaceContainer(material_index, material)
  324. continue
  325. for stack in extruder_stacks:
  326. old_material = stack.findContainer({"type": "material"})
  327. if old_material.getId() in self._id_mapping:
  328. material_index = stack.getContainerIndex(old_material)
  329. stack.replaceContainer(material_index, material)
  330. continue
  331. for stack in extruder_stacks:
  332. ExtruderManager.getInstance().registerExtruder(stack, global_stack.getId())
  333. else:
  334. # Machine has no extruders, but it needs to be registered with the extruder manager.
  335. ExtruderManager.getInstance().registerExtruder(None, global_stack.getId())
  336. Logger.log("d", "Workspace loading is notifying rest of the code of changes...")
  337. # Notify everything/one that is to notify about changes.
  338. for container in global_stack.getContainers():
  339. global_stack.containersChanged.emit(container)
  340. Job.yieldThread()
  341. for stack in extruder_stacks:
  342. stack.setNextStack(global_stack)
  343. for container in stack.getContainers():
  344. stack.containersChanged.emit(container)
  345. Job.yieldThread()
  346. # Actually change the active machine.
  347. Application.getInstance().setGlobalContainerStack(global_stack)
  348. return nodes
  349. def _stripFileToId(self, file):
  350. return file.replace("Cura/", "").split(".")[0]
  351. def _getXmlProfileClass(self):
  352. return self._container_registry.getContainerForMimeType(MimeTypeDatabase.getMimeType("application/x-ultimaker-material-profile"))
  353. ## Get the list of ID's of all containers in a container stack by partially parsing it's serialized data.
  354. def _getContainerIdListFromSerialized(self, serialized):
  355. parser = configparser.ConfigParser(interpolation=None, empty_lines_in_values=False)
  356. parser.read_string(serialized)
  357. container_string = parser["general"].get("containers", "")
  358. container_list = container_string.split(",")
  359. return [container_id for container_id in container_list if container_id != ""]