Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions src/funtracks/user_actions/user_delete_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,17 @@ class UserDeleteNodes(ActionGroup):
nodes: The node ids to delete.
pixels: Optional list of pixel masks for each node, matching the order
of nodes. Defaults to None.
_top_level: If True, add this action to the history and emit the refresh
signal. Set to False when this action is part of a bigger action group,
so that the whole group is undone in one step. Defaults to True.
"""

def __init__(
self,
tracks: Tracks,
nodes: list[int],
pixels: None | list[tuple[np.ndarray, ...]] = None,
_top_level: bool = True,
):
super().__init__(tracks, actions=[])
self.tracks: Tracks # Narrow type from base class
Expand All @@ -42,5 +46,6 @@ def __init__(
)
)

self.tracks.action_history.add_new_action(self)
self.tracks.refresh.emit()
if _top_level:
self.tracks.action_history.add_new_action(self)
self.tracks.refresh.emit()
10 changes: 8 additions & 2 deletions src/funtracks/user_actions/user_update_segmentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def __init__(
updated_pixels: list[tuple[tuple[np.ndarray, ...], int]],
current_track_id: int,
force: bool = False,
_top_level: bool = True,
):
"""Assumes that the pixels have already been updated in the project.segmentation
NOTE: Re discussion with Kasia: we should have a basic action that updates the
Expand All @@ -40,6 +41,9 @@ def __init__(
the currently selected track id in the viewer.
force (bool): Whether to force the operation by removing conflicting edges.
Defaults to False.
_top_level (bool): If True, add this action to the history and emit the
refresh signal. Set to False when this action is part of a bigger action
group, so that the whole group is undone in one step. Defaults to True.
"""
super().__init__(tracks, actions=[])
self.tracks: Tracks # Narrow type from base class
Expand Down Expand Up @@ -118,5 +122,7 @@ def __init__(
self.actions.append(
UpdateNodeSeg(tracks, old_value, mask_pixels, added=False)
)
self.tracks.action_history.add_new_action(self)
self.tracks.refresh.emit(node_to_select)
self.node_to_select = node_to_select
if _top_level:
self.tracks.action_history.add_new_action(self)
self.tracks.refresh.emit(node_to_select)
57 changes: 56 additions & 1 deletion tests/user_actions/test_user_update_segmentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import numpy as np
import pytest

from funtracks.actions import ActionGroup
from funtracks.exceptions import InvalidActionError
from funtracks.user_actions import UserUpdateSegmentation
from funtracks.user_actions import UserDeleteNodes, UserUpdateSegmentation
from funtracks.utils.tracksdata_utils import td_mask_to_pixels

iou_key = "iou"
Expand Down Expand Up @@ -289,3 +290,57 @@ def test_missing_seg(get_tracks):
tracks = get_tracks(ndim=3, with_seg=False, prefill_track_ids=True)
with pytest.raises(ValueError, match="Cannot update non-existing segmentation"):
UserUpdateSegmentation(tracks, 0, [], 1)


@pytest.mark.parametrize("ndim", [3])
def test_not_top_level_actions_group_into_one_undo(get_tracks, ndim):
"""With ``_top_level=False`` the action is applied but not recorded, so a caller can
group several actions into a single, jointly undoable step."""

tracks = get_tracks(ndim=ndim, with_seg=True, prefill_track_ids=True)
node_id = 3
orig_pixels = td_mask_to_pixels(
tracks.get_mask(node_id), tracks.get_time(node_id), ndim=tracks.ndim
)
orig_area = tracks.get_node_attr(node_id, area_key)
n_actions = len(tracks.action_history.undo_stack)

# remove the pixels in two steps, neither of which lands in the history
first = tuple(orig_pixels[d][1:2] for d in range(len(orig_pixels)))
second = tuple(orig_pixels[d][2:] for d in range(len(orig_pixels)))
actions = [
UserUpdateSegmentation(
tracks,
new_value=0,
updated_pixels=[(pixels, node_id)],
current_track_id=1,
_top_level=False,
)
for pixels in (first, second)
]

assert tracks.get_node_attr(node_id, area_key) == orig_area - (
len(first[0]) + len(second[0])
)
assert len(tracks.action_history.undo_stack) == n_actions

# grouped, the two updates are undone together
group = ActionGroup(tracks, actions=actions)
tracks.action_history.add_new_action(group)
assert len(tracks.action_history.undo_stack) == n_actions + 1

tracks.undo()
assert tracks.get_node_attr(node_id, area_key) == orig_area


@pytest.mark.parametrize("ndim", [3])
def test_delete_nodes_not_top_level(get_tracks, ndim):
"""UserDeleteNodes with ``_top_level=False`` deletes without recording history."""

tracks = get_tracks(ndim=ndim, with_seg=True, prefill_track_ids=True)
n_actions = len(tracks.action_history.undo_stack)

UserDeleteNodes(tracks, nodes=[3], _top_level=False)

assert not tracks.graph.has_node(3)
assert len(tracks.action_history.undo_stack) == n_actions
Loading