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 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 13:56:09 +02:00
co-authored by Claude Opus 4.8
parent 0c780a5cbb
commit 308265c7b1
5 changed files with 142 additions and 11 deletions
+4
View File
@@ -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);
+95 -7
View File
@@ -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)
@@ -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<const JFJochReaderImage> image);
void setAutoForeground(bool input);
void setResolutionRing(QVector<float> v);
+16 -3
View File
@@ -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;
+8 -1
View File
@@ -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