From 5a776dbd802d9a50edb460e3d3e5f9868a01286e Mon Sep 17 00:00:00 2001 From: uermel Date: Fri, 9 Oct 2026 11:56:31 -0700 Subject: [PATCH] fix: duplicate filament sets, meshes and segmentations The duplicate button did nothing for filaments (not connected), meshes and segmentations (empty handlers). All three now copy the entry to the chosen user / session with copick's copy operation (segmentations keep their type and voxel size), after storing pending edits, and show the copy; a target that already exists is reported. Co-Authored-By: Claude Opus 5.5 --- src/tool.py | 94 ++++++++++++++++++++++++++++++++++--------- src/ui/main_widget.py | 7 ++++ 2 files changed, 81 insertions(+), 20 deletions(-) diff --git a/src/tool.py b/src/tool.py index 4308119..a27398c 100644 --- a/src/tool.py +++ b/src/tool.py @@ -28,7 +28,7 @@ from copick.models import CopickMesh, CopickPicks, CopickSegmentation from copick.util.uri import serialize_copick_uri from copick_shared_ui.core.thumbnail_cache import set_global_cache_config, set_global_cache_image_interface -from copick_shared_ui.core.types import segmentation_type_flags, segmentation_type_of +from copick_shared_ui.core.types import copick_object_type, segmentation_type_flags, segmentation_type_of from copick_shared_ui.util.instances import instance_colors # Qt @@ -1203,17 +1203,76 @@ def new_particles(self, object_name: str, user_id: str, session_id: str): # Set mouse mode to "mark plane" (pick on plane) run(self.session, "ui mousemode right 'mark plane'", log=False) - def duplicate_mesh(self, index: QModelIndex, user_id: str = "", session_id: str = ""): - """Placeholder for mesh duplication""" - # Get entity from unified table model - model = index.model() - entity = model.get_entity(index) + def _duplicate_entity(self, index: QModelIndex, user_id: str, session_id: str, object_type: str) -> Any: + """Copy the filament set, mesh or segmentation at ``index`` to a new user / session. - if not isinstance(entity, CopickMesh): - return + Empty user_id/session_id fall back to the root user and "-copy-1". Returns the copy, or None (with a + warning in the log) if the target already exists or the copy failed. + """ + if not index.isValid(): + return None + entity = index.model().get_entity(index) + if entity is None or copick_object_type(entity) != object_type: + return None - # TODO: Implement mesh duplication logic - pass + self.store() # write pending edits first, so the copy includes them + + if not user_id: + user_id = self.root.user_id if self.root.user_id is not None else "ArtiaX" + if not session_id: + session_id = f"{entity.session_id}-copy-1" + + from copick.ops.manage import copy_copick_objects + + source = serialize_copick_uri(entity) + name, rest = source.split(":", 1) + suffix = rest[rest.index("@") :] if "@" in rest else "" # voxel size and segmentation type + target = f"{name}:{user_id}/{session_id}{suffix}" + kind = {"mesh": "mesh", "segmentation": "segmentation", "filaments": "filament set"}[object_type] + try: + result = copy_copick_objects(self.root, object_type, source, target, run_name=entity.run.name) + except ValueError as e: + result = {"copied": 0, "errors": [str(e)]} + if not result["copied"]: + reason = "; ".join(result["errors"]) or "nothing was copied" + self.session.logger.warning( + f"Cannot duplicate the {kind} {source} as {target} ({reason}). Choose a different session ID.", + ) + return None + + run_ = entity.run + if object_type == "filaments": + copies = run_.get_filaments(object_name=name, user_id=user_id, session_id=session_id) + elif object_type == "mesh": + copies = run_.get_meshes(object_name=name, user_id=user_id, session_id=session_id) + else: + copies = [ + s + for s in run_.get_segmentations( + name=name, + user_id=user_id, + session_id=session_id, + is_instance=None, + is_panoptic=None, + ) + if s.voxel_size == entity.voxel_size and segmentation_type_of(s) == segmentation_type_of(entity) + ] + self.session.logger.info(f"copick: duplicated the {kind} {source} as {target}") + return copies[0] if copies else None + + def duplicate_filaments(self, index: QModelIndex, user_id: str = "", session_id: str = ""): + """Duplicate a filament set to a new user / session, and show the copy.""" + copy = self._duplicate_entity(index, user_id, session_id, "filaments") + self._mw.update_filaments_table() + if copy is not None: + self._show_filaments_entity(copy) + + def duplicate_mesh(self, index: QModelIndex, user_id: str = "", session_id: str = ""): + """Duplicate a mesh to a new user / session, and show the copy.""" + copy = self._duplicate_entity(index, user_id, session_id, "mesh") + self._mw.update_meshes_table() + if copy is not None: + self._show_mesh_entity(copy) def new_mesh(self, object_name: str, user_id: str, session_id: str): """Placeholder for new mesh creation""" @@ -1221,16 +1280,11 @@ def new_mesh(self, object_name: str, user_id: str, session_id: str): pass def duplicate_segmentation(self, index: QModelIndex, user_id: str = "", session_id: str = ""): - """Placeholder for segmentation duplication""" - # Get entity from unified table model - model = index.model() - entity = model.get_entity(index) - - if not isinstance(entity, CopickSegmentation): - return - - # TODO: Implement segmentation duplication logic - pass + """Duplicate a segmentation (keeping its type and voxel size) to a new user / session, and show the copy.""" + copy = self._duplicate_entity(index, user_id, session_id, "segmentation") + self._mw.update_segmentations_table() + if copy is not None: + self._show_segmentation_entity(copy) def new_segmentation(self, object_name: str, user_id: str, session_id: str): """Start a new (empty) instance segmentation of ``object_name`` at the active tomogram's voxel spacing and diff --git a/src/ui/main_widget.py b/src/ui/main_widget.py index 900ca15..8f43de5 100644 --- a/src/ui/main_widget.py +++ b/src/ui/main_widget.py @@ -568,6 +568,7 @@ def _connect(self): self._filaments_table.get_table_view().clicked.connect( lambda i: self._copick.activate_filaments(self._map_index(self._filaments_table, i)), ) + self._filaments_table.duplicateClicked.connect(self._copick.duplicate_filaments) self._filaments_table.newClicked.connect(self._on_new_filaments) self._filaments_table.deleteClicked.connect(self._copick.delete_filaments) @@ -629,6 +630,12 @@ def update_filaments_table(self): if not isinstance(key, type(ctl.entries[key][0])) and ctl.is_shown(key): self._filaments_table.set_entity_active(key, True) + def update_meshes_table(self): + self._meshes_table.update() + for mesh, surf in list(self._copick.mesh_map.items()): + if getattr(surf, "display", False): + self._meshes_table.set_entity_active(mesh, True) + def update_segmentations_table(self): self._segmentations_table.update() for seg in list(self._copick.seg_map) + list(self._copick.segmentations.models):