// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #include #include "JFJochDiffractionImage.h" #include "../../common/DiffractionGeometry.h" #include "../../common/JFJochMath.h" #include "../../common/ROIAzimuthal.h" #include #include #include #include #include #include #include #include #include #include #include #include "JFJochSimpleImage.h" // Constructor 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::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()); if (image && (coord.x() >= 0) && (coord.x() < image->Dataset().experiment.GetXPixelsNum()) && (coord.y() >= 0) && (coord.y() < image->Dataset().experiment.GetYPixelsNum())) { float res = image->Dataset().experiment.GetDiffractionGeometry().PxlToRes(coord.x(), coord.y()); int32_t intensity = image->Image()[std::floor(coord.x()) + std::floor(coord.y()) * image->Dataset().experiment.GetXPixelsNum()]; QString intensity_str = QString("I=%1").arg(intensity, 9); if (intensity == SATURATED_PXL_VALUE) intensity_str = "I=Saturated"; else if (intensity == GAP_PXL_VALUE) intensity_str = " Gap "; else if (intensity == ERROR_PXL_VALUE) intensity_str = " Bad pxl "; emit writeStatusBar(QString("x=%1 y=%2 %3 d=%4 Å") .arg(coord.x(), 0, 'f', 1) .arg(coord.y(), 0, 'f', 1) .arg(intensity_str) .arg(res, 0, 'f', 2)); // Update hovered resolution text without rebuilding the whole overlay hover_resolution = res; DrawResolutionText(); } else { emit writeStatusBar(""); // Clear hover resolution text when outside image if (std::isfinite(hover_resolution)) { hover_resolution = NAN; DrawResolutionText(); } } } void JFJochDiffractionImage::LoadImageInternal() { if (!image) return; W = image->Dataset().experiment.GetXPixelsNum(); H = image->Dataset().experiment.GetYPixelsNum(); image_fp.resize(W*H); auto img = image->Image(); // Fill the QImage with pixel data from the array for (int pxl = 0; pxl < W * H; pxl++) { auto val = img[pxl]; if (val == GAP_PXL_VALUE) image_fp[pxl] = NAN; else if (val == ERROR_PXL_VALUE) image_fp[pxl] = -INFINITY; else if (val == SATURATED_PXL_VALUE) image_fp[pxl] = INFINITY; else image_fp[pxl] = static_cast(val); } } void JFJochDiffractionImage::DrawSpots() { // Compute current visible area in scene coordinates const QRectF visibleRect = mapToScene(viewport()->geometry()).boundingRect(); for (const auto &s: image->ImageData().spots) { // Skip reflections outside the viewport if (!visibleRect.contains(QPointF{s.x, s.y})) continue; const qreal desired_half_px = 8.0; const qreal spot_size = desired_half_px / std::sqrt(std::max(0.0001, scale_factor)); QColor pen_color = spot_color; if (s.indexed) pen_color = feature_color; else if (highlight_ice_rings && s.ice_ring) pen_color = ice_ring_color; QPen pen(pen_color, 3); pen.setCosmetic(true); auto *rect = scene()->addRect(s.x - spot_size + 0.5, s.y - spot_size + 0.5, 2 * spot_size, 2 * spot_size, pen); addOverlayItem(rect); } } void JFJochDiffractionImage::DrawPredictions() { QFont font("Arial", 2); // Font for pixel value text font.setPixelSize(2); // This will render very small text (1-pixel high). const qreal desired_half_px = 8.0; const qreal spot_size = desired_half_px / std::sqrt(std::max(0.0001, scale_factor)); QColor pen_color = prediction_color; QPen pen(pen_color, 3); pen.setCosmetic(true); // Compute current visible area in scene coordinates const QRectF visibleRect = mapToScene(viewport()->geometry()).boundingRect(); for (const auto &s: image->ImageData().reflections) { // Skip reflections outside the viewport if (!visibleRect.contains(QPointF{s.predicted_x, s.predicted_y})) continue; auto *ellipse = scene()->addEllipse(s.predicted_x - spot_size + 0.5f, s.predicted_y - spot_size + 0.5f, 2.0f * spot_size, 2.0f * spot_size, pen); addOverlayItem(ellipse); // When zoomed in enough, draw "h k l" above the box if (scale_factor >= 10.0) { // Format label QString label = QString("%1, %2, %3").arg(s.h).arg(s.k).arg(s.l); // Position slightly above the top side of the box const qreal text_x = s.predicted_x - 5.5f; const qreal text_y = s.predicted_y - 10.0f; // Use QGraphicsSimpleTextItem for much better performance auto *textItem = new QGraphicsSimpleTextItem(label); textItem->setFont(font); textItem->setBrush(pen_color); textItem->setPos(text_x, text_y); scene()->addItem(textItem); addOverlayItem(textItem); } } } void JFJochDiffractionImage::DrawResolutionRings() { if (ring_mode == RingMode::None) return; // Get the visible area in the scene coordinates QRectF visibleRect = mapToScene(viewport()->geometry()).boundingRect(); int startX = std::max(0, static_cast(std::floor(visibleRect.left()))); int endX = std::min(static_cast(image->Dataset().experiment.GetXPixelsNum()), static_cast(std::ceil(visibleRect.right()))); int startY = std::max(0, static_cast(std::floor(visibleRect.top()))); int endY = std::min(static_cast(image->Dataset().experiment.GetYPixelsNum()), static_cast(std::ceil(visibleRect.bottom()))); auto geom = image->Dataset().experiment.GetDiffractionGeometry(); geom.PoniRot3_rad(0.0); QColor ring_color = feature_color; if (ring_mode == RingMode::IceRings) { ring_color = ice_ring_color; res_ring = QVector{ICE_RING_RES_A.begin(), ICE_RING_RES_A.end()}; } else if (ring_mode == RingMode::Auto) { float radius_x_0 = geom.GetBeamX_pxl() - startX; float radius_x_1 = endX - geom.GetBeamX_pxl(); float radius_x = std::max(radius_x_0, radius_x_1); float radius_y_0 = geom.GetBeamY_pxl() - startY; float radius_y_1 = endY - geom.GetBeamY_pxl(); float radius_y = std::max(radius_y_0, radius_y_1); float radius = std::min(radius_x, radius_y); if (radius_x <= 0) radius = radius_y; if (radius_y <= 0) radius = radius_x; if (radius > 0) res_ring = { geom.PxlToRes(radius / 2.0f), geom.PxlToRes(radius / 1.02f) }; else res_ring = {}; } else if (ring_mode == RingMode::Estimation) { if (image && image->ImageData().resolution_estimate && std::isfinite(image->ImageData().resolution_estimate.value()) && image->ImageData().resolution_estimate.value() > 0.0) res_ring = {*image->ImageData().resolution_estimate}; else res_ring = {}; } if (res_ring.empty()) return; QPen pen(ring_color, 5); pen.setCosmetic(true); QVector dashPattern = {10, 15}; pen.setDashPattern(dashPattern); float phi_offset = 0; float res1 = geom.PxlToRes(0,0); float res2 = geom.PxlToRes(image->Dataset().experiment.GetXPixelsNum(),0); float res3 = geom.PxlToRes(image->Dataset().experiment.GetXPixelsNum(),image->Dataset().experiment.GetYPixelsNum()); float res4 = geom.PxlToRes(0,image->Dataset().experiment.GetYPixelsNum()); float min_res = std::min({res1, res2, res3, res4}); for (const auto &d: res_ring) { if (d < min_res) continue; auto [x1,y1] = geom.ResPhiToPxl(d, 0); auto [x2,y2] = geom.ResPhiToPxl(d, PI / 2); auto [x3,y3] = geom.ResPhiToPxl(d, PI); auto [x4,y4] = geom.ResPhiToPxl(d, 3.0 * PI / 2); auto x_min = std::min({x1, x2, x3, x4}); auto x_max = std::max({x1, x2, x3, x4}); auto y_min = std::min({y1, y2, y3, y4}); auto y_max = std::max({y1, y2, y3, y4}); QRectF boundingRect(x_min, y_min, x_max - x_min, y_max - y_min); addOverlayItem(scene()->addEllipse(boundingRect, pen)); auto [x5,y5] = geom.ResPhiToPxl(d, phi_offset + 0); auto [x6,y6] = geom.ResPhiToPxl(d, phi_offset + PI / 2); auto [x7,y7] = geom.ResPhiToPxl(d, phi_offset + PI); auto [x8,y8] = geom.ResPhiToPxl(d, phi_offset + 3.0 * PI / 2); QPointF point_1(x5, y5); QPointF point_2(x6, y6); QPointF point_3(x7, y7); QPointF point_4(x8, y8); std::optional point; if (visibleRect.contains(point_1)) point = point_1; else if (visibleRect.contains(point_2)) point = point_2; else if (visibleRect.contains(point_3)) point = point_3; else if (visibleRect.contains(point_4)) point = point_4; if (point) { QFont font("Arial", 16); const qreal f = std::clamp(scale_factor, 0.5, 50.0); font.setPointSizeF(16.0 / sqrt(f)); // base 12pt around scale_factor ~10 auto *textItem = new QGraphicsSimpleTextItem( QString("%1 Å").arg(QString::number(d, 'f', 2))); textItem->setFont(font); textItem->setBrush(ring_color); textItem->setPos(point.value()); scene()->addItem(textItem); addOverlayItem(textItem); } phi_offset += 4.0 / 180.0 * PI; } } void JFJochDiffractionImage::DrawBeamCenter() { auto geom = image->Dataset().experiment.GetDiffractionGeometry(); auto [beam_x, beam_y] = geom.GetDirectBeam_pxl(); DrawCross(beam_x, beam_y, 25, 5, 2); } void JFJochDiffractionImage::DrawTopPixels() { int i = 0; for (const auto& p : image->GetTopPixels()) { if (i >= show_highest_pixels) break; const int32_t idx = p.second; DrawCross(idx % image->Dataset().experiment.GetXPixelsNum() + 0.5, idx / image->Dataset().experiment.GetXPixelsNum() + 0.5, 15, 3); i++; } } void JFJochDiffractionImage::addCustomOverlay() { DrawResolutionRings(); DrawROIs(); DrawTopPixels(); DrawBeamCenter(); if (show_spots) DrawSpots(); if (show_predictions) DrawPredictions(); if (show_saturation) DrawSaturation(); DrawResolutionText(); } void JFJochDiffractionImage::DrawROIs() { if (!image) return; const auto &rois = image->Dataset().experiment.ROI().GetROIDefinition(); auto geom = image->Dataset().experiment.GetDiffractionGeometry(); // Distinct colours per ROI; loaded ROIs use solid lines (the interactively // drawn scratch ROI keeps its dashed feature_color). // TODO: align this palette with the ROI colours in the bottom-panel plots. static const QColor palette[] = {Qt::cyan, Qt::yellow, QColor(0xff, 0x57, 0x22), Qt::green, Qt::magenta, QColor(0x21, 0x96, 0xf3)}; const int palette_size = sizeof(palette) / sizeof(palette[0]); int color_index = 0; 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 = 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); 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_committed : rois.azimuthal) { QColor c = palette[color_index++ % palette_size]; 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); 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); } } } } void JFJochDiffractionImage::AddROILabel(const std::string &name, const QColor &color, float px, float py) { if (!show_roi_labels) return; // Just the name; per-ROI statistics are shown in the side-panel ROI list. auto *text = scene()->addText(QString::fromStdString(name)); text->setDefaultTextColor(color); text->setFlag(QGraphicsItem::ItemIgnoresTransformations); // constant on-screen size text->setPos(px, py); addOverlayItem(text); } void JFJochDiffractionImage::DrawAzimuthalROI(const ROIAzimuthal &az, const QColor &color, const DiffractionGeometry &geom) { 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); const float d_inner = az.GetDMax_A(); // larger d -> smaller radius const float d_outer = az.GetDMin_A(); auto deg2rad = [](float d) { return d * static_cast(PI) / 180.0f; }; // Sample the boundary through the geometry so the wedge matches the ROI footprint. // ResPhiToPxl throws when the resolution is too high for the wavelength; skip such ROIs. // move_to_start == true begins a new subpath (no connecting line); false continues // the current one (used for the radial edge between a sector's outer and inner arc). auto add_arc = [&](QPainterPath &path, float d, float phi_a, float phi_b, int steps, bool move_to_start) -> bool { for (int i = 0; i <= steps; i++) { float phi = phi_a + (phi_b - phi_a) * static_cast(i) / static_cast(steps); try { auto [px, py] = geom.ResPhiToPxl(d, phi); if (move_to_start && i == 0) path.moveTo(px, py); else path.lineTo(px, py); } catch (...) { return false; } } return true; }; QPainterPath path; if (az.HasPhi()) { float phi0 = deg2rad(az.GetPhiMin_deg()); float phi1 = deg2rad(az.GetPhiMax_deg()); if (phi1 < phi0) phi1 += 2.0f * static_cast(PI); // unwrap the sector int steps = std::max(8, static_cast((phi1 - phi0) * 180.0f / static_cast(PI) / 2.0f)); if (!add_arc(path, d_outer, phi0, phi1, steps, true)) return; // outer arc if (!add_arc(path, d_inner, phi1, phi0, steps, false)) return; // inner arc; radial edges close it path.closeSubpath(); } else { path.setFillRule(Qt::OddEvenFill); // annulus: two concentric rings const float two_pi = 2.0f * static_cast(PI); if (!add_arc(path, d_outer, 0, two_pi, 180, true)) return; path.closeSubpath(); if (!add_arc(path, d_inner, 0, two_pi, 180, true)) return; path.closeSubpath(); } addOverlayItem(scene()->addPath(path, pen, brush)); if (show_roi_labels) { try { auto [px, py] = geom.ResPhiToPxl(d_outer, az.HasPhi() ? deg2rad(az.GetPhiMin_deg()) : 0.0f); AddROILabel(az.GetName(), color, px, py); } catch (...) {} } } void JFJochDiffractionImage::showROILabels(bool input) { show_roi_labels = input; updateOverlay(); } void JFJochDiffractionImage::showROIFill(bool input) { show_roi_fill = input; updateOverlay(); } void JFJochDiffractionImage::setSelectedROI(QString name) { selected_roi_ = name; updateOverlay(); } bool JFJochDiffractionImage::roiEditPress(const QPointF &scenePos) { if (!image) 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)); 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())); 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; } 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()); 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; } // 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) { QPointF inner, outer, pmin, pmax; azimuthalHandles(az, geom, inner, outer, pmin, pmax); auto grab = [&](const QPointF &h) { return QLineF(h, scenePos).length() <= tol_h; }; 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()); 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(); 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; } void JFJochDiffractionImage::roiEditMove(const QPointF &scenePos) { 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 // worker is not flooded with edits faster than it can recompute them. if (!live_pending_) { live_pending_ = true; emit roiGeometryEdited(BuildEditedROIDefinition()); } } void JFJochDiffractionImage::roiEditRelease() { if (roi_edit_ == RoiEdit::None) return; const ROIDefinition rois = BuildEditedROIDefinition(); roi_edit_ = RoiEdit::None; setCursor(Qt::ArrowCursor); emit roiGeometryEdited(rois); // final, exact geometry } ROIDefinition JFJochDiffractionImage::BuildEditedROIDefinition() const { ROIDefinition rois; if (image) rois = image->Dataset().experiment.ROI().GetROIDefinition(); const std::string sel = edit_name_.toStdString(); 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::roiScratchDrawn() { // The base just drew a scratch box/circle (roiBox/roi_type, in pixel coords); // turn it into a new persistent ROI in the list. if (!image || roiBox.isNull() || roiBox.width() <= 0 || roiBox.height() <= 0) return; ROIDefinition rois = image->Dataset().experiment.ROI().GetROIDefinition(); if (rois.boxes.size() + rois.circles.size() + rois.azimuthal.size() >= 16) return; std::set used; for (const auto &b : rois.boxes) used.insert(b.GetName()); for (const auto &c : rois.circles) used.insert(c.GetName()); for (const auto &a : rois.azimuthal) used.insert(a.GetName()); std::string name; for (int i = 1; ; i++) { name = "roi" + std::to_string(i); if (!used.count(name)) break; } if (roi_type == RoiType::RoiBox) { const QRectF r = roiBox.normalized(); rois.boxes.emplace_back(name, std::lround(r.left()), std::lround(r.right()), std::lround(r.top()), std::lround(r.bottom())); } else { const QPointF c = roiBox.center(); const double rad = 0.5 * std::min(roiBox.width(), roiBox.height()); rois.circles.emplace_back(name, c.x(), c.y(), std::max(0.1, rad)); } roiBox = QRectF(); // clear the scratch overlay selected_roi_ = QString::fromStdString(name); emit roiSelected(selected_roi_); emit roiGeometryEdited(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) return; if (hdr_mode) { const auto val_range = image->ValidMinMax(); if (val_range.has_value()) foreground = val_range->second; } else { foreground = image->GetAutoContrastValue(); } emit foregroundChanged(foreground); } void JFJochDiffractionImage::setHDRMode(bool input) { hdr_mode = input; UpdateForeground(); GeneratePixmap(); Redraw(); } void JFJochDiffractionImage::loadImage(std::shared_ptr in_image) { live_pending_ = false; // a live ROI edit (if any) has now been recomputed if (in_image) { image = in_image; UpdateForeground(); LoadImageInternal(); GeneratePixmap(); Redraw(); CalcROI(); } else { image.reset(); W = 0; H = 0; if (scene()) scene()->clear(); resetScenePointers(); hover_resolution = NAN; hover_resolution_item = nullptr; CalcROI(); } } void JFJochDiffractionImage::setAutoForeground(bool input) { auto_fg = input; // If auto_foreground is not set, then view stays with the current settings till these are explicitly changed UpdateForeground(); GeneratePixmap(); Redraw(); emit autoForegroundChanged(auto_fg); } void JFJochDiffractionImage::setResolutionRing(QVector v) { res_ring = v; ring_mode = RingMode::Manual; updateOverlay(); } void JFJochDiffractionImage::showSpots(bool input) { show_spots = input; updateOverlay(); } void JFJochDiffractionImage::showPredictions(bool input) { show_predictions = input; updateOverlay(); } void JFJochDiffractionImage::setSpotColor(QColor input) { spot_color = input; updateOverlay(); } void JFJochDiffractionImage::setPredictionColor(QColor input) { prediction_color = input; updateOverlay(); } void JFJochDiffractionImage::showHighestPixels(int32_t v) { show_highest_pixels = v; updateOverlay(); } void JFJochDiffractionImage::DrawSaturation() { for (const auto &iter: image->SaturatedPixels()) DrawCross(iter % image->Dataset().experiment.GetXPixelsNum() + 0.5, iter / image->Dataset().experiment.GetXPixelsNum() + 0.5, 20, 4); } void JFJochDiffractionImage::DrawCross(float x, float y, float size, float width, float z) { float sc_size = size / sqrt(scale_factor); QPen pen(feature_color, width); pen.setCosmetic(true); QGraphicsLineItem *horizontalLine = scene()->addLine(x - sc_size, y, x + sc_size, y, pen); QGraphicsLineItem *verticalLine = scene()->addLine(x, y - sc_size, x, y + sc_size, pen); horizontalLine->setZValue(z); // Ensure it appears above other items verticalLine->setZValue(z); // Ensure it appears above other items addOverlayItem(horizontalLine); addOverlayItem(verticalLine); } void JFJochDiffractionImage::showSaturation(bool input) { show_saturation = input; GeneratePixmap(); updateOverlay(); } void JFJochDiffractionImage::highlightIceRings(bool input) { highlight_ice_rings = input; updateOverlay(); } void JFJochDiffractionImage::setResolutionRingMode(RingMode mode) { ring_mode = mode; updateOverlay(); } void JFJochDiffractionImage::DrawResolutionText() { auto scn = scene(); if (!scn) { hover_resolution_item = nullptr; // scene gone return; } // Hide item if no valid hover resolution if (!image || !std::isfinite(hover_resolution) || hover_resolution <= 0.0f) { if (hover_resolution_item) hover_resolution_item->setVisible(false); return; } const QRectF visibleRect = mapToScene(viewport()->geometry()).boundingRect(); // Fixed on-screen font size (no dependence on scale_factor) QFont font("Arial"); font.setPixelSize(32); // big, constant size on screen const QString label = QString("d = %1 Å").arg(QString::number(hover_resolution, 'f', 2)); // Create the item if it does not exist yet; otherwise reuse it // NOTE: hover_resolution_item is NOT tracked in overlay_items_ — it is persistent if (!hover_resolution_item) { hover_resolution_item = scn->addText(label, font); hover_resolution_item->setZValue(10.0); // Make the text ignore zooming / view transforms hover_resolution_item->setFlag(QGraphicsItem::ItemIgnoresTransformations, true); } else { hover_resolution_item->setFont(font); hover_resolution_item->setPlainText(label); } hover_resolution_item->setDefaultTextColor(feature_color); // Keep a roughly constant ~10 px margin by compensating with scale_factor const qreal margin_px = 10.0; const qreal margin_scene = margin_px / std::max(0.0001, scale_factor); QPointF topLeft(visibleRect.left() + margin_scene, visibleRect.top() + margin_scene); hover_resolution_item->setPos(topLeft); hover_resolution_item->setVisible(true); } void JFJochDiffractionImage::beforeOverlayCleared() { // hover_resolution_item is NOT in overlay_items_, so the selective clear won't touch it. // However, if scene()->clear() is ever called (e.g. on loadImage(nullptr)), // the caller must also set hover_resolution_item = nullptr separately. } void JFJochDiffractionImage::leaveEvent(QEvent *event) { // Mouse left the view: clear hover resolution and hide text if (std::isfinite(hover_resolution)) { hover_resolution = NAN; DrawResolutionText(); } JFJochImage::leaveEvent(event); }