feat(crosshair): restore frozen ROI profiles when a pin is re-adopted

This commit is contained in:
2026-07-10 14:48:01 +02:00
committed by Jan Wyzula
parent 41c01e6612
commit 4a6c351be3
3 changed files with 61 additions and 1 deletions
+24 -1
View File
@@ -137,6 +137,9 @@ class Crosshair(QObject):
self.pinned_v_line = None
self.pinned_h_line = None
self.pinned_pos = None
# Last emitted pin payload, kept so the pin can be re-announced to consumers
# after it is re-adopted: ("2d", payload) | ("1d", payload) | None.
self._pinned_emit = None
self._pin_hit_radius_px = 12 # how close (scene px) a click counts as "on the pin"
self.update_markers()
self.check_log()
@@ -648,8 +651,10 @@ class Crosshair(QObject):
self._draw_pin(draw_pt[0], draw_pt[1])
self.pinned_pos = draw_pt
if pinned_2d is not None:
self._pinned_emit = ("2d", pinned_2d)
self.coordinatesPinned2D.emit(pinned_2d)
if pinned_1d is not None:
elif pinned_1d is not None:
self._pinned_emit = ("1d", pinned_1d)
self.coordinatesPinned1D.emit(pinned_1d)
def _draw_pin(self, x: float, y: float):
@@ -715,6 +720,7 @@ class Crosshair(QObject):
self.plot_item.removeItem(self.pinned_label)
self.pinned_label = None
self.pinned_pos = None
self._pinned_emit = None
if had_pin:
self.pinCleared.emit()
@@ -733,6 +739,7 @@ class Crosshair(QObject):
"h_line": self.pinned_h_line,
"label": self.pinned_label,
"pos": self.pinned_pos,
"emit": self._pinned_emit,
}
# Detach the removal callback: this crosshair is about to be deleted and a
# detached pin has no owner to clear it through.
@@ -744,6 +751,7 @@ class Crosshair(QObject):
self.pinned_h_line = None
self.pinned_label = None
self.pinned_pos = None
self._pinned_emit = None
return state
def adopt_pin(self, state):
@@ -759,10 +767,25 @@ class Crosshair(QObject):
self.pinned_h_line = state.get("h_line")
self.pinned_label = state.get("label")
self.pinned_pos = state.get("pos")
self._pinned_emit = state.get("emit")
# Rebind the removal callback to this crosshair (the previous owner is gone).
if self.pinned_point is not None:
self.pinned_point._on_remove = self.clear_pin
def reemit_pin(self):
"""Re-announce the current pin's coordinates to consumers.
Used after :meth:`adopt_pin` so dependent state (e.g. the image ROI panels'
frozen reference profiles) can be rebuilt for a pin that survived a toggle.
"""
if self._pinned_emit is None:
return
kind, payload = self._pinned_emit
if kind == "2d":
self.coordinatesPinned2D.emit(payload)
elif kind == "1d":
self.coordinatesPinned1D.emit(payload)
def _pin_hit(self, scene_pos) -> bool:
"""Return True if a click at ``scene_pos`` (scene pixels) lands on the pinned marker."""
if self.pinned_pos is None:
@@ -658,6 +658,10 @@ class ImageBase(PlotBase):
self.crosshair.coordinatesPinned2D.connect(self.pin_image_slices)
self.crosshair.pinCleared.connect(self.clear_pinned_slices)
self.image_updated.connect(self.update_image_slices)
# If a pin survived a previous toggle, rebuild its frozen profiles now that
# the consumer slots are connected again.
if self.crosshair.pinned_pos is not None:
self.crosshair.reemit_pin()
else:
self.unhook_crosshair()
# Hide the ROI panels (the crosshair is destroyed, so its pin signals
@@ -1095,6 +1095,39 @@ def test_pin_survives_scan_reset(qtbot, mocked_client):
assert bec_image_view.y_roi_pinned is not None
def test_pin_and_profiles_restored_after_roi_toggle(qtbot, mocked_client):
"""Toggling crosshair-ROI off then on restores both the marker and its frozen profiles."""
import numpy as np
bec_image_view = create_widget(qtbot, Image, client=mocked_client)
test_data = np.arange(25).reshape(5, 5)
bec_image_view.on_image_update_2d({"data": test_data}, {})
switch = bec_image_view.toolbar.components.get_action("image_switch_crosshair")
switch.actions["crosshair_roi"].action.trigger()
qtbot.wait(50)
bec_image_view.crosshair.set_pin(2.0, 3.0)
assert bec_image_view.x_roi_pinned is not None
pin_point = bec_image_view.crosshair.pinned_point
# Toggle the ROI crosshair off: marker persists, frozen curves are cleared.
switch.actions["crosshair_roi"].action.trigger()
qtbot.wait(20)
assert bec_image_view.crosshair is None
assert bec_image_view._detached_pin is not None
assert bec_image_view.x_roi_pinned is None
# Toggle back on: marker is re-adopted AND the frozen profiles are rebuilt.
switch.actions["crosshair_roi"].action.trigger()
qtbot.wait(20)
assert bec_image_view.crosshair is not None
assert bec_image_view.crosshair.pinned_point is pin_point
assert bec_image_view.x_roi_pinned is not None
assert bec_image_view.y_roi_pinned is not None
_, x_pinned = bec_image_view.x_roi_pinned.getData()
np.testing.assert_array_equal(x_pinned, test_data[:, 3])
def test_detached_pin_can_be_removed_with_right_click(qtbot, mocked_client, monkeypatch):
"""A pin left on the plot after disabling crosshair remains removable."""
from qtpy.QtWidgets import QMenu