0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 11:41:49 +02:00

WIP ROI manager for Waveform, however DAP do not update correctly, DO NOT PUT INTO PLOTBASE MR

This commit is contained in:
2025-01-26 17:15:13 +01:00
parent 995960e590
commit ac8a51e821
5 changed files with 185 additions and 12 deletions

View File

@ -20,6 +20,7 @@ from bec_widgets.widgets.plots_next_gen.toolbar_bundles.mouse_interactions impor
MouseInteractionToolbarBundle,
)
from bec_widgets.widgets.plots_next_gen.toolbar_bundles.plot_export import PlotExportBundle
from bec_widgets.widgets.plots_next_gen.toolbar_bundles.roi_bundle import ROIBundle
from bec_widgets.widgets.plots_next_gen.toolbar_bundles.save_state import SaveStateBundle
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
@ -118,18 +119,14 @@ class PlotBase(BECWidget, QWidget):
self.plot_export_bundle = PlotExportBundle("plot_export", target_widget=self)
self.mouse_bundle = MouseInteractionToolbarBundle("mouse_interaction", target_widget=self)
self.state_export_bundle = SaveStateBundle("state_export", target_widget=self)
self.roi_bundle = ROIBundle("roi", target_widget=self)
# Add elements to toolbar
self.toolbar.add_bundle(self.plot_export_bundle, target_widget=self)
self.toolbar.add_bundle(self.state_export_bundle, target_widget=self)
self.toolbar.add_bundle(self.mouse_bundle, target_widget=self)
self.toolbar.add_bundle(self.roi_bundle, target_widget=self)
self.toolbar.add_action("separator_0", SeparatorAction(), target_widget=self)
self.toolbar.add_action(
"crosshair",
MaterialIconAction(icon_name="point_scan", tooltip="Show Crosshair", checkable=True),
target_widget=self,
)
self.toolbar.add_action("separator_1", SeparatorAction(), target_widget=self)
self.toolbar.add_action(
"fps_monitor",
@ -141,7 +138,6 @@ class PlotBase(BECWidget, QWidget):
self.toolbar.widgets["fps_monitor"].action.toggled.connect(
lambda checked: setattr(self, "enable_fps_monitor", checked)
)
self.toolbar.widgets["crosshair"].action.toggled.connect(self.toggle_crosshair)
def add_side_menus(self):
"""Adds multiple menus to the side panel."""

View File

@ -0,0 +1,32 @@
from bec_widgets.qt_utils.toolbar import MaterialIconAction, ToolbarBundle
class ROIBundle(ToolbarBundle):
"""
A bundle of actions that are hooked in this constructor itself,
so that you can immediately connect the signals and toggle states.
This bundle is for a toolbar that controls crosshair and ROI interaction.
"""
def __init__(self, bundle_id="roi", target_widget=None, **kwargs):
super().__init__(bundle_id=bundle_id, actions=[], **kwargs)
self.target_widget = target_widget
# Create each MaterialIconAction with a parent
# so the signals can fire even if the toolbar isn't added yet.
crosshair = MaterialIconAction(
icon_name="point_scan", tooltip="Show Crosshair", checkable=True
)
roi = MaterialIconAction(
icon_name="align_justify_space_between",
tooltip="Add ROI region for DAP",
checkable=True,
)
# Add them to the bundle
self.add_action("crosshair", crosshair)
self.add_action("roi_linear", roi)
# Immediately connect signals
crosshair.action.toggled.connect(self.target_widget.toggle_crosshair)

View File

@ -0,0 +1,83 @@
import pyqtgraph as pg
from qtpy.QtCore import QObject, Signal, Slot
from bec_widgets.utils.linear_region_selector import LinearRegionWrapper
from bec_widgets.utils.colors import get_accent_colors
class WaveformROIManager(QObject):
"""
A reusable helper class that manages a single linear ROI region on a given plot item.
It provides signals to notify about region changes and active state.
"""
roi_changed = Signal(tuple) # Emitted when the ROI (left, right) changes
roi_active = Signal(bool) # Emitted when ROI is enabled or disabled
def __init__(self, plot_item: pg.PlotItem, parent=None):
super().__init__(parent)
self._plot_item = plot_item
self._roi_wrapper: LinearRegionWrapper | None = None
self._roi_region: tuple[float, float] | None = None
self._accent_colors = get_accent_colors()
@property
def roi_region(self) -> tuple[float, float] | None:
return self._roi_region
@roi_region.setter
def roi_region(self, value: tuple[float, float] | None):
self._roi_region = value
if self._roi_wrapper is not None and value is not None:
self._roi_wrapper.linear_region_selector.setRegion(value)
@Slot(bool)
def toggle_roi(self, enabled: bool) -> None:
if enabled:
self._enable_roi()
else:
self._disable_roi()
@Slot(tuple)
def select_roi(self, region: tuple[float, float]):
# If ROI not present, enabling it
if self._roi_wrapper is None:
self.toggle_roi(True)
self.roi_region = region
def _enable_roi(self):
if self._roi_wrapper is not None:
# Already enabled
return
color = self._accent_colors.default
color.setAlpha(int(0.2 * 255))
hover_color = self._accent_colors.default
hover_color.setAlpha(int(0.35 * 255))
self._roi_wrapper = LinearRegionWrapper(
self._plot_item, color=color, hover_color=hover_color, parent=self
)
self._roi_wrapper.add_region_selector()
self._roi_wrapper.region_changed.connect(self._on_region_changed)
# If we already had a region, apply it
if self._roi_region is not None:
self._roi_wrapper.linear_region_selector.setRegion(self._roi_region)
else:
self._roi_region = self._roi_wrapper.linear_region_selector.getRegion()
self.roi_active.emit(True)
def _disable_roi(self):
if self._roi_wrapper is not None:
self._roi_wrapper.region_changed.disconnect(self._on_region_changed)
self._roi_wrapper.cleanup()
self._roi_wrapper.deleteLater()
self._roi_wrapper = None
self._roi_region = None
self.roi_active.emit(False)
@Slot(tuple)
def _on_region_changed(self, region: tuple[float, float]):
self._roi_region = region
self.roi_changed.emit(region)

View File

@ -18,6 +18,7 @@ from bec_widgets.utils.bec_signal_proxy import BECSignalProxy
from bec_widgets.utils.colors import Colors, set_theme
from bec_widgets.widgets.plots_next_gen.plot_base import PlotBase
from bec_widgets.widgets.plots_next_gen.waveform.curve import Curve, CurveConfig, DeviceSignal
from bec_widgets.widgets.plots_next_gen.waveform.utils.roi_manager import WaveformROIManager
logger = bec_logger.logger
@ -56,8 +57,9 @@ class Waveform(PlotBase):
new_scan = Signal()
new_scan_id = Signal(str)
# roi_changed = Signal(tuple)
# roi_active = Signal(bool)
roi_changed = Signal(tuple)
roi_active = Signal(bool)
# request_dap_refresh = Signal() #TODO probably replaced by request_dap_update
def __init__(
self,
@ -93,6 +95,9 @@ class Waveform(PlotBase):
"label_suffix": "",
} # TODO decide which one to use
# Specific GUI elements
self._init_roi_manager()
# Scan status update loop
self.bec_dispatcher.connect_slot(self.on_scan_status, MessageEndpoints.scan_status())
self.bec_dispatcher.connect_slot(self.on_scan_progress, MessageEndpoints.scan_progress())
@ -118,6 +123,53 @@ class Waveform(PlotBase):
# ) # TODO implement
self.scan_history(-1)
################################################################################
# Widget Specific GUI interactions
################################################################################
def _init_roi_manager(self):
"""
Initialize the ROI manager for the Waveform widget.
"""
self._roi_manager = WaveformROIManager(self.plot_item, parent=self)
# Connect manager signals -> forward them via Waveform's own signals
self._roi_manager.roi_changed.connect(self.roi_changed)
self._roi_manager.roi_active.connect(self.roi_active)
# Example: connect ROI changed to re-request DAP
self.roi_changed.connect(self._on_roi_changed_for_dap)
self.toolbar.widgets["roi_linear"].action.toggled.connect(self._roi_manager.toggle_roi)
@property
def roi_region(self) -> tuple[float, float] | None:
"""
Allows external code to get/set the ROI region easily via Waveform.
"""
return self._roi_manager.roi_region
@roi_region.setter
def roi_region(self, value: tuple[float, float] | None):
self._roi_manager.roi_region = value
def select_roi(self, region: tuple[float, float]):
"""
Public method if you want the old `select_roi` style.
"""
self._roi_manager.select_roi(region)
# If you want the old toggle_roi style:
def toggle_roi(self, enabled: bool):
self._roi_manager.toggle_roi(enabled)
def _on_roi_changed_for_dap(self, region: tuple[float, float]):
"""
Whenever the ROI changes, you might want to re-request DAP with the new x_min, x_max.
"""
logger.info(f"ROI region changed to {region}, requesting new DAP fit.")
# Example: you could store these in a local property, or directly call request_dap_update
self.request_dap_update.emit()
################################################################################
# Widget Specific Properties
################################################################################
@ -810,8 +862,6 @@ class Waveform(PlotBase):
# @SafeSlot() #FIXME type error
def request_dap(self):
"""Request new fit for data"""
print("Request DAP") # TODO change to logger
for dap_curve in self._dap_curves:
parent_label = getattr(dap_curve.config, "parent_label", None)
if not parent_label:
@ -825,6 +875,13 @@ class Waveform(PlotBase):
x_data, y_data = parent_curve.get_data()
model_name = dap_curve.config.signal.dap
model = getattr(self.dap, model_name)
try:
x_min, x_max = self.roi_region
except TypeError:
x_min = None
x_max = None
print(f"x_min: {x_min}, x_max: {x_max}")
# TODO implement DAP logic
msg = messages.DAPRequestMessage(
@ -832,7 +889,12 @@ class Waveform(PlotBase):
dap_type="on_demand",
config={
"args": [],
"kwargs": {"data_x": x_data, "data_y": y_data}, # TODO add xmin,xmax as before
"kwargs": {
"data_x": x_data,
"data_y": y_data,
"x_min": x_min,
"x_max": x_max,
}, # TODO add xmin,xmax as before -> so far do not work
"class_args": model._plugin_info["class_args"],
"class_kwargs": model._plugin_info["class_kwargs"],
"curve_label": dap_curve.name(),