75de40f52b
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 7m27s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 8m20s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 7m35s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 5m59s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 7m25s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 8m30s
Build Packages / build:rpm (rocky8) (push) Successful in 7m39s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 8m16s
Build Packages / build:rpm (rocky9) (push) Successful in 9m35s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 8m6s
Build Packages / Generate python client (push) Successful in 12s
Build Packages / Build documentation (push) Successful in 31s
Build Packages / Create release (push) Skipped
Build Packages / XDS test (durin plugin) (push) Successful in 7m6s
Build Packages / DIALS test (push) Successful in 12m3s
Build Packages / XDS test (neggia plugin) (push) Successful in 5m11s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 5m50s
Build Packages / Unit tests (push) Successful in 57m33s
This is an UNSTABLE release. The release has significant modifications for data processing - in case of troubles go back to 1.0.0-rc.144. * jfjoch_viewer: Add reciprocal space viewer * jfjoch_process: Two pass algorithm that does spot finding/indexing + integration of full dataset * jfjoch_process: Improve logic for rotation indexer, to make execution more deterministic (still work in progress) Reviewed-on: #57 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch> Co-committed-by: Filip Leonarski <filip.leonarski@psi.ch>
779 lines
24 KiB
C++
779 lines
24 KiB
C++
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include "JFJochViewerReciprocalSpaceWindow.h"
|
|
|
|
#include <QVBoxLayout>
|
|
#include <QHBoxLayout>
|
|
#include <QSurfaceFormat>
|
|
#include <QPushButton>
|
|
#include <QLabel>
|
|
#include <QtMath>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <limits>
|
|
|
|
#include "../../reader/JFJochReaderSpots.h"
|
|
|
|
// ============================================================================
|
|
// Shaders
|
|
// ============================================================================
|
|
|
|
// One shader handles both spots (GL_POINTS) and lines (GL_LINES).
|
|
// Per-vertex color comes straight from the VBO; MVP is a single uniform.
|
|
|
|
static const char *kVertSrc = R"(
|
|
#version 330 core
|
|
layout(location = 0) in vec3 aPos;
|
|
layout(location = 1) in vec4 aColor;
|
|
out vec4 vColor;
|
|
uniform mat4 uMVP;
|
|
void main() {
|
|
vColor = aColor;
|
|
gl_Position = uMVP * vec4(aPos, 1.0);
|
|
gl_PointSize = 6.0;
|
|
}
|
|
)";
|
|
|
|
static const char *kFragSrc = R"(
|
|
#version 330 core
|
|
in vec4 vColor;
|
|
out vec4 fragColor;
|
|
uniform bool uPointShape;
|
|
void main() {
|
|
if (uPointShape) {
|
|
// Discard corners to draw a circle instead of a square point.
|
|
vec2 c = gl_PointCoord - vec2(0.5);
|
|
if (dot(c, c) > 0.25) discard;
|
|
}
|
|
fragColor = vColor;
|
|
}
|
|
)";
|
|
|
|
// ============================================================================
|
|
// ReciprocalSpaceGLView
|
|
// ============================================================================
|
|
|
|
ReciprocalSpaceGLView::ReciprocalSpaceGLView(QWidget *parent)
|
|
: QOpenGLWidget(parent)
|
|
, spotsVBO_(QOpenGLBuffer::VertexBuffer)
|
|
, linesVBO_(QOpenGLBuffer::VertexBuffer)
|
|
{
|
|
// Request OpenGL 3.3 Core Profile.
|
|
QSurfaceFormat fmt;
|
|
fmt.setVersion(3, 3);
|
|
fmt.setProfile(QSurfaceFormat::CoreProfile);
|
|
fmt.setDepthBufferSize(24);
|
|
setFormat(fmt);
|
|
setFocusPolicy(Qt::StrongFocus);
|
|
}
|
|
|
|
ReciprocalSpaceGLView::~ReciprocalSpaceGLView() {
|
|
makeCurrent();
|
|
spotsVAO_.destroy();
|
|
spotsVBO_.destroy();
|
|
linesVAO_.destroy();
|
|
linesVBO_.destroy();
|
|
doneCurrent();
|
|
}
|
|
|
|
void ReciprocalSpaceGLView::initializeGL() {
|
|
initializeOpenGLFunctions();
|
|
|
|
glClearColor(20.f / 255.f, 20.f / 255.f, 25.f / 255.f, 1.0f);
|
|
glEnable(GL_DEPTH_TEST);
|
|
glEnable(GL_PROGRAM_POINT_SIZE);
|
|
glEnable(GL_BLEND);
|
|
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
|
|
|
shader_.addShaderFromSourceCode(QOpenGLShader::Vertex, kVertSrc);
|
|
shader_.addShaderFromSourceCode(QOpenGLShader::Fragment, kFragSrc);
|
|
shader_.link();
|
|
|
|
// Create empty VAO/VBOs so they are valid before the first data arrives.
|
|
setupVAO(spotsVAO_, spotsVBO_);
|
|
setupVAO(linesVAO_, linesVBO_);
|
|
|
|
glReady_ = true;
|
|
|
|
uploadBuffer(spotsVBO_, spotsVAO_, pendingSpots_);
|
|
spotsCount_ = static_cast<int>(pendingSpots_.size());
|
|
|
|
uploadBuffer(linesVBO_, linesVAO_, pendingLines_);
|
|
linesCount_ = static_cast<int>(pendingLines_.size());
|
|
}
|
|
|
|
void ReciprocalSpaceGLView::resizeGL(int w, int h) {
|
|
const float aspect = float(w) / float(h ? h : 1);
|
|
const float halfHeight = zoom_;
|
|
const float halfWidth = halfHeight * aspect;
|
|
|
|
proj_.setToIdentity();
|
|
proj_.ortho(-halfWidth, halfWidth,
|
|
-halfHeight, halfHeight,
|
|
-10000.0f, 10000.0f);
|
|
}
|
|
|
|
QMatrix4x4 ReciprocalSpaceGLView::currentViewMatrix() const {
|
|
QMatrix4x4 view;
|
|
view.translate(0, 0, -zoom_);
|
|
view.rotate(pitch_, 1, 0, 0);
|
|
view.rotate(yaw_, 0, 1, 0);
|
|
view.translate(-target_);
|
|
return view;
|
|
}
|
|
|
|
QMatrix4x4 ReciprocalSpaceGLView::currentMvpMatrix() const {
|
|
return proj_ * currentViewMatrix();
|
|
}
|
|
|
|
void ReciprocalSpaceGLView::paintGL() {
|
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
|
|
|
const QMatrix4x4 mvp = currentMvpMatrix();
|
|
|
|
shader_.bind();
|
|
shader_.setUniformValue("uMVP", mvp);
|
|
|
|
// Draw axes + cell vectors.
|
|
// Convention: first 6 line vertices are XYZ reference axes,
|
|
// remaining line vertices are reciprocal-cell vectors.
|
|
if (linesCount_ > 0) {
|
|
shader_.setUniformValue("uPointShape", false);
|
|
linesVAO_.bind();
|
|
|
|
const int axesVertexCount = std::min(linesCount_, 6);
|
|
if (axesVertexCount > 0) {
|
|
glLineWidth(1.0f);
|
|
glDrawArrays(GL_LINES, 0, axesVertexCount);
|
|
}
|
|
|
|
if (linesCount_ > axesVertexCount) {
|
|
// Note: on some OpenGL core-profile drivers wide lines may be clamped to 1 px.
|
|
glLineWidth(3.0f);
|
|
glDrawArrays(GL_LINES, axesVertexCount, linesCount_ - axesVertexCount);
|
|
glLineWidth(1.0f);
|
|
}
|
|
|
|
linesVAO_.release();
|
|
}
|
|
|
|
// Draw observed spots + predicted nodes + explicit 000 marker.
|
|
if (spotsCount_ > 0) {
|
|
shader_.setUniformValue("uPointShape", true);
|
|
spotsVAO_.bind();
|
|
glDrawArrays(GL_POINTS, 0, spotsCount_);
|
|
spotsVAO_.release();
|
|
}
|
|
|
|
shader_.release();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void ReciprocalSpaceGLView::setSpots(const std::vector<Vertex> &spots) {
|
|
pendingSpots_ = spots;
|
|
spotsCount_ = static_cast<int>(pendingSpots_.size());
|
|
|
|
if (glReady_) {
|
|
makeCurrent();
|
|
uploadBuffer(spotsVBO_, spotsVAO_, pendingSpots_);
|
|
doneCurrent();
|
|
}
|
|
|
|
update();
|
|
}
|
|
|
|
void ReciprocalSpaceGLView::setLines(const std::vector<Vertex> &lines) {
|
|
pendingLines_ = lines;
|
|
linesCount_ = static_cast<int>(pendingLines_.size());
|
|
|
|
if (glReady_) {
|
|
makeCurrent();
|
|
uploadBuffer(linesVBO_, linesVAO_, pendingLines_);
|
|
doneCurrent();
|
|
}
|
|
|
|
update();
|
|
}
|
|
|
|
void ReciprocalSpaceGLView::resetTarget() {
|
|
target_ = QVector3D(0.0f, 0.0f, 0.0f);
|
|
update();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void ReciprocalSpaceGLView::setupVAO(QOpenGLVertexArrayObject &vao,
|
|
QOpenGLBuffer &vbo) {
|
|
vao.create();
|
|
vao.bind();
|
|
|
|
vbo.create();
|
|
vbo.bind();
|
|
|
|
// location 0: position xyz
|
|
glEnableVertexAttribArray(0);
|
|
glVertexAttribPointer(0,
|
|
3,
|
|
GL_FLOAT,
|
|
GL_FALSE,
|
|
sizeof(Vertex),
|
|
reinterpret_cast<void *>(offsetof(Vertex, x)));
|
|
|
|
// location 1: color rgba
|
|
glEnableVertexAttribArray(1);
|
|
glVertexAttribPointer(1,
|
|
4,
|
|
GL_FLOAT,
|
|
GL_FALSE,
|
|
sizeof(Vertex),
|
|
reinterpret_cast<void *>(offsetof(Vertex, r)));
|
|
|
|
vbo.release();
|
|
vao.release();
|
|
}
|
|
|
|
void ReciprocalSpaceGLView::uploadBuffer(QOpenGLBuffer &vbo,
|
|
QOpenGLVertexArrayObject &vao,
|
|
const std::vector<Vertex> &data) {
|
|
vao.bind();
|
|
vbo.bind();
|
|
|
|
if (data.empty()) {
|
|
vbo.allocate(nullptr, 0);
|
|
} else {
|
|
vbo.allocate(data.data(),
|
|
static_cast<int>(data.size() * sizeof(Vertex)));
|
|
}
|
|
|
|
vbo.release();
|
|
vao.release();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mouse / wheel -> orbit camera + picking
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void ReciprocalSpaceGLView::mousePressEvent(QMouseEvent *e) {
|
|
lastMousePos_ = e->pos();
|
|
}
|
|
|
|
void ReciprocalSpaceGLView::mouseDoubleClickEvent(QMouseEvent *e) {
|
|
if (e->button() == Qt::LeftButton && emitNearestSpot(e->pos())) {
|
|
e->accept();
|
|
return;
|
|
}
|
|
|
|
QOpenGLWidget::mouseDoubleClickEvent(e);
|
|
}
|
|
|
|
void ReciprocalSpaceGLView::mouseMoveEvent(QMouseEvent *e) {
|
|
const QPoint delta = e->pos() - lastMousePos_;
|
|
lastMousePos_ = e->pos();
|
|
|
|
if (e->buttons() & Qt::LeftButton) {
|
|
yaw_ += delta.x() * 0.5f;
|
|
pitch_ += delta.y() * 0.5f;
|
|
pitch_ = qBound(-89.0f, pitch_, 89.0f);
|
|
update();
|
|
} else if (e->buttons() & Qt::RightButton) {
|
|
const float panScale = zoom_ * 0.0015f;
|
|
|
|
QMatrix4x4 rot;
|
|
rot.rotate(pitch_, 1, 0, 0);
|
|
rot.rotate(yaw_, 0, 1, 0);
|
|
|
|
const QVector3D right = rot.inverted().mapVector(QVector3D(1, 0, 0));
|
|
const QVector3D up = rot.inverted().mapVector(QVector3D(0, 1, 0));
|
|
|
|
target_ -= right * float(delta.x()) * panScale;
|
|
target_ += up * float(delta.y()) * panScale;
|
|
|
|
update();
|
|
}
|
|
}
|
|
|
|
void ReciprocalSpaceGLView::wheelEvent(QWheelEvent *e) {
|
|
zoom_ *= (e->angleDelta().y() > 0) ? 0.9f : 1.1f;
|
|
zoom_ = qBound(1.0f, zoom_, 5000.0f);
|
|
resizeGL(width(), height());
|
|
update();
|
|
}
|
|
|
|
bool ReciprocalSpaceGLView::emitNearestSpot(const QPoint &screenPos) {
|
|
if (pendingSpots_.empty() || width() <= 0 || height() <= 0)
|
|
return false;
|
|
|
|
const QMatrix4x4 mvp = currentMvpMatrix();
|
|
|
|
float bestDist2 = std::numeric_limits<float>::max();
|
|
QPointF bestImagePos;
|
|
bool found = false;
|
|
|
|
for (const auto &v : pendingSpots_) {
|
|
if (v.pickable <= 0.5f)
|
|
continue;
|
|
|
|
const QVector4D clip = mvp * QVector4D(v.x, v.y, v.z, 1.0f);
|
|
if (clip.w() <= 0.0f)
|
|
continue;
|
|
|
|
const QVector3D ndc = clip.toVector3DAffine();
|
|
if (ndc.z() < -1.0f || ndc.z() > 1.0f)
|
|
continue;
|
|
|
|
const float sx = (ndc.x() * 0.5f + 0.5f) * float(width());
|
|
const float sy = (0.5f - ndc.y() * 0.5f) * float(height());
|
|
|
|
const float dx = sx - float(screenPos.x());
|
|
const float dy = sy - float(screenPos.y());
|
|
const float dist2 = dx * dx + dy * dy;
|
|
|
|
if (dist2 < bestDist2) {
|
|
bestDist2 = dist2;
|
|
bestImagePos = QPointF(v.image_x, v.image_y);
|
|
found = true;
|
|
}
|
|
}
|
|
|
|
// Roughly 15 px picking radius.
|
|
if (!found || bestDist2 > 15.0f * 15.0f)
|
|
return false;
|
|
|
|
emit spotDoubleClicked(bestImagePos);
|
|
return true;
|
|
}
|
|
|
|
// ============================================================================
|
|
// JFJochViewerReciprocalSpaceWindow
|
|
// ============================================================================
|
|
|
|
JFJochViewerReciprocalSpaceWindow::JFJochViewerReciprocalSpaceWindow(QWidget *parent)
|
|
: JFJochHelperWindow(parent) {
|
|
setWindowTitle("Reciprocal space");
|
|
resize(800, 800);
|
|
|
|
auto *central = new QWidget(this);
|
|
auto *layout = new QVBoxLayout(central);
|
|
|
|
glView_ = new ReciprocalSpaceGLView(central);
|
|
glView_->setMinimumSize(400, 400);
|
|
layout->addWidget(glView_, 1);
|
|
|
|
auto *controls = new QHBoxLayout();
|
|
|
|
crystalFrameCheck = new QCheckBox("Crystal frame (angle = 0)", central);
|
|
crystalFrameCheck->setChecked(false);
|
|
crystalFrameCheck->setEnabled(false);
|
|
|
|
showCellCheck = new QCheckBox("Show reciprocal cell", central);
|
|
showCellCheck->setChecked(true);
|
|
|
|
showPredictedCheck = new QCheckBox("Show predicted reflections", central);
|
|
showPredictedCheck->setChecked(false);
|
|
|
|
strideCombo = new QComboBox(central);
|
|
strideCombo->addItem("1", 1);
|
|
strideCombo->addItem("2", 2);
|
|
strideCombo->addItem("5", 5);
|
|
strideCombo->addItem("10", 10);
|
|
strideCombo->addItem("100", 100);
|
|
strideCombo->setCurrentIndex(0);
|
|
|
|
auto *loadFullDatasetButton = new QPushButton("Load full dataset", central);
|
|
auto *loadCurrentImageButton = new QPushButton("Load only current image", central);
|
|
auto *centerOriginButton = new QPushButton("Center 000", central);
|
|
|
|
controls->addWidget(crystalFrameCheck);
|
|
controls->addWidget(showCellCheck);
|
|
controls->addWidget(showPredictedCheck);
|
|
controls->addSpacing(12);
|
|
controls->addWidget(new QLabel("Stride:", central));
|
|
controls->addWidget(strideCombo);
|
|
controls->addWidget(loadFullDatasetButton);
|
|
controls->addWidget(loadCurrentImageButton);
|
|
controls->addStretch(1);
|
|
controls->addWidget(centerOriginButton);
|
|
|
|
layout->addLayout(controls);
|
|
setCentralWidget(central);
|
|
|
|
connect(crystalFrameCheck, &QCheckBox::toggled,
|
|
this, &JFJochViewerReciprocalSpaceWindow::rebuildGL);
|
|
connect(showCellCheck, &QCheckBox::toggled,
|
|
this, &JFJochViewerReciprocalSpaceWindow::rebuildGL);
|
|
connect(showPredictedCheck, &QCheckBox::toggled,
|
|
this, &JFJochViewerReciprocalSpaceWindow::rebuildGL);
|
|
|
|
connect(loadFullDatasetButton, &QPushButton::clicked, this, [this] {
|
|
if (!current_dataset_)
|
|
return;
|
|
|
|
const int64_t nimages = current_dataset_->experiment.GetImageNum();
|
|
if (nimages <= 0)
|
|
return;
|
|
|
|
const int64_t stride = strideCombo ? strideCombo->currentData().toLongLong() : 1;
|
|
|
|
full_dataset_mode_ = true;
|
|
emit loadSpotsRequest(0, nimages - 1, stride);
|
|
});
|
|
|
|
connect(loadCurrentImageButton, &QPushButton::clicked, this, [this] {
|
|
full_dataset_mode_ = false;
|
|
loadCurrentImageSpots(current_image_);
|
|
rebuildGL();
|
|
});
|
|
|
|
connect(centerOriginButton, &QPushButton::clicked,
|
|
glView_, &ReciprocalSpaceGLView::resetTarget);
|
|
|
|
// Reuse the existing helper-window zoom signal. For full-dataset mode we will
|
|
// later want image-number-aware picking, but current image mode can use this now.
|
|
connect(glView_, &ReciprocalSpaceGLView::spotDoubleClicked,
|
|
this, &JFJochHelperWindow::zoom);
|
|
|
|
rebuildGL();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public slots
|
|
// ---------------------------------------------------------------------------
|
|
|
|
void JFJochViewerReciprocalSpaceWindow::datasetLoaded(
|
|
std::shared_ptr<const JFJochReaderDataset> in_dataset)
|
|
{
|
|
current_dataset_ = std::move(in_dataset);
|
|
current_image_.reset();
|
|
|
|
spots_.clear();
|
|
reflections_.clear();
|
|
indexed_lattice_.reset();
|
|
current_back_rot_.reset();
|
|
full_dataset_mode_ = false;
|
|
has_rotation_ = false;
|
|
|
|
if (current_dataset_)
|
|
has_rotation_ = current_dataset_->experiment.GetGoniometer().has_value();
|
|
|
|
crystalFrameCheck->setEnabled(has_rotation_);
|
|
if (!has_rotation_)
|
|
crystalFrameCheck->setChecked(false);
|
|
|
|
rebuildGL();
|
|
}
|
|
|
|
void JFJochViewerReciprocalSpaceWindow::imageLoaded(std::shared_ptr<const JFJochReaderImage> image) {
|
|
current_image_ = std::move(image);
|
|
|
|
// In full-dataset mode, normal image updates should not replace the range view.
|
|
if (full_dataset_mode_)
|
|
return;
|
|
|
|
loadCurrentImageSpots(current_image_);
|
|
rebuildGL();
|
|
}
|
|
|
|
void JFJochViewerReciprocalSpaceWindow::loadCurrentImageSpots(
|
|
std::shared_ptr<const JFJochReaderImage> image)
|
|
{
|
|
spots_.clear();
|
|
reflections_.clear();
|
|
indexed_lattice_.reset();
|
|
current_back_rot_.reset();
|
|
|
|
if (!image)
|
|
return;
|
|
|
|
current_dataset_ = std::shared_ptr<const JFJochReaderDataset>(
|
|
image,
|
|
&image->Dataset()
|
|
);
|
|
|
|
const auto &dataset = image->Dataset();
|
|
const auto geom = dataset.experiment.GetDiffractionGeometry();
|
|
const auto axis = dataset.experiment.GetGoniometer();
|
|
const int64_t image_number = image->ImageData().number;
|
|
|
|
if (axis) {
|
|
const float angle_deg = axis->GetAngle_deg(static_cast<float>(image_number))
|
|
+ axis->GetWedge_deg() / 2.0f;
|
|
current_back_rot_ = axis->GetTransformationAngle(angle_deg);
|
|
}
|
|
|
|
spots_.reserve(image->ImageData().spots.size());
|
|
|
|
for (const auto &s : image->ImageData().spots)
|
|
addSpot(s, geom, axis, image_number);
|
|
|
|
reflections_.reserve(image->ImageData().reflections.size());
|
|
for (const auto &r : image->ImageData().reflections) {
|
|
reflections_.push_back({
|
|
.h = r.h,
|
|
.k = r.k,
|
|
.l = r.l,
|
|
.image_x = r.predicted_x,
|
|
.image_y = r.predicted_y
|
|
});
|
|
}
|
|
|
|
if (image->ImageData().indexing_lattice)
|
|
indexed_lattice_ = image->ImageData().indexing_lattice;
|
|
}
|
|
|
|
void JFJochViewerReciprocalSpaceWindow::addSpot(const SpotToSave &s, const DiffractionGeometry &geom,
|
|
const std::optional<GoniometerAxis> &axis, int64_t image_number) {
|
|
std::optional<RotMatrix> back_rot;
|
|
if (axis) {
|
|
const float angle_deg = axis->GetAngle_deg(static_cast<float>(image_number))
|
|
+ axis->GetWedge_deg() / 2.0f;
|
|
back_rot = axis->GetTransformationAngle(angle_deg);
|
|
}
|
|
|
|
CurrentSpot spot;
|
|
spot.recip_lab = s.ReciprocalCoord(geom);
|
|
spot.recip_crystal = back_rot ? (*back_rot * spot.recip_lab) : spot.recip_lab;
|
|
spot.image_x = s.x;
|
|
spot.image_y = s.y;
|
|
spot.image_number = image_number;
|
|
spot.h = static_cast<int32_t>(s.h);
|
|
spot.k = static_cast<int32_t>(s.k);
|
|
spot.l = static_cast<int32_t>(s.l);
|
|
spot.indexed = s.indexed;
|
|
spot.ice_ring = s.ice_ring;
|
|
|
|
spots_.emplace_back(spot);
|
|
}
|
|
|
|
void JFJochViewerReciprocalSpaceWindow::spotsLoaded(std::shared_ptr<const JFJochReaderSpots> reader_spots) {
|
|
if (!reader_spots || !current_dataset_)
|
|
return;
|
|
|
|
full_dataset_mode_ = true;
|
|
|
|
spots_.clear();
|
|
reflections_.clear();
|
|
current_back_rot_.reset();
|
|
|
|
const auto geom = current_dataset_->experiment.GetDiffractionGeometry();
|
|
const auto axis = current_dataset_->experiment.GetGoniometer();
|
|
|
|
size_t total_spots = 0;
|
|
for (const auto &image_spots : reader_spots->spots)
|
|
total_spots += image_spots.size();
|
|
|
|
spots_.reserve(total_spots);
|
|
|
|
for (size_t image_index = 0; image_index < reader_spots->spots.size(); ++image_index) {
|
|
const int64_t image_number = reader_spots->start_image + image_index * reader_spots->stride;
|
|
|
|
for (const auto &s : reader_spots->spots[image_index])
|
|
addSpot(s, geom, axis, image_number);
|
|
}
|
|
|
|
// Keep the cell vectors from the currently visible image if available.
|
|
// Later we can improve this by reading/choosing a representative lattice for the range.
|
|
if (current_image_ && current_image_->ImageData().indexing_lattice)
|
|
indexed_lattice_ = current_image_->ImageData().indexing_lattice;
|
|
|
|
rebuildGL();
|
|
}
|
|
|
|
void JFJochViewerReciprocalSpaceWindow::setSpotColor(QColor input) {
|
|
if (!input.isValid())
|
|
return;
|
|
|
|
spot_color = input;
|
|
rebuildGL();
|
|
}
|
|
|
|
void JFJochViewerReciprocalSpaceWindow::setFeatureColor(QColor input) {
|
|
if (!input.isValid())
|
|
return;
|
|
|
|
indexed_color = input;
|
|
rebuildGL();
|
|
}
|
|
|
|
void JFJochViewerReciprocalSpaceWindow::setPredictionColor(QColor input) {
|
|
if (!input.isValid())
|
|
return;
|
|
|
|
prediction_color = input;
|
|
rebuildGL();
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Private
|
|
// ---------------------------------------------------------------------------
|
|
|
|
QColor JFJochViewerReciprocalSpaceWindow::spotColorFor(bool indexed,
|
|
bool ice_ring) const {
|
|
if (indexed)
|
|
return indexed_color;
|
|
if (ice_ring)
|
|
return ice_ring_color;
|
|
return spot_color;
|
|
}
|
|
|
|
void JFJochViewerReciprocalSpaceWindow::rebuildGL() {
|
|
const bool crystal_frame = crystalFrameCheck->isChecked() && has_rotation_;
|
|
|
|
std::optional<CrystalLattice> plot_lattice;
|
|
if (indexed_lattice_) {
|
|
plot_lattice = indexed_lattice_.value();
|
|
|
|
// If spots are brought back to angle zero, bring the lattice back too.
|
|
// After this, q = h*Astar + k*Bstar + l*Cstar is in the same frame as the plotted spots.
|
|
if (crystal_frame && current_back_rot_)
|
|
plot_lattice = plot_lattice->Multiply(*current_back_rot_);
|
|
}
|
|
|
|
// --- Spots ---------------------------------------------------------------
|
|
std::vector<ReciprocalSpaceGLView::Vertex> spotVerts;
|
|
spotVerts.reserve(spots_.size() + 1);
|
|
|
|
auto toF = [](const QColor &c, float &r, float &g, float &b) {
|
|
r = float(c.redF());
|
|
g = float(c.greenF());
|
|
b = float(c.blueF());
|
|
};
|
|
|
|
float sr, sg, sb;
|
|
float ir, ig, ib;
|
|
float xr, xg, xb;
|
|
float pr, pg, pb;
|
|
|
|
toF(spot_color, sr, sg, sb);
|
|
toF(indexed_color, ir, ig, ib);
|
|
toF(ice_ring_color, xr, xg, xb);
|
|
toF(prediction_color, pr, pg, pb);
|
|
|
|
for (const auto &spot : spots_) {
|
|
const Coord &c = crystal_frame ? spot.recip_crystal : spot.recip_lab;
|
|
|
|
float r, g, b;
|
|
if (spot.indexed) {
|
|
r = ir;
|
|
g = ig;
|
|
b = ib;
|
|
} else if (spot.ice_ring) {
|
|
r = xr;
|
|
g = xg;
|
|
b = xb;
|
|
} else {
|
|
r = sr;
|
|
g = sg;
|
|
b = sb;
|
|
}
|
|
|
|
spotVerts.push_back({
|
|
c.x * scene_scale_,
|
|
c.y * scene_scale_,
|
|
c.z * scene_scale_,
|
|
r, g, b, 1.0f,
|
|
spot.image_x,
|
|
spot.image_y,
|
|
static_cast<float>(spot.image_number),
|
|
1.0f
|
|
});
|
|
}
|
|
|
|
// Explicit 000 reflection marker.
|
|
spotVerts.push_back({
|
|
0.0f, 0.0f, 0.0f,
|
|
1.0f, 1.0f, 1.0f, 1.0f,
|
|
0.0f, 0.0f,
|
|
-1.0f,
|
|
0.0f
|
|
});
|
|
|
|
if (showPredictedCheck->isChecked() && plot_lattice) {
|
|
const Coord a = plot_lattice->Astar();
|
|
const Coord b = plot_lattice->Bstar();
|
|
const Coord c = plot_lattice->Cstar();
|
|
|
|
for (const auto &r : reflections_) {
|
|
bool observedIndexed = false;
|
|
for (const auto &spot : spots_) {
|
|
if (spot.indexed && spot.h == r.h && spot.k == r.k && spot.l == r.l) {
|
|
observedIndexed = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (observedIndexed)
|
|
continue;
|
|
|
|
const Coord node{
|
|
r.h * a.x + r.k * b.x + r.l * c.x,
|
|
r.h * a.y + r.k * b.y + r.l * c.y,
|
|
r.h * a.z + r.k * b.z + r.l * c.z
|
|
};
|
|
|
|
// Highlight nodes that are actual predictions for this image.
|
|
spotVerts.push_back({
|
|
node.x * scene_scale_,
|
|
node.y * scene_scale_,
|
|
node.z * scene_scale_,
|
|
pr, pg, pb, 0.80f,
|
|
r.image_x, r.image_y,
|
|
-1.0f,
|
|
1.0f
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
// --- Lines: axes + optional cell vectors ---------------------------------
|
|
std::vector<ReciprocalSpaceGLView::Vertex> lineVerts;
|
|
|
|
auto addLine = [&](QVector3D a, QVector3D b, QColor col, float alpha = 1.0f) {
|
|
const float r = float(col.redF());
|
|
const float g = float(col.greenF());
|
|
const float bv = float(col.blueF());
|
|
|
|
lineVerts.push_back({
|
|
a.x(), a.y(), a.z(),
|
|
r, g, bv, alpha,
|
|
0.0f, 0.0f,
|
|
-1.0f, 0.0f,
|
|
});
|
|
|
|
lineVerts.push_back({
|
|
b.x(), b.y(), b.z(),
|
|
r, g, bv, alpha,
|
|
0.0f, 0.0f,
|
|
-1.0f, 0.0f
|
|
});
|
|
};
|
|
|
|
// Reference coordinate axes: short and white.
|
|
const float len = 20.0f;
|
|
addLine({0, 0, 0}, {len, 0, 0}, Qt::white, 0.75f);
|
|
addLine({0, 0, 0}, {0, len, 0}, Qt::white, 0.75f);
|
|
addLine({0, 0, 0}, {0, 0, len}, Qt::white, 0.75f);
|
|
|
|
// Reciprocal cell vectors, using the same plotted lattice frame as predicted nodes.
|
|
if (showCellCheck->isChecked() && plot_lattice) {
|
|
auto addCell = [&](const Coord &v, QColor col) {
|
|
addLine({0, 0, 0},
|
|
{
|
|
v.x * scene_scale_,
|
|
v.y * scene_scale_,
|
|
v.z * scene_scale_
|
|
},
|
|
col,
|
|
1.0f);
|
|
};
|
|
|
|
addCell(plot_lattice->Astar(), Qt::red);
|
|
addCell(plot_lattice->Bstar(), Qt::green);
|
|
addCell(plot_lattice->Cstar(), Qt::blue);
|
|
}
|
|
|
|
glView_->setSpots(spotVerts);
|
|
glView_->setLines(lineVerts);
|
|
} |