diff --git a/viewer/image_viewer/JFJochDiffractionImage.cpp b/viewer/image_viewer/JFJochDiffractionImage.cpp index 6414de8f..62cd60a2 100644 --- a/viewer/image_viewer/JFJochDiffractionImage.cpp +++ b/viewer/image_viewer/JFJochDiffractionImage.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -20,7 +21,38 @@ #include "JFJochSimpleImage.h" // Constructor -JFJochDiffractionImage::JFJochDiffractionImage(QWidget *parent) : JFJochImage(parent) {} +static float AngularDistance_deg(float a, float b) { + float d = std::fabs(a - b); + return (d > 180.0f) ? 360.0f - d : d; +} + +static bool InPhiSector(float phi, float phi_min, float phi_max) { + if (phi_min <= phi_max) + return phi >= phi_min && phi <= phi_max; + return phi >= phi_min || phi <= phi_max; +} + +JFJochDiffractionImage::JFJochDiffractionImage(QWidget *parent) : JFJochImage(parent) { + setFocusPolicy(Qt::StrongFocus); // so the Delete key reaches the view +} + +JFJochImage::ResizeHandle JFJochDiffractionImage::hitTestBoxHandle(const QRectF &r, const QPointF &p, qreal tol) const { + auto on = [&](qreal a, qreal b) { return std::abs(a - b) <= tol; }; + const bool L = on(p.x(), r.left()), R = on(p.x(), r.right()); + const bool T = on(p.y(), r.top()), B = on(p.y(), r.bottom()); + const bool inX = p.x() >= r.left() - tol && p.x() <= r.right() + tol; + const bool inY = p.y() >= r.top() - tol && p.y() <= r.bottom() + tol; + if (L && T) return ResizeHandle::TopLeft; + if (R && T) return ResizeHandle::TopRight; + if (L && B) return ResizeHandle::BottomLeft; + if (R && B) return ResizeHandle::BottomRight; + if (L && inY) return ResizeHandle::Left; + if (R && inY) return ResizeHandle::Right; + if (T && inX) return ResizeHandle::Top; + if (B && inX) return ResizeHandle::Bottom; + if (r.contains(p)) return ResizeHandle::Inside; + return ResizeHandle::None; +} void JFJochDiffractionImage::mouseHover(QMouseEvent *event) { auto coord = mapToScene(event->pos()); @@ -348,38 +380,67 @@ void JFJochDiffractionImage::DrawROIs() { auto fill_brush = [&](const QColor &c) { return show_roi_fill ? QBrush(QColor(c.red(), c.green(), c.blue(), 60)) : QBrush(Qt::NoBrush); }; + auto draw_handle = [&](const QPointF &p, const QColor &c) { + const qreal s = 4.0 / std::sqrt(std::max(1e-4, scale_factor)); + addOverlayItem(scene()->addRect(QRectF(p.x() - s, p.y() - s, 2 * s, 2 * s), QPen(c, 1), QBrush(c))); + }; for (const auto &b : rois.boxes) { QColor c = palette[color_index++ % palette_size]; const bool selected = (QString::fromStdString(b.GetName()) == selected_roi_); + const bool editing = b.GetName() == edit_name_.toStdString() + && (roi_edit_ == RoiEdit::MoveBox || roi_edit_ == RoiEdit::ResizeBox); 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()); + const QRectF rect = editing ? 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()); + if (selected) { + draw_handle(rect.topLeft(), c); draw_handle(rect.topRight(), c); + draw_handle(rect.bottomLeft(), c); draw_handle(rect.bottomRight(), c); + draw_handle({rect.center().x(), rect.top()}, c); + draw_handle({rect.center().x(), rect.bottom()}, c); + draw_handle({rect.left(), rect.center().y()}, c); + draw_handle({rect.right(), rect.center().y()}, c); + } } for (const auto &c_roi : rois.circles) { QColor c = palette[color_index++ % palette_size]; const bool selected = (QString::fromStdString(c_roi.GetName()) == selected_roi_); + const bool editing = c_roi.GetName() == edit_name_.toStdString() + && (roi_edit_ == RoiEdit::MoveCircle || roi_edit_ == RoiEdit::ResizeCircle); 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_; - } + const QPointF center = editing ? edit_center_ : QPointF(c_roi.GetX(), c_roi.GetY()); + const double r = editing ? edit_radius_ : c_roi.GetRadius_pxl(); 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()); + if (selected) { + draw_handle({center.x() + r, center.y()}, c); + draw_handle({center.x() - r, center.y()}, c); + draw_handle({center.x(), center.y() + r}, c); + draw_handle({center.x(), center.y() - r}, c); + } } - for (const auto &az : rois.azimuthal) - DrawAzimuthalROI(az, palette[color_index++ % palette_size], geom); + for (const auto &az : rois.azimuthal) { + QColor c = palette[color_index++ % palette_size]; + const bool editing = az.GetName() == edit_name_.toStdString() + && (roi_edit_ == RoiEdit::AzimInner || roi_edit_ == RoiEdit::AzimOuter + || roi_edit_ == RoiEdit::RotatePhiMin || roi_edit_ == RoiEdit::RotatePhiMax); + if (editing) { + const ROIAzimuthal edited = edit_has_phi_ + ? ROIAzimuthal(az.GetName(), edit_d_min_, edit_d_max_, edit_phi_min_, edit_phi_max_) + : ROIAzimuthal(az.GetName(), edit_d_min_, edit_d_max_); + DrawAzimuthalROI(edited, c, geom); + } else { + DrawAzimuthalROI(az, c, geom); + } + } } void JFJochDiffractionImage::AddROILabel(const std::string &name, const QColor &color, float px, float py) { @@ -396,7 +457,9 @@ void JFJochDiffractionImage::AddROILabel(const std::string &name, const QColor & void JFJochDiffractionImage::DrawAzimuthalROI(const ROIAzimuthal &az, const QColor &color, const DiffractionGeometry &geom) { - QPen pen(color, 2); pen.setCosmetic(true); + const bool selected = (QString::fromStdString(az.GetName()) == selected_roi_); + QPen pen(color, selected ? 3 : 2); pen.setCosmetic(true); + if (selected) pen.setStyle(Qt::DashLine); QBrush brush = show_roi_fill ? QBrush(QColor(color.red(), color.green(), color.blue(), 60)) : QBrush(Qt::NoBrush); @@ -470,45 +533,137 @@ bool JFJochDiffractionImage::roiEditPress(const QPointF &scenePos) { return false; const auto &rois = image->Dataset().experiment.ROI().GetROIDefinition(); + auto geom = image->Dataset().experiment.GetDiffractionGeometry(); + const qreal tol = 6.0 / std::sqrt(std::max(1e-4, scale_factor)); - // Pick the first box/circle under the cursor, so an ROI can be grabbed by clicking it. + auto start = [&](const std::string &name) { + edit_name_ = QString::fromStdString(name); + selected_roi_ = edit_name_; + move_last_ = scenePos; + emit roiSelected(edit_name_); + setCursor(Qt::ClosedHandCursor); + }; + + // Box: corners/edges resize, interior moves. for (const auto &b : rois.boxes) { const QRectF r(QPointF(b.GetXMin(), b.GetYMin()), QPointF(b.GetXMax(), b.GetYMax())); - if (r.contains(scenePos)) { + const ResizeHandle h = hitTestBoxHandle(r, scenePos, tol); + if (h == ResizeHandle::None) + continue; + start(b.GetName()); + edit_box_ = r; + if (h == ResizeHandle::Inside) { roi_edit_ = RoiEdit::MoveBox; - edit_name_ = QString::fromStdString(b.GetName()); - edit_box_ = r; - move_last_ = scenePos; - selected_roi_ = edit_name_; - emit roiSelected(edit_name_); - setCursor(Qt::ClosedHandCursor); - return true; + } else { + roi_edit_ = RoiEdit::ResizeBox; + box_handle_ = h; } + return true; } + + // Circle: perimeter resizes, interior moves. for (const auto &c : rois.circles) { const QPointF center(c.GetX(), c.GetY()); - if (QLineF(center, scenePos).length() <= c.GetRadius_pxl()) { - roi_edit_ = RoiEdit::MoveCircle; - edit_name_ = QString::fromStdString(c.GetName()); - edit_center_ = center; - edit_radius_ = c.GetRadius_pxl(); - move_last_ = scenePos; - selected_roi_ = edit_name_; - emit roiSelected(edit_name_); - setCursor(Qt::ClosedHandCursor); - return true; - } + const double dist = QLineF(center, scenePos).length(); + if (dist > c.GetRadius_pxl() + tol) + continue; + start(c.GetName()); + edit_center_ = center; + edit_radius_ = c.GetRadius_pxl(); + roi_edit_ = (std::abs(dist - c.GetRadius_pxl()) <= tol) ? RoiEdit::ResizeCircle : RoiEdit::MoveCircle; + return true; } - return false; // azimuthal editing is not yet supported + + // Azimuthal: inner/outer arc resizes Q/d, radial edge rotates phi. + for (const auto &az : rois.azimuthal) { + const auto [bx, by] = geom.GetDirectBeam_pxl(); + const double cursor_r = QLineF(QPointF(bx, by), scenePos).length(); + const double r_inner = geom.ResToPxl(az.GetDMax_A()); // larger d -> smaller radius + const double r_outer = geom.ResToPxl(az.GetDMin_A()); + const float phi = geom.Phi_rad(scenePos.x(), scenePos.y()) * 180.0f / static_cast(PI); + + if (az.HasPhi() && !InPhiSector(phi, az.GetPhiMin_deg(), az.GetPhiMax_deg())) + continue; + if (cursor_r < r_inner - tol || cursor_r > r_outer + tol) + continue; + + start(az.GetName()); + edit_d_min_ = az.GetDMin_A(); + edit_d_max_ = az.GetDMax_A(); + edit_has_phi_ = az.HasPhi(); + edit_phi_min_ = az.GetPhiMin_deg(); + edit_phi_max_ = az.GetPhiMax_deg(); + + if (std::abs(cursor_r - r_inner) <= tol) { + roi_edit_ = RoiEdit::AzimInner; + } else if (std::abs(cursor_r - r_outer) <= tol) { + roi_edit_ = RoiEdit::AzimOuter; + } else if (az.HasPhi()) { + const float tol_deg = (cursor_r > 1.0) ? tol / cursor_r * 180.0f / static_cast(PI) : 180.0f; + if (AngularDistance_deg(phi, az.GetPhiMin_deg()) <= tol_deg) + roi_edit_ = RoiEdit::RotatePhiMin; + else if (AngularDistance_deg(phi, az.GetPhiMax_deg()) <= tol_deg) + roi_edit_ = RoiEdit::RotatePhiMax; + else + roi_edit_ = RoiEdit::None; // interior: select only + } else { + roi_edit_ = RoiEdit::None; + } + return true; + } + return false; } 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; + if (!image) + return; + auto geom = image->Dataset().experiment.GetDiffractionGeometry(); + const auto [bx, by] = geom.GetDirectBeam_pxl(); + const float cursor_r = QLineF(QPointF(bx, by), scenePos).length(); + + switch (roi_edit_) { + case RoiEdit::MoveBox: + edit_box_.translate(scenePos - move_last_); + move_last_ = scenePos; + break; + case RoiEdit::ResizeBox: { + QRectF r = edit_box_; + switch (box_handle_) { + case ResizeHandle::Left: r.setLeft(scenePos.x()); break; + case ResizeHandle::Right: r.setRight(scenePos.x()); break; + case ResizeHandle::Top: r.setTop(scenePos.y()); break; + case ResizeHandle::Bottom: r.setBottom(scenePos.y()); break; + case ResizeHandle::TopLeft: r.setTopLeft(scenePos); break; + case ResizeHandle::TopRight: r.setTopRight(scenePos); break; + case ResizeHandle::BottomLeft: r.setBottomLeft(scenePos); break; + case ResizeHandle::BottomRight: r.setBottomRight(scenePos); break; + default: break; + } + edit_box_ = r.normalized(); + break; + } + case RoiEdit::MoveCircle: + edit_center_ += (scenePos - move_last_); + move_last_ = scenePos; + break; + case RoiEdit::ResizeCircle: + edit_radius_ = std::max(1.0, QLineF(edit_center_, scenePos).length()); + break; + case RoiEdit::AzimInner: + edit_d_max_ = geom.PxlToRes(cursor_r); + break; + case RoiEdit::AzimOuter: + edit_d_min_ = geom.PxlToRes(cursor_r); + break; + case RoiEdit::RotatePhiMin: + edit_phi_min_ = geom.Phi_rad(scenePos.x(), scenePos.y()) * 180.0f / static_cast(PI); + break; + case RoiEdit::RotatePhiMax: + edit_phi_max_ = geom.Phi_rad(scenePos.x(), scenePos.y()) * 180.0f / static_cast(PI); + break; + default: + return; // None: select only, nothing to drag + } updateOverlay(); // Live recompute, but keep at most one in flight (cleared in loadImage) so the @@ -525,7 +680,7 @@ void JFJochDiffractionImage::roiEditRelease() { const ROIDefinition rois = BuildEditedROIDefinition(); roi_edit_ = RoiEdit::None; setCursor(Qt::ArrowCursor); - emit roiGeometryEdited(rois); // final, exact position + emit roiGeometryEdited(rois); // final, exact geometry } ROIDefinition JFJochDiffractionImage::BuildEditedROIDefinition() const { @@ -533,23 +688,61 @@ ROIDefinition JFJochDiffractionImage::BuildEditedROIDefinition() const { if (image) 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; - } + switch (roi_edit_) { + case RoiEdit::MoveBox: + case RoiEdit::ResizeBox: + 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; + } + break; + case RoiEdit::MoveCircle: + case RoiEdit::ResizeCircle: + for (auto &c : rois.circles) + if (c.GetName() == sel) { + c = ROICircle(sel, edit_center_.x(), edit_center_.y(), edit_radius_); + break; + } + break; + case RoiEdit::AzimInner: + case RoiEdit::AzimOuter: + case RoiEdit::RotatePhiMin: + case RoiEdit::RotatePhiMax: + for (auto &a : rois.azimuthal) + if (a.GetName() == sel) { + a = edit_has_phi_ + ? ROIAzimuthal(sel, edit_d_min_, edit_d_max_, edit_phi_min_, edit_phi_max_) + : ROIAzimuthal(sel, edit_d_min_, edit_d_max_); + break; + } + break; + default: + break; } return rois; } +void JFJochDiffractionImage::keyPressEvent(QKeyEvent *event) { + if (event->key() == Qt::Key_Delete && image && !selected_roi_.isEmpty()) { + ROIDefinition rois = image->Dataset().experiment.ROI().GetROIDefinition(); + const std::string sel = selected_roi_.toStdString(); + auto erase = [&sel](auto &vec) { + for (auto it = vec.begin(); it != vec.end(); ++it) + if (it->GetName() == sel) { vec.erase(it); return true; } + return false; + }; + if (erase(rois.boxes) || erase(rois.circles) || erase(rois.azimuthal)) { + selected_roi_.clear(); + emit roiGeometryEdited(rois); + } + event->accept(); + return; + } + QGraphicsView::keyPressEvent(event); +} + void JFJochDiffractionImage::UpdateForeground() { if (!image || !auto_fg) diff --git a/viewer/image_viewer/JFJochDiffractionImage.h b/viewer/image_viewer/JFJochDiffractionImage.h index ab00c8ea..41d9b610 100644 --- a/viewer/image_viewer/JFJochDiffractionImage.h +++ b/viewer/image_viewer/JFJochDiffractionImage.h @@ -37,15 +37,21 @@ private: bool roiEditPress(const QPointF &scenePos) override; void roiEditMove(const QPointF &scenePos) override; void roiEditRelease() override; + void keyPressEvent(QKeyEvent *event) override; // Delete removes the selected ROI [[nodiscard]] ROIDefinition BuildEditedROIDefinition() const; // current ROIs with the edit applied + [[nodiscard]] ResizeHandle hitTestBoxHandle(const QRectF &r, const QPointF &p, qreal tol) const; QString selected_roi_; - enum class RoiEdit { None, MoveBox, MoveCircle }; + enum class RoiEdit { None, MoveBox, ResizeBox, MoveCircle, ResizeCircle, + AzimInner, AzimOuter, RotatePhiMin, RotatePhiMax }; RoiEdit roi_edit_ = RoiEdit::None; + ResizeHandle box_handle_ = ResizeHandle::None; QString edit_name_; QRectF edit_box_; QPointF edit_center_; double edit_radius_ = 0; + float edit_d_min_ = 0, edit_d_max_ = 0, edit_phi_min_ = 0, edit_phi_max_ = 0; // azimuthal edit state + bool edit_has_phi_ = false; QPointF move_last_; bool live_pending_ = false; // a live edit is being recomputed; throttles emissions