Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: stage explorer part 1 #407

Open
wants to merge 210 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
210 commits
Select commit Hold shift + click to select a range
0d34b2a
refactor: subclass explorer form mda widget
fdrgsp Dec 8, 2022
acec80a
feat: add docstring
fdrgsp Dec 8, 2022
5a162aa
fix: update _update_total_time
fdrgsp Dec 8, 2022
88f5a59
refactor: remove print
fdrgsp Dec 8, 2022
b64e552
refactor: remove unused
fdrgsp Dec 8, 2022
733cb6d
feat: add coll
fdrgsp Dec 8, 2022
9159bba
Merge remote-tracking branch 'upstream/main' into subclass_explorer_f…
fdrgsp Dec 11, 2022
da3132a
fix: update explorer gui + fixes
fdrgsp Dec 12, 2022
e2c7b06
fix: update gui
fdrgsp Dec 12, 2022
983c238
refactor: add comment
fdrgsp Dec 12, 2022
2fa3b3f
Merge remote-tracking branch 'upstream/main' into main
fdrgsp Dec 13, 2022
f6e852f
fix: update explorer
fdrgsp Dec 14, 2022
8b08566
Merge branch 'pymmcore-plus:main' into main
fdrgsp Dec 30, 2022
7d47422
Merge remote-tracking branch 'upstream/main' into main
fdrgsp Jan 3, 2023
f12b2ed
Merge branch 'main' of https://github.com/fdrgsp/pymmcore-widgets int…
fdrgsp Jan 3, 2023
af6ac3a
Merge remote-tracking branch 'upstream/main' into main
fdrgsp Jan 5, 2023
53793c7
Merge remote-tracking branch 'upstream/main' into main
fdrgsp Jan 5, 2023
7d35040
Merge remote-tracking branch 'upstream/main' into main
fdrgsp Jan 16, 2023
4cfa1d8
Merge remote-tracking branch 'upstream/main' into main
fdrgsp Feb 11, 2023
2013f12
Merge remote-tracking branch 'upstream/main' into main
fdrgsp Feb 14, 2023
461d122
Merge branch 'pymmcore-plus:main' into main
fdrgsp Feb 27, 2023
7f632e8
Merge branch 'pymmcore-plus:main' into main
fdrgsp Mar 20, 2023
4d05527
Merge branch 'main' of https://github.com/pymmcore-plus/pymmcore-widgets
fdrgsp Apr 28, 2023
db8a9a1
Merge remote-tracking branch 'upstream/main'
fdrgsp May 13, 2023
ae92ce8
Merge remote-tracking branch 'upstream/main'
fdrgsp May 19, 2023
51344b0
Merge remote-tracking branch 'upstream/main'
fdrgsp Jun 17, 2023
8c517d2
Merge remote-tracking branch 'upstream/main'
fdrgsp Jun 27, 2023
b219eb0
Merge remote-tracking branch 'upstream/main'
fdrgsp Jul 12, 2023
b4ec3ac
Merge remote-tracking branch 'upstream/main'
fdrgsp Jul 21, 2023
dd78bbb
Merge remote-tracking branch 'upstream/main'
fdrgsp Jul 26, 2023
430ed4b
Merge remote-tracking branch 'upstream/main'
fdrgsp Jul 27, 2023
4953e25
Merge remote-tracking branch 'upstream/main'
fdrgsp Jul 27, 2023
9452213
Merge remote-tracking branch 'upstream/main'
fdrgsp Jul 28, 2023
45f2f36
Merge remote-tracking branch 'upstream/main'
fdrgsp Jul 29, 2023
1b057ec
Merge remote-tracking branch 'upstream/main'
fdrgsp Aug 1, 2023
9d68592
Merge branch 'pymmcore-plus:main' into main
fdrgsp Aug 1, 2023
734de3d
Merge branch 'main' of https://github.com/fdrgsp/pymmcore-widgets
fdrgsp Sep 3, 2023
9ee2aa9
Merge remote-tracking branch 'upstream/main'
fdrgsp Sep 5, 2023
eede39a
Merge remote-tracking branch 'upstream/main'
fdrgsp Sep 8, 2023
7cc62b7
Merge remote-tracking branch 'upstream/main'
fdrgsp Sep 22, 2023
3b55c48
Merge remote-tracking branch 'upstream/main'
fdrgsp Sep 23, 2023
de82a7e
Merge remote-tracking branch 'upstream/main'
fdrgsp Sep 27, 2023
4723bf1
Merge remote-tracking branch 'upstream/main'
fdrgsp Sep 28, 2023
0652cfd
fix: fix mypy
fdrgsp Oct 2, 2023
45e2e3b
Merge remote-tracking branch 'upstream/main'
fdrgsp Oct 3, 2023
986945b
Merge remote-tracking branch 'upstream/main'
fdrgsp Oct 13, 2023
b08b801
Merge remote-tracking branch 'upstream/main'
fdrgsp Oct 17, 2023
621f035
Merge remote-tracking branch 'upstream/main'
fdrgsp Oct 27, 2023
0a4f036
Merge remote-tracking branch 'upstream/main'
fdrgsp Nov 1, 2023
e859764
Merge remote-tracking branch 'upstream/main'
fdrgsp Nov 16, 2023
4be0b2a
Merge remote-tracking branch 'upstream/main'
fdrgsp Nov 20, 2023
2b27b9b
Merge remote-tracking branch 'upstream/main'
fdrgsp Dec 3, 2023
35b1c80
Merge remote-tracking branch 'upstream/main'
fdrgsp Jan 6, 2024
c2c47ba
Merge remote-tracking branch 'upstream/main'
fdrgsp Jan 24, 2024
3645d34
Merge branch 'main' of https://github.com/pymmcore-plus/pymmcore-widgets
fdrgsp Jan 24, 2024
fbc20cc
Merge remote-tracking branch 'upstream/main'
fdrgsp Jan 26, 2024
12f22fc
Merge remote-tracking branch 'upstream/main'
fdrgsp Feb 3, 2024
00ce338
Merge remote-tracking branch 'upstream/main'
fdrgsp Feb 6, 2024
37f9a68
Merge remote-tracking branch 'upstream/main'
fdrgsp Feb 14, 2024
3a24246
Merge remote-tracking branch 'upstream/main'
fdrgsp Feb 15, 2024
cc0be31
Merge branch 'main' of https://github.com/pymmcore-plus/pymmcore-widgets
fdrgsp Mar 4, 2024
596e3da
Merge remote-tracking branch 'upstream/main'
fdrgsp Mar 8, 2024
c98526a
Merge remote-tracking branch 'upstream/main'
fdrgsp May 2, 2024
b696c99
Merge remote-tracking branch 'upstream/main'
fdrgsp May 8, 2024
d566379
Merge branch 'pymmcore-plus:main' into main
fdrgsp Jun 3, 2024
215b23f
Merge remote-tracking branch 'upstream/main'
fdrgsp Jun 23, 2024
1a06f17
Merge remote-tracking branch 'upstream/main'
fdrgsp Jul 6, 2024
1a2cd58
Merge remote-tracking branch 'upstream/main'
fdrgsp Jul 7, 2024
3be55a1
Merge remote-tracking branch 'upstream/main'
fdrgsp Jul 7, 2024
2d603a9
Merge remote-tracking branch 'upstream/main'
fdrgsp Jul 10, 2024
cd932c2
Merge remote-tracking branch 'upstream/main'
fdrgsp Jul 11, 2024
73c227b
Merge remote-tracking branch 'upstream/main'
fdrgsp Jul 16, 2024
5238976
Merge remote-tracking branch 'upstream/main'
fdrgsp Jul 18, 2024
3d2cbed
Merge remote-tracking branch 'upstream/main'
fdrgsp Jul 22, 2024
2eb8359
Merge remote-tracking branch 'upstream/main'
fdrgsp Jul 23, 2024
bd381bb
merge
fdrgsp Sep 12, 2024
01cc327
Merge remote-tracking branch 'upstream/main'
fdrgsp Sep 15, 2024
5946ba7
Merge remote-tracking branch 'upstream/main'
fdrgsp Sep 24, 2024
49bd6d5
Merge remote-tracking branch 'upstream/main'
fdrgsp Oct 4, 2024
ec0a068
Merge remote-tracking branch 'upstream/main'
fdrgsp Oct 6, 2024
009e161
Merge remote-tracking branch 'upstream/main'
fdrgsp Nov 4, 2024
2447e41
Merge remote-tracking branch 'upstream/main'
fdrgsp Nov 7, 2024
cf61710
Merge remote-tracking branch 'upstream/main'
fdrgsp Nov 8, 2024
c4178af
Merge branch 'pymmcore-plus:main' into main
fdrgsp Nov 9, 2024
f59bfa9
Merge remote-tracking branch 'upstream/main'
fdrgsp Nov 9, 2024
2e242b8
Merge remote-tracking branch 'upstream/main'
fdrgsp Nov 12, 2024
cf715b8
Merge remote-tracking branch 'upstream/main'
fdrgsp Nov 23, 2024
29a7557
feat: wip add stage explorer
fdrgsp Nov 27, 2024
b993122
feat: add properties + example
fdrgsp Nov 27, 2024
ff302b0
fix: auto_reset_view
fdrgsp Nov 27, 2024
811d96c
fix: update
fdrgsp Nov 27, 2024
3bb6df1
fix: _on_pixel_size_changed + rename
fdrgsp Nov 27, 2024
570ef9a
fix: DataStore get_image
fdrgsp Nov 27, 2024
724fdf1
fix: _on_setting_checked + rename
fdrgsp Nov 27, 2024
8d18acd
fix: remove _draw_scale_info
fdrgsp Nov 28, 2024
1efb902
fix: remove core from StageViewer and add it only to StageExplorer
fdrgsp Nov 28, 2024
5290dbf
fix: simplify StageViewer and move logic to StageExplorer
fdrgsp Nov 28, 2024
fb9fc83
fix: text
fdrgsp Nov 28, 2024
3bccb86
fix: reset_view
fdrgsp Nov 28, 2024
5984b33
fix: use float
fdrgsp Nov 28, 2024
f7a30cd
fix: remove swttings btn and use toolsbtns
fdrgsp Nov 28, 2024
9b98c4d
fix: remove swttings btn and use toolsbtns
fdrgsp Nov 28, 2024
14bbdf3
fix
fdrgsp Nov 28, 2024
48e755f
fix: use generator
fdrgsp Nov 28, 2024
9ebb34a
fix: remove print
fdrgsp Nov 29, 2024
3c1091c
fix: remove DataStore and use dict
fdrgsp Nov 29, 2024
d6c3ebe
fix: SS_TOOLBUTTON
fdrgsp Nov 29, 2024
c7e0382
fix: Reset View
fdrgsp Nov 29, 2024
7c96933
fix: map_marker
fdrgsp Nov 29, 2024
5a7d03c
fix: pos stage label
fdrgsp Nov 29, 2024
232c753
fix: example
fdrgsp Nov 29, 2024
c8bad47
fix: update ss
fdrgsp Nov 30, 2024
51128ec
wip
fdrgsp Nov 30, 2024
2d994a0
fix: not snap while mda is running
fdrgsp Dec 4, 2024
7b7a704
fix: _poll_stage_position
fdrgsp Dec 4, 2024
f4c55ad
fix: parent
fdrgsp Dec 5, 2024
58bab22
fix: example of crash
fdrgsp Dec 5, 2024
2e5bacb
fix: example of crash
fdrgsp Dec 5, 2024
312eab4
fix: remove unused
fdrgsp Dec 5, 2024
07334d1
Merge branch 'pymmcore-plus:main' into stage-explorer
fdrgsp Dec 6, 2024
0b7447a
wip
Dec 6, 2024
d0dc640
feat: use Rectangle
fdrgsp Dec 7, 2024
71fcdf4
fix: better handle reset_view
fdrgsp Dec 7, 2024
475852f
fix: remove auto_reset_view
fdrgsp Dec 7, 2024
7f84113
fix: fix pixel size
fdrgsp Dec 7, 2024
bdfabc1
fix: scale
fdrgsp Dec 7, 2024
4a2b9ba
fix: is_running
fdrgsp Dec 7, 2024
a7241f2
fix: remove print
fdrgsp Dec 7, 2024
adc29a1
fix: flip
fdrgsp Dec 7, 2024
848370d
fix: don't use private attr
fdrgsp Dec 8, 2024
ecf5433
fix: _is_image_within_view
fdrgsp Dec 8, 2024
11de99f
fix: wip
fdrgsp Dec 9, 2024
cac5fae
fix: add vispy
fdrgsp Dec 9, 2024
cc31110
wip
fdrgsp Dec 12, 2024
cdb827b
fix: example
fdrgsp Dec 12, 2024
12f14c8
fix: add MIN_XY_DIFF to reset view when polling
fdrgsp Dec 21, 2024
b5ff070
wip: rotation
fdrgsp Jan 2, 2025
8f8fbd6
fix: addSeparator
fdrgsp Jan 3, 2025
0426604
test: add StageViewer tests
fdrgsp Jan 3, 2025
27c6662
Merge branch 'pymmcore-plus:main' into stage-explorer
fdrgsp Jan 3, 2025
78aaa97
fix: update
fdrgsp Jan 4, 2025
12e2494
fix: rotation
fdrgsp Jan 4, 2025
446d419
fix: reorder
fdrgsp Jan 4, 2025
481aa2f
fix: update marker logic
fdrgsp Jan 4, 2025
27f3294
fix: wip _is_image_within_view
fdrgsp Jan 4, 2025
793b214
Merge remote-tracking branch 'upstream/main'
fdrgsp Jan 26, 2025
327c2bd
Merge remote-tracking branch 'upstream/main' into stage-explorer
fdrgsp Feb 8, 2025
93d6856
style(pre-commit.ci): auto fixes [...]
pre-commit-ci[bot] Feb 11, 2025
0203f21
Merge branch 'main' into stage-explorer
fdrgsp Feb 11, 2025
5e86861
fix flip
fdrgsp Feb 18, 2025
fd6cd84
handle rotation of stage pos marker
fdrgsp Feb 18, 2025
a581260
_handle_rotation
fdrgsp Feb 18, 2025
cdc2904
remove print
fdrgsp Feb 18, 2025
34673e7
updatre rotation
fdrgsp Feb 18, 2025
9e77238
wip roi
fdrgsp Feb 19, 2025
9833179
wip roi
fdrgsp Feb 19, 2025
5a150fe
wip multi roi
fdrgsp Feb 19, 2025
c3e1180
fix
fdrgsp Feb 19, 2025
b647e9c
fix
fdrgsp Feb 19, 2025
df6d9df
rectChanged
fdrgsp Feb 19, 2025
728066b
style(pre-commit.ci): auto fixes [...]
pre-commit-ci[bot] Feb 19, 2025
065a0e4
wip
fdrgsp Feb 20, 2025
b253609
wip
fdrgsp Feb 20, 2025
b422f0a
wip
fdrgsp Feb 20, 2025
26f004b
wip
fdrgsp Feb 20, 2025
173ec5c
style(pre-commit.ci): auto fixes [...]
pre-commit-ci[bot] Feb 20, 2025
eeea3b3
scale
fdrgsp Feb 21, 2025
d6f9be5
wip
fdrgsp Feb 21, 2025
4e40fba
wip
fdrgsp Feb 21, 2025
91d48c4
flip
fdrgsp Feb 21, 2025
5d01b2d
style(pre-commit.ci): auto fixes [...]
pre-commit-ci[bot] Feb 21, 2025
7848068
fix flip
fdrgsp Feb 21, 2025
adf0b85
example
fdrgsp Feb 21, 2025
fdd01f6
fix flip
Feb 21, 2025
79b394a
style(pre-commit.ci): auto fixes [...]
pre-commit-ci[bot] Feb 21, 2025
e08bbba
fix flip
Feb 21, 2025
a80915b
style(pre-commit.ci): auto fixes [...]
pre-commit-ci[bot] Feb 21, 2025
2303e35
remove print
fdrgsp Feb 22, 2025
f68744b
fix get x y from matrix
fdrgsp Feb 22, 2025
35dd645
fix _get_full_boundaries
fdrgsp Feb 23, 2025
fe0f7ec
remove unused
fdrgsp Feb 23, 2025
b785d50
roi
fdrgsp Feb 23, 2025
c0c7c8d
reorder
fdrgsp Feb 23, 2025
9d7629a
wip roi
fdrgsp Feb 23, 2025
cb955c3
cursor
fdrgsp Feb 23, 2025
006814b
comment + (dis)connect
fdrgsp Feb 23, 2025
cbd3eea
update rois
fdrgsp Feb 23, 2025
619bdda
update value()
fdrgsp Feb 23, 2025
341f59e
roi text
fdrgsp Feb 24, 2025
74f4eea
fix GridFromEdges
fdrgsp Feb 25, 2025
5a2ac78
_build_grid_plan
fdrgsp Feb 25, 2025
e3dcab7
update timerEvent
fdrgsp Feb 25, 2025
073fd79
style(pre-commit.ci): auto fixes [...]
pre-commit-ci[bot] Feb 25, 2025
627ffa7
TODO
fdrgsp Feb 25, 2025
cac6c86
increase marker pixels
fdrgsp Feb 25, 2025
d1f7d54
move roi logic to explorer
fdrgsp Feb 26, 2025
bd951b5
text
fdrgsp Feb 26, 2025
5b3b25e
fix section
fdrgsp Feb 26, 2025
09b6bbd
rois btns
fdrgsp Feb 26, 2025
a0021ae
fix RotationControl
fdrgsp Feb 26, 2025
bc219f0
Merge remote-tracking branch 'upstream/main'
fdrgsp Feb 27, 2025
8e4686d
fix _get_full_boundaries + stage marker
Feb 27, 2025
b3045b9
style(pre-commit.ci): auto fixes [...]
pre-commit-ci[bot] Feb 27, 2025
d0c615e
wip
fdrgsp Feb 28, 2025
ee729f9
fix
fdrgsp Feb 28, 2025
fc6ac85
Merge remote-tracking branch 'origin/main' into 1-stage-viewer
fdrgsp Feb 28, 2025
a69ba14
style(pre-commit.ci): auto fixes [...]
pre-commit-ci[bot] Mar 5, 2025
7f7356d
tests
fdrgsp Mar 5, 2025
28f1b54
fix pre-commit
fdrgsp Mar 5, 2025
0c7eb94
Merge branch 'main' into 1-stage-viewer
fdrgsp Mar 5, 2025
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
30 changes: 30 additions & 0 deletions examples/stage_viewer_widget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import numpy as np
from vispy import scene

from pymmcore_widgets.control._stage_explorer._stage_viewer import StageViewer

img = np.random.randint(0, 255, (256, 256))


class SV(StageViewer):
"""Stage viewer widget with mouse press event."""

def __init__(self):
super().__init__()

self.canvas.events.mouse_press.connect(self._on_mouse_press)

def _on_mouse_press(self, event) -> None:
"""Handle the mouse press event."""
canvas_pos = (event.pos[0], event.pos[1])
world_pos = self._tform().map(canvas_pos)[:2]
print()
print(world_pos)

def _tform(self) -> scene.transforms.BaseTransform:
"""Return the transform from canvas to scene."""
return list(self._get_images())[-1].transforms.get_transform("canvas", "scene")


wdg = SV()
wdg.show()
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -53,6 +53,7 @@ dependencies = [
'qtpy >=2.0',
'superqt[quantity,cmap] >=0.7.1',
'useq-schema >=0.5.0',
'vispy'
]

[tool.hatch.metadata]
Empty file.
105 changes: 105 additions & 0 deletions src/pymmcore_widgets/control/_stage_explorer/_stage_viewer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from collections.abc import Iterator
from typing import Optional, cast

import numpy as np
from qtpy.QtCore import Qt
from qtpy.QtWidgets import (
QVBoxLayout,
QWidget,
)
from vispy import scene
from vispy.scene.visuals import Image
from vispy.scene.widgets import ViewBox


class StageViewer(QWidget):
"""A widget to add images with a transform to a vispy canves."""

def __init__(self, parent: Optional[QWidget] = None) -> None:
super().__init__(parent)
self.setWindowTitle("Stage Explorer")
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)

self.canvas = scene.SceneCanvas(keys="interactive", show=True)
self.view = cast(ViewBox, self.canvas.central_widget.add_view())
self.view.camera = scene.PanZoomCamera(aspect=1)

main_layout = QVBoxLayout(self)
main_layout.setSpacing(0)
main_layout.setContentsMargins(0, 0, 0, 0)
main_layout.addWidget(self.canvas.native)

# --------------------PUBLIC METHODS--------------------

def add_image(self, img: np.ndarray, transform: np.ndarray | None = None) -> None:
"""Add an image to the scene with the given transform."""
frame = Image(img, cmap="grays", parent=self.view.scene, clim="auto")
# keep the added image on top of the others
frame.order = min(child.order for child in self._get_images()) - 1
# add the image to the scene with the transform
transform = np.eye(4) if transform is None else transform
frame.transform = scene.MatrixTransform(matrix=transform)

def clear_scene(self) -> None:
"""Clear the scene."""
# remove all images from the scene
for child in reversed(self.view.scene.children):
if isinstance(child, Image):
child.parent = None

def reset_view(self) -> None:
"""Recenter the view to the center of all images."""
min_x, max_x, min_y, max_y = self._get_full_boundaries()
if any(val is None for val in (min_x, max_x, min_y, max_y)):
return

Check warning on line 54 in src/pymmcore_widgets/control/_stage_explorer/_stage_viewer.py

Codecov / codecov/patch

src/pymmcore_widgets/control/_stage_explorer/_stage_viewer.py#L54

Added line #L54 was not covered by tests
self.view.camera.set_range(x=(min_x, max_x), y=(min_y, max_y), margin=0)

# --------------------PRIVATE METHODS--------------------

def _get_images(self) -> Iterator[Image]:
"""Yield images in the scene."""
for child in self.view.scene.children:
if isinstance(child, Image):
yield child

def _get_full_boundaries(
self, pixel_size: float = 1.0
) -> tuple[float | None, float | None, float | None, float | None]:
"""Return the boundaries of the images in the scene...

as (min_x, max_x, min_y, max_y).

The pixel_size is used to convert the image dimensions to the scene
coordinates.
"""
# TODO: maybe consider also the rectangles (ROIs) in the scene
all_corners: list[np.ndarray] = []
for child in self._get_images():
# get image dimensions
w, h = child.bounds(0)[1] * pixel_size, child.bounds(1)[1] * pixel_size
# get the (x, y) coordinates from the transform matrix
x, y = child.transform.matrix[3, :2]
# get the four corners of the image. NOTE: when an image is added to
# the scene with vispy, image origin is at the bottom-left corner
# TODO: Fix me! If we invert the coordinates in micromnager
# (e.g. transpose X), the image origin will be at the bottom-right.
# need to figure out what to do...
corners = np.array(
[
[x, y], # bottom-left
[x + w, y], # bottom-right
[x + w, y + h], # top-right
[x, y + h], # top-left
]
)
# transform the corners to scene coordinates
all_corners.append(corners)

if not all_corners:
return None, None, None, None

Check warning on line 99 in src/pymmcore_widgets/control/_stage_explorer/_stage_viewer.py

Codecov / codecov/patch

src/pymmcore_widgets/control/_stage_explorer/_stage_viewer.py#L99

Added line #L99 was not covered by tests

# combine all corners into one array and compute the bounding box
all_corners_combined = np.vstack(all_corners)
min_x, min_y = all_corners_combined.min(axis=0)
max_x, max_y = all_corners_combined.max(axis=0)
return min_x, max_x, min_y, max_y
3 changes: 3 additions & 0 deletions src/pymmcore_widgets/control/_stage_widget.py
Original file line number Diff line number Diff line change
@@ -464,6 +464,7 @@ def _move_stage_relative(self, x: float, y: float) -> None:
self._mmc.logMessage(f"Error moving stage: {e}") # pragma: no cover
else:
if self.snap_checkbox.isChecked():
self._mmc.waitForDevice(self._device)
self._mmc.snap()

def _move_x_absolute(self) -> None:
@@ -475,6 +476,7 @@ def _move_x_absolute(self) -> None:
self._mmc.logMessage(f"Error moving stage: {e}") # pragma: no cover
else:
if self.snap_checkbox.isChecked():
self._mmc.waitForDevice(self._device)
self._mmc.snap()

def _move_y_absolute(self) -> None:
@@ -489,6 +491,7 @@ def _move_y_absolute(self) -> None:
self._mmc.logMessage(f"Error moving stage: {e}") # pragma: no cover
else:
if self.snap_checkbox.isChecked():
self._mmc.waitForDevice(self._device)
self._mmc.snap()

def _disconnect(self) -> None:
52 changes: 52 additions & 0 deletions tests/test_stage_viewer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np
from vispy.scene.visuals import Image

from pymmcore_widgets.control._stage_explorer._stage_viewer import StageViewer

if TYPE_CHECKING:
from pytestqt.qtbot import QtBot

IMG = np.random.randint(0, 255, (100, 50), dtype=np.uint8)


def _build_transform_matrix(x: float, y: float) -> np.ndarray:
T = np.eye(4)
T[0, 3] += x
T[1, 3] += y
return T


def test_add_image(qtbot: QtBot):
stage_viewer = StageViewer()
qtbot.addWidget(stage_viewer)
T = _build_transform_matrix(100, 150)
stage_viewer.add_image(IMG, T.T)
images = [i for i in stage_viewer.view.scene.children if isinstance(i, Image)]
assert len(images) == 1
added_img = next(iter(images))
assert tuple(added_img.transform.matrix[3, :2]) == (100, 150)


def test_clear_scene(qtbot: QtBot):
stage_viewer = StageViewer()
qtbot.addWidget(stage_viewer)
T = _build_transform_matrix(200, 50)
stage_viewer.add_image(IMG, T.T)
assert [i for i in stage_viewer.view.scene.children if isinstance(i, Image)]
stage_viewer.clear_scene()
assert not [i for i in stage_viewer.view.scene.children if isinstance(i, Image)]


def test_reset_view(qtbot: QtBot):
stage_viewer = StageViewer()
qtbot.addWidget(stage_viewer)
T = _build_transform_matrix(500, 100)
stage_viewer.add_image(IMG, T.T)
stage_viewer.reset_view()
cx, cy = stage_viewer.view.camera.rect.center
assert round(cx) == 525 # image width is 50, center should be Tx + width/2
assert round(cy) == 150 # image height is 100, center should be Ty + height/2