Files
Jungfraujoch/viewer/windows/JFJochViewerReciprocalSpaceWindow.cpp
T

274 lines
9.6 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 <QWidget>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <Qt3DCore/QEntity>
#include <Qt3DCore/QTransform>
#include <Qt3DExtras/Qt3DWindow>
#include <Qt3DExtras/QOrbitCameraController>
#include <Qt3DExtras/QSphereMesh>
#include <Qt3DExtras/QCylinderMesh>
#include <Qt3DExtras/QPhongMaterial>
#include <Qt3DRender/QCamera>
#include <Qt3DRender/QPointLight>
#include <Qt3DExtras/QForwardRenderer>
#include "../../common/CrystalLattice.h"
namespace {
// Add a small sphere at the given position with the given color.
void AddBall(Qt3DCore::QEntity *parent, const QVector3D &pos, const QColor &color) {
auto *entity = new Qt3DCore::QEntity(parent);
auto *mesh = new Qt3DExtras::QSphereMesh(entity);
auto *material = new Qt3DExtras::QPhongMaterial(entity);
auto *transform = new Qt3DCore::QTransform(entity);
mesh->setRadius(0.4f);
mesh->setRings(6); // low-poly: plenty for a tiny spot
mesh->setSlices(6);
material->setDiffuse(color);
material->setAmbient(color.darker(150));
transform->setTranslation(pos);
entity->addComponent(mesh);
entity->addComponent(material);
entity->addComponent(transform);
}
// Draw a cylinder from the origin along the given (already scaled) vector.
void AddVector(Qt3DCore::QEntity *parent, const QVector3D &vec,
const QColor &color, float thickness) {
const float length = vec.length();
if (length < 1e-6f)
return;
auto *entity = new Qt3DCore::QEntity(parent);
auto *mesh = new Qt3DExtras::QCylinderMesh(entity);
auto *material = new Qt3DExtras::QPhongMaterial(entity);
auto *transform = new Qt3DCore::QTransform(entity);
mesh->setRadius(thickness);
mesh->setLength(length);
material->setDiffuse(color);
// QCylinderMesh points along +Y; rotate to match vec direction.
const QQuaternion rot = QQuaternion::rotationTo(QVector3D(0, 1, 0), vec.normalized());
transform->setRotation(rot);
transform->setTranslation(vec * 0.5f);
entity->addComponent(mesh);
entity->addComponent(material);
entity->addComponent(transform);
}
} // namespace
// ---------------------------------------------------------------------------
JFJochViewerReciprocalSpaceWindow::JFJochViewerReciprocalSpaceWindow(QWidget *parent)
: JFJochHelperWindow(parent)
{
setWindowTitle("Reciprocal space");
resize(800, 800);
// --- Widget layout -------------------------------------------------------
auto *central = new QWidget(this);
auto *layout = new QVBoxLayout(central);
view_ = new Qt3DExtras::Qt3DWindow();
view_->defaultFrameGraph()->setClearColor(QColor(20, 20, 25));
QWidget *container = QWidget::createWindowContainer(view_, central);
container->setMinimumSize(400, 400);
container->setFocusPolicy(Qt::StrongFocus);
layout->addWidget(container, 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);
accumulateCheck = new QCheckBox("Accumulate", central);
accumulateCheck->setChecked(false);
auto *clearButton = new QPushButton("Clear accumulated", central);
controls->addWidget(crystalFrameCheck);
controls->addWidget(showCellCheck);
controls->addWidget(accumulateCheck);
controls->addStretch(1);
controls->addWidget(clearButton);
layout->addLayout(controls);
setCentralWidget(central);
connect(crystalFrameCheck, &QCheckBox::toggled,
this, &JFJochViewerReciprocalSpaceWindow::rebuildScene);
connect(showCellCheck, &QCheckBox::toggled,
this, &JFJochViewerReciprocalSpaceWindow::rebuildScene);
connect(clearButton, &QPushButton::clicked, this, [this] {
spots_.clear();
rebuildScene();
});
// --- Qt3D scene ----------------------------------------------------------
root_ = new Qt3DCore::QEntity();
auto *camera = view_->camera();
camera->lens()->setPerspectiveProjection(45.0f, 1.0f, 0.1f, 10000.0f);
camera->setPosition(QVector3D(0, 0, 80));
camera->setViewCenter(QVector3D(0, 0, 0));
auto *lightEntity = new Qt3DCore::QEntity(root_);
auto *light = new Qt3DRender::QPointLight(lightEntity);
light->setIntensity(1.0f);
auto *lightTransform = new Qt3DCore::QTransform(lightEntity);
lightTransform->setTranslation(QVector3D(0, 0, 200));
lightEntity->addComponent(light);
lightEntity->addComponent(lightTransform);
auto *camController = new Qt3DExtras::QOrbitCameraController(root_);
camController->setCamera(camera);
view_->setRootEntity(root_);
rebuildScene();
}
// ---------------------------------------------------------------------------
// Public slots
// ---------------------------------------------------------------------------
void JFJochViewerReciprocalSpaceWindow::datasetLoaded(
std::shared_ptr<const JFJochReaderDataset> in_dataset)
{
spots_.clear();
indexed_lattice_.reset();
has_rotation_ = false;
if (in_dataset)
has_rotation_ = in_dataset->experiment.GetGoniometer().has_value();
crystalFrameCheck->setEnabled(has_rotation_);
if (!has_rotation_)
crystalFrameCheck->setChecked(false);
rebuildScene();
}
void JFJochViewerReciprocalSpaceWindow::imageLoaded(std::shared_ptr<const JFJochReaderImage> image)
{
if (!accumulateCheck->isChecked()) {
spots_.clear();
indexed_lattice_.reset();
}
if (!image) {
rebuildScene();
return;
}
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;
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);
}
spots_.reserve(spots_.size() + image->ImageData().spots.size());
for (const auto &s : image->ImageData().spots) {
AccumulatedSpot acc;
acc.recip_lab = s.ReciprocalCoord(geom);
acc.recip_crystal = back_rot ? (*back_rot * acc.recip_lab) : acc.recip_lab;
acc.indexed = s.indexed;
acc.ice_ring = s.ice_ring;
spots_.emplace_back(acc);
}
// Soft cap: drop oldest when accumulation grows too large.
constexpr size_t max_accumulated = 5000; // sphere-per-spot: keep this sane
while (spots_.size() > max_accumulated)
spots_.erase(spots_.begin());
if (image->ImageData().indexing_lattice)
indexed_lattice_ = image->ImageData().indexing_lattice;
rebuildScene();
}
void JFJochViewerReciprocalSpaceWindow::setSpotColor(QColor input) {
if (!input.isValid()) return;
spot_color = input;
rebuildScene();
}
void JFJochViewerReciprocalSpaceWindow::setFeatureColor(QColor input) {
if (!input.isValid()) return;
indexed_color = input;
rebuildScene();
}
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------
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::rebuildScene() {
// Delete the previous scene content (axes, cell vectors, all spot balls).
// root_ itself stays alive — only its dynamic child is replaced.
if (sceneContent_) {
delete sceneContent_; // Qt parent-child: also deletes all children
sceneContent_ = nullptr;
}
sceneContent_ = new Qt3DCore::QEntity(root_);
addAxes();
if (showCellCheck->isChecked() && indexed_lattice_)
addCellVectors();
addSpots();
}
void JFJochViewerReciprocalSpaceWindow::addAxes() {
const float len = 10.0f;
const float thickness = 0.05f;
AddVector(sceneContent_, QVector3D(len, 0, 0), QColor(120, 60, 60), thickness);
AddVector(sceneContent_, QVector3D(0, len, 0), QColor(60, 120, 60), thickness);
AddVector(sceneContent_, QVector3D(0, 0, len), QColor(60, 60, 120), thickness);
}
void JFJochViewerReciprocalSpaceWindow::addCellVectors() {
const Coord astar = indexed_lattice_->Astar();
const Coord bstar = indexed_lattice_->Bstar();
const Coord cstar = indexed_lattice_->Cstar();
const float thickness = 0.15f;
AddVector(sceneContent_, QVector3D(astar.x, astar.y, astar.z) * scene_scale_, Qt::red, thickness);
AddVector(sceneContent_, QVector3D(bstar.x, bstar.y, bstar.z) * scene_scale_, Qt::green, thickness);
AddVector(sceneContent_, QVector3D(cstar.x, cstar.y, cstar.z) * scene_scale_, Qt::blue, thickness);
}
void JFJochViewerReciprocalSpaceWindow::addSpots() {
const bool crystal_frame = crystalFrameCheck->isChecked() && has_rotation_;
for (const auto &spot : spots_) {
const Coord &c = crystal_frame ? spot.recip_crystal : spot.recip_lab;
const QVector3D pos(c.x * scene_scale_, c.y * scene_scale_, c.z * scene_scale_);
AddBall(sceneContent_, pos, spotColorFor(spot.indexed, spot.ice_ring));
}
}