diff --git a/viewer/image_viewer/JFJochDiffractionImage.cpp b/viewer/image_viewer/JFJochDiffractionImage.cpp index 62cd60a2..85917787 100644 --- a/viewer/image_viewer/JFJochDiffractionImage.cpp +++ b/viewer/image_viewer/JFJochDiffractionImage.cpp @@ -21,11 +21,6 @@ #include "JFJochSimpleImage.h" // Constructor -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; @@ -54,6 +49,31 @@ JFJochImage::ResizeHandle JFJochDiffractionImage::hitTestBoxHandle(const QRectF return ResizeHandle::None; } +void JFJochDiffractionImage::azimuthalHandles(const ROIAzimuthal &az, const DiffractionGeometry &geom, + QPointF &inner, QPointF &outer, QPointF &phimin, QPointF &phimax) const { + const float d2r = static_cast(PI) / 180.0f; + const float phi0 = az.GetPhiMin_deg(); + const float phi1 = az.GetPhiMax_deg(); + const float mid_phi = az.HasPhi() + ? (phi1 >= phi0 ? (phi0 + phi1) / 2.0f : std::fmod((phi0 + phi1 + 360.0f) / 2.0f, 360.0f)) + : 0.0f; + const float r_inner = geom.ResToPxl(az.GetDMax_A()); + const float r_outer = geom.ResToPxl(az.GetDMin_A()); + const float d_mid = geom.PxlToRes((r_inner + r_outer) / 2.0f); + auto pt = [&](float d, float phi_deg) -> QPointF { + try { + auto [x, y] = geom.ResPhiToPxl(d, phi_deg * d2r); + return QPointF(x, y); + } catch (...) { + return QPointF(-1e9, -1e9); // off-image: never matches a handle hit-test + } + }; + inner = pt(az.GetDMax_A(), mid_phi); + outer = pt(az.GetDMin_A(), mid_phi); + phimin = pt(d_mid, phi0); + phimax = pt(d_mid, phi1); +} + void JFJochDiffractionImage::mouseHover(QMouseEvent *event) { auto coord = mapToScene(event->pos()); @@ -427,18 +447,26 @@ void JFJochDiffractionImage::DrawROIs() { } } - for (const auto &az : rois.azimuthal) { + for (const auto &az_committed : rois.azimuthal) { QColor c = palette[color_index++ % palette_size]; - const bool editing = az.GetName() == edit_name_.toStdString() + const bool selected = (QString::fromStdString(az_committed.GetName()) == selected_roi_); + const bool editing = az_committed.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); + const ROIAzimuthal az = editing + ? (edit_has_phi_ ? ROIAzimuthal(az_committed.GetName(), edit_d_min_, edit_d_max_, edit_phi_min_, edit_phi_max_) + : ROIAzimuthal(az_committed.GetName(), edit_d_min_, edit_d_max_)) + : az_committed; + DrawAzimuthalROI(az, c, geom); + if (selected) { + QPointF inner, outer, pmin, pmax; + azimuthalHandles(az, geom, inner, outer, pmin, pmax); + draw_handle(inner, c); + draw_handle(outer, c); + if (az.HasPhi()) { + draw_handle(pmin, c); + draw_handle(pmax, c); + } } } } @@ -574,17 +602,20 @@ bool JFJochDiffractionImage::roiEditPress(const QPointF &scenePos) { return true; } - // Azimuthal: inner/outer arc resizes Q/d, radial edge rotates phi. + // Azimuthal: grab one of the discrete handles to resize Q/d (inner/outer arc) or + // rotate a phi edge. Larger tolerance than the thin arcs would give. + const qreal tol_h = 9.0 / std::sqrt(std::max(1e-4, scale_factor)); 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); + QPointF inner, outer, pmin, pmax; + azimuthalHandles(az, geom, inner, outer, pmin, pmax); + auto grab = [&](const QPointF &h) { return QLineF(h, scenePos).length() <= tol_h; }; - if (az.HasPhi() && !InPhiSector(phi, az.GetPhiMin_deg(), az.GetPhiMax_deg())) - continue; - if (cursor_r < r_inner - tol || cursor_r > r_outer + tol) + RoiEdit mode = RoiEdit::None; + if (grab(inner)) mode = RoiEdit::AzimInner; + else if (grab(outer)) mode = RoiEdit::AzimOuter; + else if (az.HasPhi() && grab(pmin)) mode = RoiEdit::RotatePhiMin; + else if (az.HasPhi() && grab(pmax)) mode = RoiEdit::RotatePhiMax; + if (mode == RoiEdit::None) continue; start(az.GetName()); @@ -593,24 +624,24 @@ bool JFJochDiffractionImage::roiEditPress(const QPointF &scenePos) { 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; - } + roi_edit_ = mode; return true; } + + // Inside an azimuthal ROI but not on a handle: select it and let the base pan + // (these ROIs are large and should not capture the panning gesture). + for (const auto &az : rois.azimuthal) { + const auto [bx, by] = geom.GetDirectBeam_pxl(); + const double cursor_r = QLineF(QPointF(bx, by), scenePos).length(); + const float phi = geom.Phi_rad(scenePos.x(), scenePos.y()) * 180.0f / static_cast(PI); + if (cursor_r < geom.ResToPxl(az.GetDMax_A()) || cursor_r > geom.ResToPxl(az.GetDMin_A())) + continue; + if (az.HasPhi() && !InPhiSector(phi, az.GetPhiMin_deg(), az.GetPhiMax_deg())) + continue; + selected_roi_ = QString::fromStdString(az.GetName()); + emit roiSelected(selected_roi_); + break; + } return false; } diff --git a/viewer/image_viewer/JFJochDiffractionImage.h b/viewer/image_viewer/JFJochDiffractionImage.h index 41d9b610..d9e6dd04 100644 --- a/viewer/image_viewer/JFJochDiffractionImage.h +++ b/viewer/image_viewer/JFJochDiffractionImage.h @@ -40,6 +40,10 @@ private: 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; + // Grab points for an azimuthal ROI: inner/outer arc (resize Q/d) and the two phi + // edges (rotate). The phi handles are only meaningful for a sector (HasPhi()). + void azimuthalHandles(const ROIAzimuthal &az, const DiffractionGeometry &geom, + QPointF &inner, QPointF &outer, QPointF &phimin, QPointF &phimax) const; QString selected_roi_; enum class RoiEdit { None, MoveBox, ResizeBox, MoveCircle, ResizeCircle,