From 308265c7b1584ed77d5be3c4f81877e66b3f8eba Mon Sep 17 00:00:00 2001 From: leonarski_f Date: Fri, 19 Jun 2026 13:23:03 +0200 Subject: [PATCH] viewer: move the selected box/circle ROI on the canvas Add base-class mouse hooks (roiEditPress/Move/Release) so the diffraction image can edit the persistent ROI selected in the side-panel combobox. The selected ROI is highlighted (dashed, thicker), and dragging its interior moves a box or circle; on release the new geometry is committed through SetROIDefinition, which recomputes only the ROIs. Resize, azimuthal handles and delete-key follow. Co-Authored-By: Claude Opus 4.8 --- viewer/JFJochViewerWindow.cpp | 4 + .../image_viewer/JFJochDiffractionImage.cpp | 102 ++++++++++++++++-- viewer/image_viewer/JFJochDiffractionImage.h | 19 ++++ viewer/image_viewer/JFJochImage.cpp | 19 +++- viewer/image_viewer/JFJochImage.h | 9 +- 5 files changed, 142 insertions(+), 11 deletions(-) diff --git a/viewer/JFJochViewerWindow.cpp b/viewer/JFJochViewerWindow.cpp index 42b905cc..57215cd6 100644 --- a/viewer/JFJochViewerWindow.cpp +++ b/viewer/JFJochViewerWindow.cpp @@ -206,6 +206,10 @@ JFJochViewerWindow::JFJochViewerWindow(QWidget *parent, bool dbus, const QString connect(side_panel, &JFJochViewerSidePanel::roisChanged, reading_worker, &JFJochImageReadingWorker::SetROIDefinition); + connect(side_panel, &JFJochViewerSidePanel::selectedROIChanged, + viewer, &JFJochDiffractionImage::setSelectedROI); + connect(viewer, &JFJochDiffractionImage::roiGeometryEdited, + reading_worker, &JFJochImageReadingWorker::SetROIDefinition); connect(side_panel, &JFJochViewerSidePanel::ROIBoxConfigured, reading_worker, &JFJochImageReadingWorker::SetROIBox); diff --git a/viewer/image_viewer/JFJochDiffractionImage.cpp b/viewer/image_viewer/JFJochDiffractionImage.cpp index 93889186..ec1165cb 100644 --- a/viewer/image_viewer/JFJochDiffractionImage.cpp +++ b/viewer/image_viewer/JFJochDiffractionImage.cpp @@ -351,17 +351,31 @@ void JFJochDiffractionImage::DrawROIs() { for (const auto &b : rois.boxes) { QColor c = palette[color_index++ % palette_size]; - QPen pen(c, 2); pen.setCosmetic(true); - addOverlayItem(scene()->addRect(b.GetXMin(), b.GetYMin(), b.GetWidth(), b.GetHeight(), pen, fill_brush(c))); - AddROILabel(b.GetName(), c, b.GetXMin(), b.GetYMin()); + const bool selected = (QString::fromStdString(b.GetName()) == selected_roi_); + QPen pen(c, selected ? 3 : 2); + pen.setCosmetic(true); + if (selected) pen.setStyle(Qt::DashLine); // highlight the editable ROI + const QRectF rect = (roi_edit_ == RoiEdit::MoveBox && b.GetName() == edit_name_.toStdString()) + ? edit_box_ + : QRectF(b.GetXMin(), b.GetYMin(), b.GetWidth(), b.GetHeight()); + addOverlayItem(scene()->addRect(rect, pen, fill_brush(c))); + AddROILabel(b.GetName(), c, rect.left(), rect.top()); } for (const auto &c_roi : rois.circles) { QColor c = palette[color_index++ % palette_size]; - QPen pen(c, 2); pen.setCosmetic(true); - const float r = c_roi.GetRadius_pxl(); - addOverlayItem(scene()->addEllipse(c_roi.GetX() - r, c_roi.GetY() - r, 2 * r, 2 * r, pen, fill_brush(c))); - AddROILabel(c_roi.GetName(), c, c_roi.GetX(), c_roi.GetY()); + const bool selected = (QString::fromStdString(c_roi.GetName()) == selected_roi_); + QPen pen(c, selected ? 3 : 2); + pen.setCosmetic(true); + if (selected) pen.setStyle(Qt::DashLine); + QPointF center(c_roi.GetX(), c_roi.GetY()); + double r = c_roi.GetRadius_pxl(); + if (roi_edit_ == RoiEdit::MoveCircle && c_roi.GetName() == edit_name_.toStdString()) { + center = edit_center_; + r = edit_radius_; + } + addOverlayItem(scene()->addEllipse(center.x() - r, center.y() - r, 2 * r, 2 * r, pen, fill_brush(c))); + AddROILabel(c_roi.GetName(), c, center.x(), center.y()); } for (const auto &az : rois.azimuthal) @@ -446,6 +460,80 @@ void JFJochDiffractionImage::showROIFill(bool input) { updateOverlay(); } +void JFJochDiffractionImage::setSelectedROI(QString name) { + selected_roi_ = name; + updateOverlay(); +} + +bool JFJochDiffractionImage::roiEditPress(const QPointF &scenePos) { + if (!image || selected_roi_.isEmpty()) + return false; + + const auto &rois = image->Dataset().experiment.ROI().GetROIDefinition(); + const std::string sel = selected_roi_.toStdString(); + + for (const auto &b : rois.boxes) + if (b.GetName() == sel) { + const QRectF r(QPointF(b.GetXMin(), b.GetYMin()), QPointF(b.GetXMax(), b.GetYMax())); + if (!r.contains(scenePos)) + return false; + roi_edit_ = RoiEdit::MoveBox; + edit_name_ = selected_roi_; + edit_box_ = r; + move_last_ = scenePos; + return true; + } + for (const auto &c : rois.circles) + if (c.GetName() == sel) { + const QPointF center(c.GetX(), c.GetY()); + if (QLineF(center, scenePos).length() > c.GetRadius_pxl()) + return false; + roi_edit_ = RoiEdit::MoveCircle; + edit_name_ = selected_roi_; + edit_center_ = center; + edit_radius_ = c.GetRadius_pxl(); + move_last_ = scenePos; + return true; + } + return false; // azimuthal editing is not yet supported +} + +void JFJochDiffractionImage::roiEditMove(const QPointF &scenePos) { + const QPointF delta = scenePos - move_last_; + move_last_ = scenePos; + if (roi_edit_ == RoiEdit::MoveBox) + edit_box_.translate(delta); + else if (roi_edit_ == RoiEdit::MoveCircle) + edit_center_ += delta; + updateOverlay(); +} + +void JFJochDiffractionImage::roiEditRelease() { + if (roi_edit_ == RoiEdit::None || !image) { + roi_edit_ = RoiEdit::None; + return; + } + + ROIDefinition rois = image->Dataset().experiment.ROI().GetROIDefinition(); + const std::string sel = edit_name_.toStdString(); + if (roi_edit_ == RoiEdit::MoveBox) { + for (auto &b : rois.boxes) + if (b.GetName() == sel) { + b = ROIBox(sel, std::lround(edit_box_.left()), std::lround(edit_box_.right()), + std::lround(edit_box_.top()), std::lround(edit_box_.bottom())); + break; + } + } else if (roi_edit_ == RoiEdit::MoveCircle) { + for (auto &c : rois.circles) + if (c.GetName() == sel) { + c = ROICircle(sel, edit_center_.x(), edit_center_.y(), edit_radius_); + break; + } + } + roi_edit_ = RoiEdit::None; + emit roiGeometryEdited(rois); +} + void JFJochDiffractionImage::UpdateForeground() { if (!image || !auto_fg) diff --git a/viewer/image_viewer/JFJochDiffractionImage.h b/viewer/image_viewer/JFJochDiffractionImage.h index 14917fdc..dc068b00 100644 --- a/viewer/image_viewer/JFJochDiffractionImage.h +++ b/viewer/image_viewer/JFJochDiffractionImage.h @@ -5,6 +5,7 @@ #include "JFJochImage.h" #include "../../reader/JFJochReaderImage.h" +#include "../../common/ROIDefinition.h" class ROIAzimuthal; class DiffractionGeometry; @@ -31,6 +32,21 @@ private: void DrawROIs(); void DrawAzimuthalROI(const ROIAzimuthal &az, const QColor &color, const DiffractionGeometry &geom); void AddROILabel(const std::string &name, const QColor &color, float px, float py); + + // Interactive editing of the selected (named) ROI: move box/circle for now. + bool roiEditPress(const QPointF &scenePos) override; + void roiEditMove(const QPointF &scenePos) override; + void roiEditRelease() override; + + QString selected_roi_; + enum class RoiEdit { None, MoveBox, MoveCircle }; + RoiEdit roi_edit_ = RoiEdit::None; + QString edit_name_; + QRectF edit_box_; + QPointF edit_center_; + double edit_radius_ = 0; + QPointF move_last_; + void DrawSpots(); void DrawPredictions(); void DrawBeamCenter(); @@ -63,7 +79,10 @@ private: void mouseHover(QMouseEvent* event) override; +signals: + void roiGeometryEdited(ROIDefinition rois); public slots: + void setSelectedROI(QString name); void loadImage(std::shared_ptr image); void setAutoForeground(bool input); void setResolutionRing(QVector v); diff --git a/viewer/image_viewer/JFJochImage.cpp b/viewer/image_viewer/JFJochImage.cpp index 7f151a92..0dd1b6d3 100644 --- a/viewer/image_viewer/JFJochImage.cpp +++ b/viewer/image_viewer/JFJochImage.cpp @@ -157,6 +157,13 @@ void JFJochImage::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { const QPointF scenePos = mapToScene(event->pos()); + + if (roiEditPress(scenePos)) { + mouse_event_type = MouseEventType::EditingExternalROI; + event->accept(); + return; + } + active_handle_ = hitTestROIHandle(scenePos, 4.0 / std::sqrt(std::max(1e-4, scale_factor))); if (active_handle_ != ResizeHandle::None && active_handle_ != ResizeHandle::Inside) { @@ -193,6 +200,9 @@ void JFJochImage::mouseMoveEvent(QMouseEvent *event) { QPointF delta; switch (mouse_event_type) { + case MouseEventType::EditingExternalROI: + roiEditMove(scenePos); + return; case MouseEventType::Panning: { const QPoint viewDelta = event->pos() - lastMousePos; lastMousePos = event->pos(); @@ -284,10 +294,13 @@ void JFJochImage::mouseReleaseEvent(QMouseEvent *event) { if (!scene()) return; if (event->button() == Qt::LeftButton) { - if (mouse_event_type == MouseEventType::DrawingROI) { - roiEndPos = RoundPoint(mapToScene(event->pos())); + if (mouse_event_type == MouseEventType::EditingExternalROI) { + roiEditRelease(); + } else { + if (mouse_event_type == MouseEventType::DrawingROI) + roiEndPos = RoundPoint(mapToScene(event->pos())); + updateROI(); } - updateROI(); } mouse_event_type = MouseEventType::None; diff --git a/viewer/image_viewer/JFJochImage.h b/viewer/image_viewer/JFJochImage.h index 5e98e599..cbdd3634 100644 --- a/viewer/image_viewer/JFJochImage.h +++ b/viewer/image_viewer/JFJochImage.h @@ -75,8 +75,15 @@ protected: // Helper: add an overlay item to the scene and track it for selective removal void addOverlayItem(QGraphicsItem *item); - enum class MouseEventType {None, Panning, DrawingROI, MovingROI, ResizingROI}; + enum class MouseEventType {None, Panning, DrawingROI, MovingROI, ResizingROI, EditingExternalROI}; MouseEventType mouse_event_type = MouseEventType::None; + + // Hooks for editing a named, persistent ROI drawn by a subclass (the base only + // owns the mouse events; the subclass knows the loaded ROIs). roiEditPress returns + // true to claim the gesture for ROI editing. + virtual bool roiEditPress(const QPointF &scenePos) { return false; } + virtual void roiEditMove(const QPointF &scenePos) {} + virtual void roiEditRelease() {} QPoint lastMousePos; // To track panning movement // Resizing which edge/corner