Files
Jungfraujoch/viewer/windows/JFJochViewerReciprocalSpaceWindow.cpp
T
leonarski_f 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
v1.0.0-rc.147 (#57)
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>
2026-06-02 11:49:24 +02:00

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);
}