274 lines
9.6 KiB
C++
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));
|
|
}
|
|
} |