diff --git a/gui/src/aaregui/main_window.py b/gui/src/aaregui/main_window.py index 7ef2256b..4bcfbba4 100644 --- a/gui/src/aaregui/main_window.py +++ b/gui/src/aaregui/main_window.py @@ -15,9 +15,11 @@ from aaredaqlib.sample_geometry import SampleGeometryModel from aaregui.panels.beamline_controls import BeamlineControls from aaregui.panels.data_collection_settings import DataCollectionSettings +from aaregui.panels.raster_data_collection import RasterDataCollectionPanel from aaregui.panels.sample_queue_panel import SampleQueuePanel from aaregui.panels.status_panel import StatusPanel from aaregui.panels.tell_sample_panel import TellSamplePanel +from aaregui.scan_logic.raster_grid_manager import RasterGridManager from aaregui.threads.camera_thread import SampleCameraThread from aaregui.threads.daq_worker import DAQWorker from aaregui.widgets.camera_image import SampleCameraImageLabel @@ -48,18 +50,19 @@ class MainWindow(QMainWindow): top_widget.setLayout(top_widget_layout) geom = SampleGeometryModel(beam_location_pxl=Coordinate(x=1000,y=1000), - pixel_in_mm=1, + pixel_in_mm=0.001, aerotech=Coordinate(), smargon=SmargonCoordinate(sh_mm=Coordinate(), phi_deg=0, chi_deg=0), omega_deg=0, - beam_size_mm=Coordinate(x=1, y=1), + beam_size_mm=Coordinate(x=0.01, y=0.01), aerotech_meas=Coordinate() ) self.data_collection = DataCollectionSettings(s=geom, parent=top_widget) top_widget_layout.addWidget(self.data_collection) - self.camera_image = SampleCameraImageLabel(geom=geom, parent=top_widget, default_image=default_image) + self.raster = RasterGridManager(geom=geom) + self.camera_image = SampleCameraImageLabel(geom=geom, raster=self.raster, parent=top_widget, default_image=default_image) top_widget_layout.addWidget(self.camera_image) @@ -164,9 +167,7 @@ class MainWindow(QMainWindow): self.tell_samples.mount.connect(self.daq.mount) self.tell_samples.unmount.connect(self.daq.unmount) - self.data_collection.raster.grid_size_updated.connect( - self.camera_image.update_raster_grid_size - ) + self.data_collection.raster.grid_size_updated.connect(self.raster.update_grid_size) self.status_bar.set_pgroup.connect(self.daq.set_pgroup) self.status_bar.end_session.connect(self.daq.end_session) self.status_bar.force_session.connect(self.daq.force_session) diff --git a/gui/src/aaregui/scan_logic/__init__.py b/gui/src/aaregui/scan_logic/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gui/src/aaregui/scan_logic/raster_grid_manager.py b/gui/src/aaregui/scan_logic/raster_grid_manager.py index 0303ad8d..ea334c67 100644 --- a/gui/src/aaregui/scan_logic/raster_grid_manager.py +++ b/gui/src/aaregui/scan_logic/raster_grid_manager.py @@ -63,18 +63,19 @@ class RasterGridManager(QObject): def __init__(self, geom): super().__init__() self.__geom = geom - self.__sample = None + + self.__start_point : QPointF = QPointF(0, 0) self.__active_grid : RasterGridRequest = RasterGridRequest( n_x= 0, n_y= 0, smargon = self.__geom.smargon, - grid_size_mm=Coordinate(x=0.1, y=0.1), + grid_size_mm=Coordinate(x=0.01, y=0.01), omega_deg=self.__geom.omega_deg, exp_time_s=1.0 ) self.__completed_grids : List[CompletedRasterGrid] = [] - def is_grid_visible(self, grid: RasterGridRequest): + def _is_grid_visible(self, grid: RasterGridRequest): if ( abs(normalize_angle(grid.omega_deg - self.__geom.omega_deg)) < 1.0 and abs(grid.smargon.phi_deg - self.__geom.smargon.phi_deg) < 1.0 @@ -89,21 +90,28 @@ class RasterGridManager(QObject): def update_daq_status(self, s: DAQStatusModel): self.__geom = s.geom - @Slot(QPointF, QPointF) - def update_raster_grid(self, start: QPointF, end: QPointF): + def resize_active_grid(self, end_point: QPointF): + self.update_active_grid(self.__start_point, end_point) + def update_active_grid(self, start: QPointF, end: QPointF): delta_x = (end.x() - start.x()) * self.__geom.pixel_in_mm delta_y = (end.y() - start.y()) * self.__geom.pixel_in_mm - self.__active_grid.n_x = round(abs(delta_x / self.__active_grid.grid_mm.x)) - self.__active_grid.n_y = round(abs(delta_y / self.__active_grid.grid_mm.y)) + self.__active_grid.n_x = round(abs(delta_x / self.__active_grid.grid_size_mm.x)) + self.__active_grid.n_y = round(abs(delta_y / self.__active_grid.grid_size_mm.y)) c = self.__geom.picture_to_sample(Coordinate(x=start.x(), y=start.y())) if delta_x < 0: - c.x -= self.__active_grid.n_x * self.__active_grid.grid_mm.x + c.x -= self.__active_grid.n_x * self.__active_grid.grid_size_mm.x if delta_y < 0: - c.y -= self.__active_grid.n_y * self.__active_grid.grid_mm.y + c.y -= self.__active_grid.n_y * self.__active_grid.grid_size_mm.y + + if delta_x < 0 or delta_y < 0: + c_pic = self.__geom.sample_to_picture(c) + self.__start_point = QPointF(c_pic.x, c_pic.y) + else: + self.__start_point = start self.__active_grid.smargon = SmargonCoordinate( sh_mm=self.__geom.beamline_to_smargon(c), @@ -113,6 +121,34 @@ class RasterGridManager(QObject): self.__active_grid.omega_deg = self.__geom.omega_deg + def move_active_grid(self, delta: QPointF): + if not self._is_grid_visible(self.__active_grid): + return + + delta_pxl = Coordinate(x=delta.x(), y=delta.y()) + + delta_mm = self.__geom.smargon_nudge(delta_pxl * self.__geom.pixel_in_mm) + + self.__active_grid.smargon = SmargonCoordinate( + sh_mm=self.__active_grid.smargon.sh_mm + delta_mm, + phi_deg=self.__geom.smargon.phi_deg, + chi_deg=self.__geom.smargon.chi_deg, + ) + + def clear_active_grid(self): + self.__active_grid.n_x = 0 + self.__active_grid.n_y = 0 + + def is_part_of_active_grid(self, point: QPointF) -> bool: + if not self._is_grid_visible(self.__active_grid): + return False + point_bl = self.__geom.picture_to_sample(Coordinate(x=point.x(), y=point.y())) + + delta = point_bl - self.__geom.smargon_to_beamline(self.__active_grid.smargon.sh_mm) + return (0 <= delta.x < self.__active_grid.n_x * self.__active_grid.grid_size_mm.x) and ( + 0 <= delta.y < self.__active_grid.n_y * self.__active_grid.grid_size_mm.y + ) + @Slot(float) def update_exposure_time(self, exp_time_s: float): self.__active_grid.exp_time_s = exp_time_s @@ -139,24 +175,26 @@ class RasterGridManager(QObject): self._draw_grid(painter, i.request, i.spot_count) def _draw_grid(self, painter: QPainter, grid: RasterGridRequest, values: List[float] | List[int] | None = None): - if not self.is_grid_visible(grid): + if not self._is_grid_visible(grid): return painter.setPen(QPen(QColor(114, 159, 207), 2, Qt.PenStyle.SolidLine)) - - min_value = min((x for x in values if not math.isnan(x)), default=0) - max_value = max((x for x in values if not math.isnan(x)), default=1) + if values is not None: + min_value = min((x for x in values if not math.isnan(x)), default=0) + max_value = max((x for x in values if not math.isnan(x)), default=1) + else: + min_value = 0 + max_value = 1 g = grid.grid_size_pxl(self.__geom) c0 = grid.start_pxl(self.__geom) for x in range(self.__active_grid.n_x): for y in range(self.__active_grid.n_y): pxl = x + y * self.__active_grid.n_x - if len(values) <= pxl or math.isnan(values[pxl]): + if values is None or len(values) <= pxl or math.isnan(values[pxl]): painter.setBrush(Qt.BrushStyle.NoBrush) else: painter.setBrush(QBrush(float_to_viridis_brush((values[pxl] - min_value) / (max_value - min_value), alpha=127))) - c = self.__geom.sample_to_picture( c0 + Coordinate(x=x * grid.grid_size_mm.x, y=y * grid.grid_size_mm.y) diff --git a/gui/src/aaregui/widgets/camera_image.py b/gui/src/aaregui/widgets/camera_image.py index f7806469..93184480 100644 --- a/gui/src/aaregui/widgets/camera_image.py +++ b/gui/src/aaregui/widgets/camera_image.py @@ -1,4 +1,4 @@ -from typing import List +from enum import Enum from PySide6.QtCore import Qt, QTimer, QRect, QPoint, Signal, Slot, QPointF from PySide6.QtGui import ( @@ -26,7 +26,14 @@ from aaregui.models.bookmark import SmargonBookmarkList from aaredaqlib.coordinate import Coordinate, SmargonCoordinate from aaredaqlib.raster_grid import RasterGridRequest from aaredaqlib.sample_geometry import SampleGeometryModel +from aaregui.scan_logic.raster_grid_manager import RasterGridManager +class SampleCameraImageState(Enum): + IDLE = 0 + DRAWING_RASTER_GRID = 1 + MOVING_RASTER_GRID = 2 + RESIZE_RASTER_GRID = 3 + BEAM_MARKING = 4 class SampleCameraImageLabel(QGraphicsView): smargon = Signal(SmargonCoordinate) @@ -41,29 +48,24 @@ class SampleCameraImageLabel(QGraphicsView): def __init__( self, geom: SampleGeometryModel, + raster: RasterGridManager, default_image: str | None, parent=None, ): super().__init__(parent) - self.__raster = [] + self.__raster_mgr = raster + self.__state = SampleCameraImageState.IDLE self.__geom = geom self.__bookmarks: SmargonBookmarkList = SmargonBookmarkList() - self.__autoscale = True + self.__autoscale = False self.__show_coords = False self.__helical_start = SmargonCoordinate() self.__helical_end = SmargonCoordinate() - self.drawing = False # Indicates whether the mouse is currently drawing - self.moving_raster_grid = None - self.right_click_grid = None - self.active_grid = None - self.drawn_grid = None - self.start_point = None # Starting point of the rectangle self.end_point = None # Ending point of the rectangle - self.rectangles = [] # List of all drawn rectangles self.scene = QGraphicsScene(self) self.setScene(self.scene) @@ -75,7 +77,10 @@ class SampleCameraImageLabel(QGraphicsView): self.pixmap_item = QGraphicsPixmapItem(pixmap) self.scene.addItem(self.pixmap_item) else: - self.pixmap_item = None + pixmap = QPixmap(2000, 2000) + pixmap.fill(Qt.GlobalColor.white) + self.pixmap_item = QGraphicsPixmapItem(pixmap) + self.scene.addItem(self.pixmap_item) self.setFrameShape(QFrame.Shape.NoFrame) self.setRenderHints(QPainter.RenderHint.Antialiasing) @@ -91,28 +96,44 @@ class SampleCameraImageLabel(QGraphicsView): self.left_click_hold_threshold = 200 self.right_click_hold_threshold = 200 - # Default raster grid size - self.raster_grid_x = 0.02 - self.raster_grid_y = 0.02 - def drawForeground(self, painter, rect): self.__draw_beam_center(painter) - self.__draw_grids(painter) + self.__raster_mgr.draw_grid(painter) self.__draw_helical(painter) def resizeEvent(self, event): - # Call the base class implementation of resizeEvent to maintain default behavior super().resizeEvent(event) self.__scaling() def mousePressEvent(self, event): - pos = self.mapToScene(event.pos()) - if event.button() == Qt.MouseButton.RightButton: - self.__start_drawing(pos) - self.click_timer.start(self.right_click_hold_threshold) - elif event.button() == Qt.MouseButton.LeftButton: - self.__start_moving(pos) - self.click_timer.start(self.left_click_hold_threshold) + self.start_point = self.mapToScene(event.pos()) + print(self.__state) + + match self.__state: + case SampleCameraImageState.BEAM_MARKING: + if event.button() == Qt.MouseButton.LeftButton: + self.update_beam_center.emit(self.start_point.x(), self.start_point.y()) + case SampleCameraImageState.IDLE: + self.click_timer.start(self.right_click_hold_threshold) + if event.button() == Qt.MouseButton.RightButton: + if self.__raster_mgr.is_part_of_active_grid(self.start_point): + self.__state = SampleCameraImageState.RESIZE_RASTER_GRID + else: + self.__state = SampleCameraImageState.DRAWING_RASTER_GRID + elif event.button() == Qt.MouseButton.LeftButton: + if self.__raster_mgr.is_part_of_active_grid(self.start_point): + self.__state = SampleCameraImageState.MOVING_RASTER_GRID + + def _update_grid(self): + pass + match self.__state: + case SampleCameraImageState.DRAWING_RASTER_GRID: + self.__raster_mgr.update_active_grid(self.start_point, self.end_point) + case SampleCameraImageState.MOVING_RASTER_GRID: + self.__raster_mgr.move_active_grid(self.end_point - self.start_point) + self.start_point = self.end_point + case SampleCameraImageState.RESIZE_RASTER_GRID: + self.__raster_mgr.resize_active_grid(self.end_point) def mouseMoveEvent(self, event): mouse_pos = self.mapToScene(event.pos()) @@ -122,11 +143,10 @@ class SampleCameraImageLabel(QGraphicsView): self.mapToGlobal(event.pos()), f"{x:.0f}, {y:.0f} pxl", self ) self.end_point = mouse_pos - if self.drawing and not self.click_timer.isActive(): - self.__update_drawn_grid() - self.update() - elif self.moving_raster_grid is not None and not self.click_timer.isActive(): - self.__update_moving_raster_grid() + + if not self.click_timer.isActive(): + self.end_point = self.mapToScene(event.pos()) + self._update_grid() self.update() def mouseReleaseEvent(self, event): @@ -134,14 +154,18 @@ class SampleCameraImageLabel(QGraphicsView): if self.click_timer.isActive(): self.click_timer.stop() self.__left_single_click(event) - self.__end_moving(event, self.click_timer.isActive()) - + else: + self._update_grid() + self.update() if event.button() == Qt.MouseButton.RightButton: if self.click_timer.isActive(): self.click_timer.stop() self.__right_click_menu(event) else: - self.__end_drawing(event) + self._update_grid() + self.update() + if self.__state != SampleCameraImageState.BEAM_MARKING: + self.__state = SampleCameraImageState.IDLE def __right_click_menu(self, event): self.drawing = False @@ -166,9 +190,9 @@ class SampleCameraImageLabel(QGraphicsView): delete_action = None evaluate_action = None - self.active_grid = self.right_click_grid + scene_pos = self.mapToScene(event.pos()) - if self.right_click_grid is not None: + if self.__raster_mgr.is_part_of_active_grid(scene_pos): menu.addSection("Grid") delete_action = menu.addAction("Delete") evaluate_action = menu.addAction("Evaluate") @@ -180,11 +204,10 @@ class SampleCameraImageLabel(QGraphicsView): elif action == scale_action: self.__autoscale = not self.__autoscale self.__scaling() - elif self.right_click_grid is not None and action == delete_action: - self.__raster.remove(self.right_click_grid) - self.right_click_grid = None - elif self.right_click_grid is not None and action == evaluate_action: - self.run_raster.emit(self.right_click_grid) + elif action == delete_action: + self.__raster_mgr.clear_active_grid() + elif action == evaluate_action: + self.__raster_mgr.run_grid_scan() elif action == grab_action: self.__screenshot_with_dialog(overlay=False) elif action == grab_with_overlay_action: @@ -197,7 +220,6 @@ class SampleCameraImageLabel(QGraphicsView): elif action == beam_mark_action: c = self.mapToScene(event.pos()) self.update_beam_center.emit(c.x(), c.y()) - self.active_grid = None self.update() def __screenshot_with_dialog(self, overlay: bool): @@ -232,9 +254,7 @@ class SampleCameraImageLabel(QGraphicsView): if self.pixmap_item.boundingRect().width() == 0: ratio = 1.0 else: - ratio = ( - self.viewport().size().width() / self.pixmap_item.boundingRect().width() - ) + ratio = self.viewport().size().width() / self.pixmap_item.boundingRect().width() if ratio < 0.2: ratio = 0.2 @@ -246,11 +266,6 @@ class SampleCameraImageLabel(QGraphicsView): matrix.scale(ratio, ratio) self.setTransform(matrix) - @Slot(float, float) - def update_raster_grid_size(self, grid_size_x: float, grid_size_y: float): - self.raster_grid_x = grid_size_x - self.raster_grid_y = grid_size_y - @Slot(QPixmap) def update_pixmap(self, pixmap: QPixmap): if self.pixmap_item is not None: # Ensure pixmap_item exists @@ -264,86 +279,19 @@ class SampleCameraImageLabel(QGraphicsView): @Slot(DAQStatusModel) def update_daq_status(self, s: DAQStatusModel): self.__geom = s.geom - # View is redrawn when new camera image will arrive - - def __point_is_part_of_raster(self, pos) -> RasterGridRequest | None: - c = self.__geom.picture_to_sample(Coordinate(x=pos.x(), y=pos.y())) - - for i in self.__raster: - if i is not None and i.point_sample_belongs_to_grid(self.__geom, c): - return i - return None - - def __update_moving_raster_grid(self): - delta_pxl = Coordinate( - x=self.end_point.x() - self.start_point.x(), - y=self.end_point.y() - self.start_point.y(), - ) - self.start_point = self.end_point - - delta_mm = self.__geom.smargon_nudge(delta_pxl * self.__geom.pixel_in_mm) - - self.moving_raster_grid.smargon = SmargonCoordinate( - sh_mm=self.moving_raster_grid.smargon.sh_mm + delta_mm, - phi_deg=self.__geom.smargon.phi_deg, - chi_deg=self.__geom.smargon.chi_deg, - ) def __left_single_click(self, event): - self.end_point = self.mapToScene(event.pos()) + match self.__state: + case SampleCameraImageState.IDLE: + self.end_point = self.mapToScene(event.pos()) - sample_coord = self.__geom.picture_to_sample( - Coordinate(x=self.end_point.x(), y=self.end_point.y()) - ) - smargon_coord = SmargonCoordinate( - sh_mm=self.__geom.beamline_to_smargon(sample_coord) - ) - - self.smargon.emit(smargon_coord) - - def __start_moving(self, pos): - self.moving_raster_grid = self.__point_is_part_of_raster(pos) - self.active_grid = self.moving_raster_grid - self.start_point = pos # Record the starting position - self.end_point = self.start_point - - def __end_moving(self, event, too_short: bool): - self.end_point = self.mapToScene(event.pos()) - self.active_grid = None - if self.moving_raster_grid is not None and not too_short: - self.__update_moving_raster_grid() - self.moving_raster_grid = None - self.update() - - def __start_drawing(self, pos): - self.drawing = True - self.right_click_grid = self.__point_is_part_of_raster(pos) - if self.right_click_grid is not None: - # Starting position is top-left corner of the old raster - old_start_pos = self.__geom.sample_to_picture( - self.right_click_grid.start_pxl(geom=self.__geom) - ) - self.start_point = QPoint(int(old_start_pos.x), int(old_start_pos.y)) - else: - self.start_point = pos - self.end_point = pos - - def __end_drawing(self, event): - self.drawing = False - self.end_point = self.mapToScene(event.pos()) - - self.__update_drawn_grid() - - if self.right_click_grid is not None: - self.__raster.remove(self.right_click_grid) - if self.drawn_grid is not None: - self.__raster.append(self.drawn_grid) - - self.active_grid = None - self.right_click_grid = None - self.drawn_grid = None - - self.update() # Repaint to permanently draw the rectangle + sample_coord = self.__geom.picture_to_sample( + Coordinate(x=self.end_point.x(), y=self.end_point.y()) + ) + smargon_coord = SmargonCoordinate( + sh_mm=self.__geom.beamline_to_smargon(sample_coord) + ) + self.smargon.emit(smargon_coord) def __draw_beam_center(self, painter: QPainter): beam_size_pxl = self.__geom.beam_size_pxl @@ -398,67 +346,6 @@ class SampleCameraImageLabel(QGraphicsView): if start_pxl is not None and end_pxl is not None: self.__draw_arrow(painter, start_pxl, end_pxl) - def __draw_grid(self, painter: QPainter, r: RasterGridRequest): - painter.setBrush(Qt.BrushStyle.NoBrush) - if r == self.active_grid: - painter.setPen(QPen(QColor(114, 159, 207), 2, Qt.PenStyle.SolidLine)) - else: - painter.setPen(QPen(QColor(255, 0, 255), 2, Qt.PenStyle.SolidLine)) - - g = r.grid_size_pxl(self.__geom) - c0 = r.start_pxl(self.__geom) - for x in range(r.n_x): - for y in range(r.n_y): - c = self.__geom.sample_to_picture( - c0 + Coordinate(x=x * r.grid_size_mm.x, y=y * r.grid_size_mm.y) - ) - painter.drawRect(QRect(int(c.x), int(c.y), int(g.x), int(g.y))) - - def __draw_grids(self, painter): - for i in self.__raster: - if i is not None and i.grid_visible(self.__geom): - self.__draw_grid(painter, i) - - if self.drawn_grid is not None: - self.__draw_grid(painter, self.drawn_grid) - - def __update_drawn_grid(self): - if self.right_click_grid is None: - grid_mm = Coordinate(x=self.raster_grid_x, y=self.raster_grid_y) - else: - grid_mm = self.right_click_grid.grid_size_mm - - delta_x = self.end_point.x() - self.start_point.x() - delta_y = self.end_point.y() - self.start_point.y() - n_x = round(abs(delta_x * self.__geom.pixel_in_mm / grid_mm.x)) - n_y = round(abs(delta_y * self.__geom.pixel_in_mm / grid_mm.y)) - - c = self.__geom.picture_to_sample( - Coordinate(x=self.start_point.x(), y=self.start_point.y()) - ) - - if delta_x < 0: - c.x -= n_x * grid_mm.x - if delta_y < 0: - c.y -= n_y * grid_mm.y - - if n_x > 0 and n_y > 0: - self.drawn_grid = RasterGridRequest( - n_x=n_x, - n_y=n_y, - smargon=SmargonCoordinate( - sh_mm=self.__geom.beamline_to_smargon(c), - phi_deg=self.__geom.smargon.phi_deg, - chi_deg=self.__geom.smargon.chi_deg, - ), - grid_size_mm=grid_mm, - omega_deg=self.__geom.omega_deg, - exp_time_s=1.0 - ) - self.active_grid = self.drawn_grid - else: - self.drawn_grid = None - @Slot(SmargonCoordinate) def show_helical_start(self, pos: SmargonCoordinate): self.__helical_start = pos