From 53d6aa4ddfcdb246d529b91aca8ba652ebee3314 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sun, 13 Sep 2026 20:20:54 +0200 Subject: [PATCH] viewer: ring labels that stay in view, and an inspector that reads at a glance The resolution-ring label was placed at one of four fixed azimuths with its corner on the ring line, in a hardcoded 16 pt Arial, in the ring's own magenta: it clipped at the viewport edge, ignored the font size, and vanished into busy patterns. It now walks the ring's cached trace, centres on the first point where the whole text fits in the viewport (failing that, clamps the nearest visible point inside), keeps a constant on-screen size of 1.2x the application font, and is cased like the spot markers so it reads on any colour map. In the same function visibleRect now maps viewport()->rect(), which is what mapToScene expects. The dataset plot's angle axis snaps its ticks to round angles - multiples of 90 degrees when the sweep is wide enough - instead of whatever angle the evenly-spaced image ticks landed on. The inspector prints the indexed cell as two aligned rows (alpha below a, beta below b, gamma below c), mosaicity with two decimals, and "No lattice" in grey, keeping red for actual trouble. The armed "Analyze image" button stays navy and is marked by a coral outline instead of turning into a differently-coloured button. The dataset navigation keys leave the image's keyPressEvent - the window-wide filter of the previous commit sees them first. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015vRwzF8PLaaa2novAKYorq --- docs/CHANGELOG.md | 6 + viewer/charts/JFJochDatasetInfoChartView.cpp | 55 +++++++-- .../image_viewer/JFJochDiffractionImage.cpp | 114 ++++++++++-------- .../widgets/JFJochViewerImageStatistics.cpp | 33 +++-- viewer/widgets/JFJochViewerSettingsDock.cpp | 8 +- 5 files changed, 138 insertions(+), 78 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 7e6dee1ca..f83a6e968 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,12 @@ * Building Jungfraujoch no longer needs zlib or Eigen installed on the machine: the build downloads and builds both, like every other dependency. * rugnux reports what became of every frame of a rotation sweep - how many were merged, downgraded and rejected, in frames and in degrees - and what keeping each flagged stretch costs the merged intensities (delta-CC1/2); nothing is excluded on the strength of it. * The viewer draws its spot markers over a black outline, so they stay visible on a light colour map and are no longer mistaken for a magenta bad pixel or the coral beam stop. +* The viewer has a background (black-point) slider beside the foreground one; hold B and scroll the wheel to adjust it. +* The viewer's keyboard shortcuts - F, B, A, Home/End/PageUp/PageDown - work whichever panel has the focus, not only with the image focused. +* The viewer's resolution-ring labels follow the font size, are cased like the spot markers so they read on any colour map, and stay whole inside the view instead of clipping at its edge. +* The viewer's dataset plots put the rotation-angle ticks on round angles - multiples of 90 degrees when the sweep is wide enough. +* The viewer's inspector prints the indexed cell as two aligned rows, mosaicity with two decimals, and "No lattice" in grey rather than alarm red. +* The viewer's armed "Analyze image" button is marked by a coral outline, instead of turning into a differently-coloured button. * The viewer has a font size of its own - View -> Font size, or Ctrl+plus and Ctrl+minus - at 100%, 125% or 150% of whatever size the desktop asks for, remembered across restarts. * The viewer's dataset plots label their axes in full, instead of rendering every label as "...". * The viewer's dataset plots draw their curves with a heavier line that grows with the font size. diff --git a/viewer/charts/JFJochDatasetInfoChartView.cpp b/viewer/charts/JFJochDatasetInfoChartView.cpp index db6bcbe44..2aa4296f1 100644 --- a/viewer/charts/JFJochDatasetInfoChartView.cpp +++ b/viewer/charts/JFJochDatasetInfoChartView.cpp @@ -363,20 +363,51 @@ void JFJochDatasetInfoChartView::buildTimeDomainChart() { const double xstep = (tickCountX > 1) ? (xmax - xmin) / (tickCountX - 1) : 0.0; const int64_t lastIdx = static_cast(values.empty() ? 0 : values.size() - 1); - for (int i = 0; i < tickCountX; ++i) { - const double xv = (i == tickCountX - 1) ? xmax : (xmin + i * xstep); - // Map tick position to closest image index - int64_t imgIdx = static_cast(std::llround(xv)); - if (imgIdx < 0) imgIdx = 0; - if (imgIdx > lastIdx) imgIdx = lastIdx; - - double angleDeg = 0.0; - if (lastIdx >= 0) { - angleDeg = goniometer_axis->GetAngle_deg(imgIdx); + // Ticks snap to round angles - multiples of 90° when the sweep is wide enough, + // finer otherwise - instead of whatever angle the evenly-spaced image ticks land + // on (a 90-450° sweep used to label 359.80). The angle is affine in the image + // index, so each round angle maps back to a fractional index position. + QList> snapped; + if (lastIdx > 0) { + const double a0 = goniometer_axis->GetAngle_deg(0); + const double a1 = goniometer_axis->GetAngle_deg(lastIdx); + const double lo = std::min(a0, a1), hi = std::max(a0, a1); + double interval = 0.0; + for (double cand : {90.0, 45.0, 30.0, 15.0, 10.0, 5.0, 2.0, 1.0, 0.5, 0.2, 0.1}) + if (hi - lo >= 2.0 * cand) { interval = cand; break; } + if (interval > 0.0) { + const double per_image = (a1 - a0) / static_cast(lastIdx); + for (double ang = std::ceil(lo / interval) * interval; + ang <= hi + interval * 1e-6; ang += interval) { + const double xv = (ang - a0) / per_image; + if (xv < xmin || xv > xmax) + continue; + snapped.append({xv, QString::number(ang, 'f', interval < 1.0 ? 1 : 0)}); + } + std::sort(snapped.begin(), snapped.end(), + [](const auto &l, const auto &r) { return l.first < r.first; }); } + } - QString lab = QString::number(angleDeg, 'f', 2); - axXcat->append(lab, xv); + if (snapped.size() >= 2) { + for (const auto &t : snapped) + axXcat->append(t.second, t.first); + } else { + for (int i = 0; i < tickCountX; ++i) { + const double xv = (i == tickCountX - 1) ? xmax : (xmin + i * xstep); + // Map tick position to closest image index + int64_t imgIdx = static_cast(std::llround(xv)); + if (imgIdx < 0) imgIdx = 0; + if (imgIdx > lastIdx) imgIdx = lastIdx; + + double angleDeg = 0.0; + if (lastIdx >= 0) { + angleDeg = goniometer_axis->GetAngle_deg(imgIdx); + } + + QString lab = QString::number(angleDeg, 'f', 2); + axXcat->append(lab, xv); + } } chart()->addAxis(axXcat, Qt::AlignBottom); diff --git a/viewer/image_viewer/JFJochDiffractionImage.cpp b/viewer/image_viewer/JFJochDiffractionImage.cpp index 512cb36ad..4285bcfb3 100644 --- a/viewer/image_viewer/JFJochDiffractionImage.cpp +++ b/viewer/image_viewer/JFJochDiffractionImage.cpp @@ -247,8 +247,10 @@ void JFJochDiffractionImage::DrawResolutionRings() { if (ring_mode == RingMode::None) return; - // Get the visible area in the scene coordinates - QRectF visibleRect = mapToScene(viewport()->geometry()).boundingRect(); + // Get the visible area in the scene coordinates (viewport()->rect(), not geometry(): + // mapToScene takes viewport coordinates, and geometry() is offset by the viewport's + // position in its parent). + QRectF visibleRect = mapToScene(viewport()->rect()).boundingRect(); int startX = std::max(0, static_cast(std::floor(visibleRect.left()))); int endX = std::min(static_cast(image->Dataset().experiment.GetXPixelsNum()), @@ -306,7 +308,7 @@ void JFJochDiffractionImage::DrawResolutionRings() { QVector dashPattern = {10, 15}; pen.setDashPattern(dashPattern); - float phi_offset = 0; + int label_stagger = 0; // Tracing the contours costs 361 geometry evaluations per ring, and they only move when the // ring list or the geometry changes - not when the view is panned or zoomed, which is when @@ -360,39 +362,71 @@ void JFJochDiffractionImage::DrawResolutionRings() { for (const auto &[d, path]: ring_cache_) { addOverlayItem(scene()->addPath(path, pen)); - // Place the "d Å" label at the first cardinal azimuth (staggered per ring) that is visible. + // The "d Å" label follows the application font (View -> Font size) and is white cased in + // black, like the spot markers, so it reads on any colour map and on the ring's own + // colour. The item lives in scene coordinates, so dividing by the view scale keeps it a + // constant size on screen - 1.2x the application font at every zoom. It is centred on the + // first sampled azimuth (staggered per ring) where the whole text fits in the viewport; + // failing that, on the visible ring point closest to the viewport centre, clamped inside - + // never half a label off the edge. + QFont font = this->font(); + const qreal f = std::clamp(scale_factor, 0.05, 500.0); + font.setPointSizeF(font.pointSizeF() * 1.2 / f); + + auto *textItem = new QGraphicsSimpleTextItem( + QString("%1 Å").arg(QString::number(d, 'f', 2))); + textItem->setFont(font); + textItem->setBrush(Qt::white); + textItem->setPen(QPen(Qt::black, font.pointSizeF() / 8.0)); + const QSizeF sz = textItem->boundingRect().size(); + + QRectF inner = visibleRect.adjusted(sz.height() * 0.3, sz.height() * 0.3, + -sz.height() * 0.3, -sz.height() * 0.3); + if (!inner.isValid()) + inner = visibleRect; + bool have_label = false; - QPointF label_pos; - for (float base : {0.0f, static_cast(PI) / 2.0f, - static_cast(PI), 3.0f * static_cast(PI) / 2.0f}) { - try { - auto [x, y] = geom.ResPhiToPxl(d, phi_offset + base); - QPointF p(x, y); - if (std::isfinite(x) && std::isfinite(y) && visibleRect.contains(p)) { - label_pos = p; - have_label = true; - break; - } - } catch (...) { + bool fits = false; + QPointF label_center; + double best_dist = std::numeric_limits::max(); + // Walk the ring's own traced points (dense enough that a narrow visible arc is never + // missed), starting at a per-ring stagger so neighbouring rings label different azimuths. + const int n = path.elementCount(); + for (int k = 0; k < n; k++) { + const auto e = path.elementAt((k + label_stagger) % n); + const QPointF p(e.x, e.y); + if (!visibleRect.contains(p)) + continue; + const QRectF r(p - QPointF(sz.width() / 2, sz.height() / 2), sz); + if (inner.contains(r)) { + label_center = p; + have_label = true; + fits = true; break; } + const double dist = QLineF(p, visibleRect.center()).length(); + if (dist < best_dist) { + best_dist = dist; + label_center = p; + have_label = true; + } } if (have_label) { - 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(label_pos); + QRectF r(label_center - QPointF(sz.width() / 2, sz.height() / 2), sz); + if (!fits) { + if (r.left() < inner.left()) r.moveLeft(inner.left()); + if (r.right() > inner.right()) r.moveRight(inner.right()); + if (r.top() < inner.top()) r.moveTop(inner.top()); + if (r.bottom() > inner.bottom()) r.moveBottom(inner.bottom()); + } + textItem->setPos(r.topLeft()); scene()->addItem(textItem); addOverlayItem(textItem); + } else { + delete textItem; } - phi_offset += 4.0 / 180.0 * PI; + label_stagger += 29; } } @@ -841,30 +875,8 @@ void JFJochDiffractionImage::roiScratchDrawn() { } void JFJochDiffractionImage::keyPressEvent(QKeyEvent *event) { - // QGraphicsView would otherwise consume these to scroll the viewport; claim them first so they - // navigate the dataset instead, via the same stepImage() the navigation toolbar already listens - // to. Home/End step by an effectively infinite amount, which stepImage's clamp turns into - // "jump to the first/last image". - switch (event->key()) { - case Qt::Key_Home: - emit stepImage(std::numeric_limits::min()); - event->accept(); - return; - case Qt::Key_End: - emit stepImage(std::numeric_limits::max()); - event->accept(); - return; - case Qt::Key_PageUp: - emit stepImage(1); - event->accept(); - return; - case Qt::Key_PageDown: - emit stepImage(-1); - event->accept(); - return; - default: - break; - } + // Home/End/PageUp/PageDown dataset navigation lives in the main window's application-wide + // event filter, so it works with any panel focused - this handler never sees those keys. if (event->key() == Qt::Key_Delete && image && !selected_roi_.isEmpty()) { ROIDefinition rois = image->Dataset().experiment.ROI().GetROIDefinition(); diff --git a/viewer/widgets/JFJochViewerImageStatistics.cpp b/viewer/widgets/JFJochViewerImageStatistics.cpp index 0b386dc66..1eab6d29a 100644 --- a/viewer/widgets/JFJochViewerImageStatistics.cpp +++ b/viewer/widgets/JFJochViewerImageStatistics.cpp @@ -6,14 +6,23 @@ #include #include -static QString mkUnitCell(const UnitCell &uc) { - return QString("%1 Å %2 Å %3 Å %4° %5° %6°") - .arg(QString::number(uc.a, 'f', 1)) - .arg(QString::number(uc.b, 'f', 1)) - .arg(QString::number(uc.c, 'f', 1)) - .arg(QString::number(uc.alpha, 'f', 1)) - .arg(QString::number(uc.beta, 'f', 1)) - .arg(QString::number(uc.gamma, 'f', 1)); +// Two aligned rows - the lengths over their angles, so that alpha sits below a, beta below b, +// gamma below c. Qt's rich text does not inherit a surrounding colour into a table, so the +// colour is applied per cell. +static QString mkUnitCell(const UnitCell &uc, const char *color = nullptr) { + const QString style = color ? QString(" style=\"color: %1;\"").arg(color) : QString(); + auto num = [&style](double v) { + return QString("%2  ") + .arg(style).arg(QString::number(v, 'f', 1)); + }; + auto unit = [&style](const char *u) { + return QString("%2").arg(style).arg(u); + }; + return "" + + num(uc.a) + num(uc.b) + num(uc.c) + unit("Å") + + "" + + num(uc.alpha) + num(uc.beta) + num(uc.gamma) + unit("°") + + "
"; } static QString mkSourceInstrumentText(const DiffractionExperiment& exp) { @@ -315,7 +324,7 @@ void JFJochViewerImageStatistics::loadImage(std::shared_ptrImageData().profile_radius; auto mos = image->ImageData().mosaicity_deg; if (mos && std::isfinite(mos.value())) { - text = QString("%1°").arg(QString::number(mos.value(), 'f', 6)); + text = QString("%1°").arg(QString::number(mos.value(), 'f', 2)); profile_radius_label->setText("Mosaicity:"); profile_radius->setText(text); if (pr && std::isfinite(pr.value())) { @@ -366,8 +375,7 @@ void JFJochViewerImageStatistics::loadImage(std::shared_ptrImageData().indexing_lattice; if (latt) { - text = QString("%1") - .arg(mkUnitCell(latt->GetUnitCell())); + text = mkUnitCell(latt->GetUnitCell(), "purple"); auto vec0 = latt->Vec0(); auto vec1 = latt->Vec1(); @@ -395,7 +403,8 @@ void JFJochViewerImageStatistics::loadImage(std::shared_ptrDataset().experiment.GetSpaceGroupName())); } } else - text = QString("No lattice"); + // Not indexed is a normal state, not an error - grey, so red keeps meaning trouble. + text = QString("No lattice"); indexed->setToolTip(tooltip); indexed->setText(text); diff --git a/viewer/widgets/JFJochViewerSettingsDock.cpp b/viewer/widgets/JFJochViewerSettingsDock.cpp index 2539599d6..cc1cbeaf6 100644 --- a/viewer/widgets/JFJochViewerSettingsDock.cpp +++ b/viewer/widgets/JFJochViewerSettingsDock.cpp @@ -78,10 +78,12 @@ JFJochViewerSettingsDock::JFJochViewerSettingsDock(const SpotFindingSettings &sp // The two analysis actions sit on top of the panel. "Analyze image" is a toggle (re-analyse the // current frame now and on every change while armed); "Analyze dataset" launches a processing job // whose kind (MX vs azimuthal) is decided by the MX/AzInt toggle below — no separate switch. + // The armed (checked) state stays navy like its sibling - a full coral fill read as a + // different kind of button - and is marked by a coral outline instead. const QString heroStyle = - "QPushButton { background-color:#1F3A5F; color:white; border:none; border-radius:3px;" - " padding:5px 10px; } QPushButton:hover { background-color:#16314f; }" - " QPushButton:checked { background-color:#FA7268; } QPushButton:disabled { background-color:#9aa6b3; }"; + "QPushButton { background-color:#1F3A5F; color:white; border:2px solid transparent;" + " border-radius:3px; padding:3px 8px; } QPushButton:hover { background-color:#16314f; }" + " QPushButton:checked { border-color:#FA7268; } QPushButton:disabled { background-color:#9aa6b3; }"; auto *analyzeImageBtn = new QPushButton(FramesIcon(1), " Analyze image", this); analyzeImageBtn->setCheckable(true); analyzeImageBtn->setStyleSheet(heroStyle);