Skip to content

Commit 7723aff

Browse files
committed
fix: reuse wizard candidate color slot
1 parent 6131619 commit 7723aff

2 files changed

Lines changed: 106 additions & 0 deletions

File tree

phy/cluster/_selection.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,33 @@ def set_similarity_selection(self, similar_ids, presentation_order=None):
298298
)
299299
return self._apply(after)
300300

301+
def navigate_similarity_selection(self, similar_ids, presentation_order=None):
302+
"""Replace the Normal-mode wizard candidate while reusing its color slot."""
303+
self._require_normal_mode()
304+
current = self._state
305+
similar_ids = _as_unique_ids(similar_ids)
306+
if len(similar_ids) > 1:
307+
raise ValueError('Similarity navigation selects at most one candidate.')
308+
effective_ids = _ordered_union(current.cluster_ids, similar_ids)
309+
if presentation_order is None:
310+
presentation_order = _ordered_union(
311+
tuple(
312+
cluster_id
313+
for cluster_id in current.presentation_order
314+
if cluster_id in effective_ids
315+
),
316+
effective_ids,
317+
)
318+
color_order = self._navigation_color_order(similar_ids)
319+
after = CurationSelectionState(
320+
cluster_ids=current.cluster_ids,
321+
similar_ids=similar_ids,
322+
reference_id=current.reference_id,
323+
presentation_order=presentation_order,
324+
color_order=color_order,
325+
)
326+
return self._apply(after)
327+
301328
def set_presentation_order(self, presentation_order):
302329
"""Set scientific-view order without changing roles or color slots."""
303330
current = self._state
@@ -440,6 +467,41 @@ def _next_color_order(self, reference_id, presentation_order):
440467
return tuple(presentation_order)
441468
return _ordered_union(current.color_order, presentation_order)
442469

470+
def _navigation_color_order(self, similar_ids):
471+
"""Give a replacement wizard candidate the outgoing candidate's slot."""
472+
current = self._state
473+
if not similar_ids:
474+
return current.color_order
475+
candidate = similar_ids[0]
476+
color_order = list(current.color_order)
477+
outgoing_slots = [
478+
color_order.index(cluster_id)
479+
for cluster_id in current.similar_ids
480+
if cluster_id in color_order
481+
]
482+
if outgoing_slots:
483+
target = min(outgoing_slots)
484+
else:
485+
primary_ids = set(current.cluster_ids)
486+
target = next(
487+
(
488+
index
489+
for index, cluster_id in enumerate(color_order)
490+
if cluster_id not in primary_ids
491+
),
492+
len(color_order),
493+
)
494+
if candidate in color_order:
495+
source = color_order.index(candidate)
496+
color_order[target], color_order[source] = color_order[source], color_order[target]
497+
elif target < len(color_order):
498+
displaced = color_order[target]
499+
color_order[target] = candidate
500+
color_order.append(displaced)
501+
else:
502+
color_order.append(candidate)
503+
return tuple(color_order)
504+
443505
def _apply(self, after):
444506
before = self._state
445507
self._state = after

phy/cluster/tests/test_selection.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,50 @@ def test_similarity_deselection_and_reselection_preserve_color_slots():
116116
assert not change.colors_changed
117117

118118

119+
def test_similarity_navigation_reuses_outgoing_or_inactive_color_slot():
120+
controller = CurationSelectionController(
121+
CurationSelectionState(
122+
cluster_ids=(1,),
123+
similar_ids=(2,),
124+
reference_id=1,
125+
color_order=(1, 2, 3),
126+
)
127+
)
128+
129+
change = controller.navigate_similarity_selection((3,))
130+
assert change.after.similar_ids == (3,)
131+
assert change.after.presentation_order == (1, 3)
132+
assert change.after.color_order == (1, 3, 2)
133+
assert change.colors_changed
134+
135+
change = controller.navigate_similarity_selection((2,))
136+
assert change.after.color_order == (1, 2, 3)
137+
138+
controller.clear_similarity_selection()
139+
change = controller.navigate_similarity_selection((4,))
140+
assert change.after.color_order == (1, 4, 3, 2)
141+
142+
143+
def test_similarity_navigation_preserves_primary_colors_and_is_normal_only():
144+
controller = CurationSelectionController(
145+
CurationSelectionState(
146+
cluster_ids=(1, 4),
147+
similar_ids=(2,),
148+
reference_id=1,
149+
color_order=(1, 4, 2, 3),
150+
)
151+
)
152+
153+
change = controller.navigate_similarity_selection((3,))
154+
assert change.after.color_order == (1, 4, 3, 2)
155+
with raises(ValueError, match='at most one'):
156+
controller.navigate_similarity_selection((2, 3))
157+
158+
controller.enter_merge_mode()
159+
with raises(RuntimeError, match='unavailable'):
160+
controller.navigate_similarity_selection((5,))
161+
162+
119163
def test_set_normal_selection_replaces_all_roles_atomically():
120164
controller = CurationSelectionController()
121165

0 commit comments

Comments
 (0)