From 506d731c5ea6a54efa8c8223f8a787ed59385eca Mon Sep 17 00:00:00 2001 From: Fernando Pietchaki <6844712+pietchaki@users.noreply.github.com> Date: Tue, 29 Aug 2023 13:37:09 -0300 Subject: [PATCH] Refactor object reload to handle 3mf files Refactor object reload to better handling of 3mf files Make a single file read for all objects from that file, and then replace the objects in scene to the new ones, matching by name. This Require Update to the CuraApplication that currently rename all objects with the file name. 3mf Files can have multiple objects in them. When that is the case, and the file is updated, all objects from that file where being replaced by the first object in the file. --- UM/Scene/Scene.py | 70 +++++++++++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/UM/Scene/Scene.py b/UM/Scene/Scene.py index d3f46e6404..ca68ab1442 100644 --- a/UM/Scene/Scene.py +++ b/UM/Scene/Scene.py @@ -188,6 +188,7 @@ def _onFileChanged(self, file_path: str) -> None: if modified_nodes: # Hide the message if it was already visible + # Todo: keep one message for each modified file, when multiple had been updated at same time if self._reload_message is not None: self._reload_message.hide() @@ -197,11 +198,11 @@ def _onFileChanged(self, file_path: str) -> None: self._reload_message.addAction("reload", i18n_catalog.i18nc("@action:button", "Reload"), icon = "", description = i18n_catalog.i18nc("@action:description", "This will trigger the modified files to reload from disk.")) - self._reload_callback = functools.partial(self._reloadNodes, modified_nodes) + self._reload_callback = functools.partial(self._reloadNodes, modified_nodes, file_path) self._reload_message.actionTriggered.connect(self._reload_callback) self._reload_message.show() - def _reloadNodes(self, nodes: List["SceneNode"], message: str, action: str) -> None: + def _reloadNodes(self, nodes: List["SceneNode"], file_path: str, message: str, action: str) -> None: """Reloads a list of nodes after the user pressed the "Reload" button. :param nodes: The list of nodes that needs to be reloaded. @@ -213,31 +214,54 @@ def _reloadNodes(self, nodes: List["SceneNode"], message: str, action: str) -> N return if self._reload_message is not None: self._reload_message.hide() - for node in nodes: - meshdata = node.getMeshData() - if meshdata: - filename = meshdata.getFileName() - if not filename or not os.path.isfile(filename): # File doesn't exist any more. - continue - job = ReadMeshJob(filename) - reload_finished_callback = functools.partial(self._reloadJobFinished, node) - - # Store it so it won't get garbage collected. This is a memory leak, but just one partial per reload so - # it's not much. - self._callbacks.add(reload_finished_callback) - - job.finished.connect(reload_finished_callback) - job.start() - - def _reloadJobFinished(self, replaced_node: SceneNode, job: ReadMeshJob) -> None: + + if not file_path or not os.path.isfile(file_path): # File doesn't exist anymore. + return + + job = ReadMeshJob(file_path) + reload_finished_callback = functools.partial(self._reloadJobFinished, nodes) + + # Store it so it won't get garbage collected. This is a memory leak, but just one partial per reload so + # it's not much. + self._callbacks.add(reload_finished_callback) + + job.finished.connect(reload_finished_callback) + job.start() + + def _reloadJobFinished(self, replaced_nodes: [SceneNode], job: ReadMeshJob) -> None: """Triggered when reloading has finished. - This then puts the resulting mesh data in the node. + This then puts the resulting mesh data in the nodes. + Objects in the scene that are not in the reloaded file will be kept. (same as in the ReloadAll action) """ + renamed_nodes = {} # type: Dict[str, int] for node in job.getResult(): mesh_data = node.getMeshData() - if mesh_data: - replaced_node.setMeshData(mesh_data) - else: + if not mesh_data: Logger.log("w", "Could not find a mesh in reloaded node.") + continue + + # Solves issues with object naming + node_name = node.getName() + if not node_name: + node_name = os.path.basename(mesh_data.getFileName()) + if node_name in renamed_nodes: # objects may get renamed by Cura.UI.ObjectsModel._renameNodes() when loaded + renamed_nodes[node_name] += 1 + node_name = "{0}({1})".format(node.getName(), renamed_nodes[node.getName()]) + else: + renamed_nodes[node.getName()] = 0 + + # Find the matching scene node to replace + scene_node = None + for replaced_node in replaced_nodes: + if replaced_node.getName() == node_name: + scene_node = replaced_node + break + + if scene_node: + scene_node.setMeshData(mesh_data) + else: + # Current node is a new one in the file, or it's name has changed + # TODO: Load this mesh into the scene. Also alter the "ReloadAll" action in CuraApplication. + Logger.log("w", "Could not find matching node for object '{0}' in the scene.", node_name)