1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-04-12 19:50:54 +02:00

Compare commits

...

3 Commits

Author SHA1 Message Date
semantic-release
9f91eb2e08 2.42.0
Automatically generated by python-semantic-release
2025-10-21 13:17:23 +00:00
1e19092319 feat(positioner_box_2d): added properties to enable/disable vertical and horizontal controls 2025-10-21 15:16:24 +02:00
96664c3923 feat(image_roi): enhance get_coordinates to include rectangle center and dimensions 2025-10-21 15:16:01 +02:00
7 changed files with 172 additions and 10 deletions

View File

@@ -1,6 +1,17 @@
# CHANGELOG
## v2.42.0 (2025-10-21)
### Features
- **image_roi**: Enhance get_coordinates to include rectangle center and dimensions
([`96664c3`](https://github.com/bec-project/bec_widgets/commit/96664c3923737df0b09aa8f35df388f9fd630b55))
- **positioner_box_2d**: Added properties to enable/disable vertical and horizontal controls
([`1e19092`](https://github.com/bec-project/bec_widgets/commit/1e190923196f8b28c92dfdd83b9ce90873dd792d))
## v2.41.1 (2025-10-15)
### Bug Fixes

View File

@@ -3534,6 +3534,34 @@ class PositionerBox2D(RPCBase):
Take a screenshot of the dock area and save it to a file.
"""
@property
@rpc_call
def enable_controls_hor(self) -> "bool":
"""
Persisted switch for horizontal control buttons (tweak/step).
"""
@enable_controls_hor.setter
@rpc_call
def enable_controls_hor(self) -> "bool":
"""
Persisted switch for horizontal control buttons (tweak/step).
"""
@property
@rpc_call
def enable_controls_ver(self) -> "bool":
"""
Persisted switch for vertical control buttons (tweak/step).
"""
@enable_controls_ver.setter
@rpc_call
def enable_controls_ver(self) -> "bool":
"""
Persisted switch for vertical control buttons (tweak/step).
"""
class PositionerControlLine(RPCBase):
"""A widget that controls a single device."""
@@ -3653,8 +3681,8 @@ class RectangularROI(RPCBase):
@rpc_call
def get_coordinates(self, typed: "bool | None" = None) -> "dict | tuple":
"""
Returns the coordinates of a rectangle's corners. Supports returning them
as either a dictionary with descriptive keys or a tuple of coordinates.
Returns the coordinates of a rectangle's corners, rectangle center and dimensions.
Supports returning them as either a dictionary with descriptive keys or a tuple of coordinates.
Args:
typed (bool | None): If True, returns coordinates as a dictionary with
@@ -3662,7 +3690,7 @@ class RectangularROI(RPCBase):
the value of `self.description`.
Returns:
dict | tuple: The rectangle's corner coordinates, where the format
dict | tuple: The rectangle's corner coordinates, rectangle center and dimensions, where the format
depends on the `typed` parameter.
"""

View File

@@ -34,7 +34,15 @@ class PositionerBox2D(PositionerBoxBase):
PLUGIN = True
RPC = True
USER_ACCESS = ["set_positioner_hor", "set_positioner_ver", "screenshot"]
USER_ACCESS = [
"set_positioner_hor",
"set_positioner_ver",
"screenshot",
"enable_controls_hor",
"enable_controls_hor.setter",
"enable_controls_ver",
"enable_controls_ver.setter",
]
device_changed_hor = Signal(str, str)
device_changed_ver = Signal(str, str)
@@ -63,6 +71,8 @@ class PositionerBox2D(PositionerBoxBase):
self._limits_hor = None
self._limits_ver = None
self._dialog = None
self._enable_controls_hor = True
self._enable_controls_ver = True
if self.current_path == "":
self.current_path = os.path.dirname(__file__)
self.init_ui()
@@ -281,6 +291,7 @@ class PositionerBox2D(PositionerBoxBase):
self.on_device_readback_hor,
self._device_ui_components_hv("horizontal"),
)
self._apply_controls_enabled("horizontal")
@SafeSlot(str, str)
def on_device_change_ver(self, old_device: str, new_device: str):
@@ -300,6 +311,7 @@ class PositionerBox2D(PositionerBoxBase):
self.on_device_readback_ver,
self._device_ui_components_hv("vertical"),
)
self._apply_controls_enabled("vertical")
def _device_ui_components_hv(self, device: DeviceId) -> DeviceUpdateUIComponents:
if device == "horizontal":
@@ -337,6 +349,25 @@ class PositionerBox2D(PositionerBoxBase):
if device == self.device_ver:
return self._device_ui_components_hv("vertical")
def _apply_controls_enabled(self, axis: DeviceId):
state = self._enable_controls_hor if axis == "horizontal" else self._enable_controls_ver
if axis == "horizontal":
widgets = [
self.ui.tweak_increase_hor,
self.ui.tweak_decrease_hor,
self.ui.step_increase_hor,
self.ui.step_decrease_hor,
]
else:
widgets = [
self.ui.tweak_increase_ver,
self.ui.tweak_decrease_ver,
self.ui.step_increase_ver,
self.ui.step_decrease_ver,
]
for w in widgets:
w.setEnabled(state)
@SafeSlot(dict, dict)
def on_device_readback_hor(self, msg_content: dict, metadata: dict):
"""Callback for device readback.
@@ -417,6 +448,26 @@ class PositionerBox2D(PositionerBoxBase):
"""Step size for tweak"""
self.ui.step_size_ver.setValue(val)
@SafeProperty(bool)
def enable_controls_hor(self) -> bool:
"""Persisted switch for horizontal control buttons (tweak/step)."""
return self._enable_controls_hor
@enable_controls_hor.setter
def enable_controls_hor(self, value: bool):
self._enable_controls_hor = value
self._apply_controls_enabled("horizontal")
@SafeProperty(bool)
def enable_controls_ver(self) -> bool:
"""Persisted switch for vertical control buttons (tweak/step)."""
return self._enable_controls_ver
@enable_controls_ver.setter
def enable_controls_ver(self, value: bool):
self._enable_controls_ver = value
self._apply_controls_enabled("vertical")
@SafeSlot()
def on_tweak_inc_hor(self):
"""Tweak device a up"""

View File

@@ -558,8 +558,8 @@ class RectangularROI(BaseROI, pg.RectROI):
def get_coordinates(self, typed: bool | None = None) -> dict | tuple:
"""
Returns the coordinates of a rectangle's corners. Supports returning them
as either a dictionary with descriptive keys or a tuple of coordinates.
Returns the coordinates of a rectangle's corners, rectangle center and dimensions.
Supports returning them as either a dictionary with descriptive keys or a tuple of coordinates.
Args:
typed (bool | None): If True, returns coordinates as a dictionary with
@@ -567,13 +567,17 @@ class RectangularROI(BaseROI, pg.RectROI):
the value of `self.description`.
Returns:
dict | tuple: The rectangle's corner coordinates, where the format
dict | tuple: The rectangle's corner coordinates, rectangle center and dimensions, where the format
depends on the `typed` parameter.
"""
if typed is None:
typed = self.description
x_left, y_bottom, x_right, y_top = self._normalized_edges()
width = x_right - x_left
height = y_top - y_bottom
cx = x_left + width / 2
cy = y_bottom + height / 2
if typed:
return {
@@ -581,8 +585,19 @@ class RectangularROI(BaseROI, pg.RectROI):
"bottom_right": (x_right, y_bottom),
"top_left": (x_left, y_top),
"top_right": (x_right, y_top),
"center_x": cx,
"center_y": cy,
"width": width,
"height": height,
}
return (x_left, y_bottom), (x_right, y_bottom), (x_left, y_top), (x_right, y_top)
return (
(x_left, y_bottom),
(x_right, y_bottom),
(x_left, y_top),
(x_right, y_top),
(cx, cy),
(width, height),
)
def _lookup_scene_image(self):
"""

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "bec_widgets"
version = "2.41.1"
version = "2.42.0"
description = "BEC Widgets"
requires-python = ">=3.11"
classifiers = [

View File

@@ -81,7 +81,7 @@ def test_data_extraction_matches_coordinates(bec_image_widget_with_roi):
# For rectangular ROI: pixel bounding box equals coordinate bbox
if isinstance(roi, RectangularROI):
(x0, y0), (_, _), (_, _), (x1, y1) = roi.get_coordinates(typed=False)
(x0, y0), (_, _), (_, _), (x1, y1), *_ = roi.get_coordinates(typed=False)
# ensure ints inside image shape
x0, y0, x1, y1 = map(int, (x0, y0, x1, y1))
expected = widget.main_image.image[y0:y1, x0:x1]

View File

@@ -80,3 +80,60 @@ def test_positioner_box_setpoint_changes(positioner_box_2d: PositionerBox2D):
positioner_box_2d.ui.setpoint_ver.setText("100")
positioner_box_2d.on_setpoint_change_ver()
mock_move.assert_called_once_with(100, relative=False)
def _hor_buttons(widget: PositionerBox2D):
return [
widget.ui.tweak_increase_hor,
widget.ui.tweak_decrease_hor,
widget.ui.step_increase_hor,
widget.ui.step_decrease_hor,
]
def _ver_buttons(widget: PositionerBox2D):
return [
widget.ui.tweak_increase_ver,
widget.ui.tweak_decrease_ver,
widget.ui.step_increase_ver,
widget.ui.step_decrease_ver,
]
def test_controls_default_enabled(positioner_box_2d: PositionerBox2D):
"""By default both axes controls are enabled and UI reflects it."""
assert positioner_box_2d.enable_controls_hor is True
assert positioner_box_2d.enable_controls_ver is True
assert all(w.isEnabled() for w in _hor_buttons(positioner_box_2d))
assert all(w.isEnabled() for w in _ver_buttons(positioner_box_2d))
def test_disable_enable_controls_and_persist_across_device_change(
positioner_box_2d: PositionerBox2D, qtbot
):
"""Disabling an axis should disable its buttons and remain disabled after device (re)binding."""
# Disable horizontal and verify UI
positioner_box_2d.enable_controls_hor = False
assert positioner_box_2d.enable_controls_hor is False
assert all(not w.isEnabled() for w in _hor_buttons(positioner_box_2d))
# Simulate a horizontal device change; state must persist after queued re-apply
positioner_box_2d.on_device_change_hor("samx", "samx")
qtbot.waitUntil(lambda: all(not w.isEnabled() for w in _hor_buttons(positioner_box_2d)))
# Re-enable and verify UI
positioner_box_2d.enable_controls_hor = True
qtbot.waitUntil(lambda: all(w.isEnabled() for w in _hor_buttons(positioner_box_2d)))
# Disable vertical and verify UI
positioner_box_2d.enable_controls_ver = False
assert positioner_box_2d.enable_controls_ver is False
assert all(not w.isEnabled() for w in _ver_buttons(positioner_box_2d))
# Simulate a vertical device change; state must persist after queued re-apply
positioner_box_2d.on_device_change_ver("samy", "samy")
qtbot.waitUntil(lambda: all(not w.isEnabled() for w in _ver_buttons(positioner_box_2d)))
# Re-enable and verify UI
positioner_box_2d.enable_controls_ver = True
qtbot.waitUntil(lambda: all(w.isEnabled() for w in _ver_buttons(positioner_box_2d)))