viewer: azimuthal ROI handles, and keep panning inside the ROI
Azimuthal ROIs now show discrete grab handles on the selected ROI: the inner/outer arc (resize Q/d) and, for a sector, the two phi edges (rotate). Editing is by clicking a handle, with a generous tolerance, which fixes the previously near-impossible phi-edge grab. Clicking the (large) interior of an azimuthal ROI now only selects it and lets the view pan, instead of capturing the gesture. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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<float>(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<float>(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<float>(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<float>(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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user