File main_3d_window.cpp
File List > gui > main_3d_window.cpp
Go to the documentation of this file
#include "main_3d_window.hpp"
#include <qmessagebox.h>
#include <qtreewidget.h>
#include <QCheckBox>
#include <QColorDialog>
#include <QComboBox>
#include <QCoreApplication>
#include <QDialog>
#include <QDialogButtonBox>
#include <QDoubleSpinBox>
#include <QFileInfo>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QMenu>
#include <QOpenGLWidget>
#include <QPointer>
#include <QPushButton>
#include <QShortcut>
#include <QSignalBlocker>
#include <QSizePolicy>
#include <QSlider>
#include <QTabWidget>
#include <QVBoxLayout>
#include <QtConcurrent>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <tuple>
#include <utility>
#include <vector>
#include "blaze_output_loader.hpp"
#include "config_editor.hpp"
#include "config_input/config_input.hpp"
#include "error_dialog.hpp"
#include "gui/point_cloud_visualization.hpp"
#include "progress_box.hpp"
#include "run.hpp"
#include "ui_main_3d_window.h"
#include "utilities/env.hpp"
#include "utilities/progress_tracker.hpp"
namespace {
bool is_las_extension(const fs::path& path) {
std::string ext = path.extension().string();
for (char& c : ext) {
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
return ext == ".las" || ext == ".laz";
}
std::vector<fs::path> collect_open_las_files(const std::vector<std::shared_ptr<Layer>>& layers) {
std::vector<fs::path> las_files;
for (const auto& layer : layers) {
if (layer->kind() != LayerKind::PointCloud) {
continue;
}
const auto* las_layer = dynamic_cast<const LASLayer*>(layer.get());
if (!las_layer) {
continue;
}
const fs::path& path = las_layer->file_path();
if (!path.empty() && fs::exists(path) && is_las_extension(path)) {
las_files.push_back(fs::absolute(path));
}
}
return las_files;
}
bool is_draped_surface_layer(LayerKind kind) {
return kind == LayerKind::DemSurface || kind == LayerKind::SlopeSurface ||
kind == LayerKind::TexturedDem;
}
bool is_surface_style_layer(LayerKind kind) {
return is_draped_surface_layer(kind) || kind == LayerKind::Contours;
}
QString format_classification_label(uint8_t classification) {
for (const ClassificationStyle& style : CLASSIFICATION_STYLES) {
if (style.code == classification) {
return QString::fromLatin1(style.label);
}
}
return QStringLiteral("Class %1").arg(classification);
}
} // namespace
Q_DECLARE_METATYPE(std::shared_ptr<Layer>)
Main3DWindow::Main3DWindow() : ui(std::make_unique<Ui::Main3DWindow>()) {
gl_widget = std::make_unique<GLWidget>(this);
setWindowTitle(tr("Blaze 3D"));
try {
if (!QIcon::hasThemeIcon("list-add")) {
QIcon::setThemeName("Humanity");
QIcon::setFallbackThemeName("default");
}
ui->setupUi(this);
connect(ui->actionOpen, &QAction::triggered, this, &Main3DWindow::open_layer_file);
connect(ui->actionImportBlazeOutput, &QAction::triggered, this,
&Main3DWindow::import_blaze_output);
connect(ui->actionLightingSettings, &QAction::triggered, this,
&Main3DWindow::open_lighting_settings);
connect(ui->actionRunBlaze, &QAction::triggered, this, &Main3DWindow::run_blaze_on_layers);
ui->horizontalLayout->addWidget(gl_widget.get(), 1);
gl_widget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
gl_widget->setMinimumSize(320, 240);
gl_widget->set_point_pick_callback([this](const std::optional<PointPickResult>& pick) {
if (pick) {
show_point_pick_details(*pick);
} else {
clear_point_pick_details();
}
});
qRegisterMetaType<std::shared_ptr<Layer>>();
ui->treeWidget->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->treeWidget, &QTreeWidget::customContextMenuRequested, this,
&Main3DWindow::on_treeWidget_customContextMenuRequested);
connect(ui->treeWidget, &QTreeWidget::currentItemChanged, this,
[this](QTreeWidgetItem* current, QTreeWidgetItem* previous) {
(void)previous;
(void)current;
update_layer_panels_for_selection();
});
setup_animation_panel();
setup_point_cloud_panel();
setup_surface_layer_panel();
setup_point_details_panel();
ui->verticalLayout->setStretch(0, 1);
auto* remove_shortcut = new QShortcut(QKeySequence::Delete, ui->treeWidget);
connect(remove_shortcut, &QShortcut::activated, this, &Main3DWindow::remove_selected_layer);
} catch (const std::exception& e) {
show_error_message(this, "Error", e.what());
exit(1);
}
}
Main3DWindow::~Main3DWindow() {}
void Main3DWindow::setup_animation_panel() {
auto* panel = new QGroupBox(tr("Animation"), this);
auto* layout = new QVBoxLayout(panel);
// Type selector
auto* type_label = new QLabel(tr("Type"), panel);
layout->addWidget(type_label);
auto* type_combo = new QComboBox(panel);
type_combo->addItem(tr("None"), static_cast<int>(GLWidget::AnimType::None));
type_combo->addItem(tr("Orbit"), static_cast<int>(GLWidget::AnimType::Orbit));
type_combo->addItem(tr("Wobble"), static_cast<int>(GLWidget::AnimType::Wobble));
layout->addWidget(type_combo);
// Orbit settings
auto* orbit_group = new QWidget(panel);
auto* orbit_layout = new QVBoxLayout(orbit_group);
orbit_layout->setContentsMargins(0, 0, 0, 0);
auto* orbit_period_label = new QLabel(tr("Period (s)"), orbit_group);
orbit_layout->addWidget(orbit_period_label);
auto* orbit_period_slider = new QSlider(Qt::Horizontal, orbit_group);
orbit_period_slider->setRange(2, 120);
orbit_period_slider->setValue(static_cast<int>(gl_widget->orbit_period()));
auto* orbit_period_value = new QLabel(orbit_group);
orbit_period_value->setNum(static_cast<int>(gl_widget->orbit_period()));
orbit_period_value->setMinimumWidth(36);
orbit_period_value->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
auto* orbit_period_row = new QHBoxLayout();
orbit_period_row->addWidget(orbit_period_slider);
orbit_period_row->addWidget(orbit_period_value);
orbit_layout->addLayout(orbit_period_row);
layout->addWidget(orbit_group);
// Wobble settings
auto* wobble_group = new QWidget(panel);
auto* wobble_layout = new QVBoxLayout(wobble_group);
wobble_layout->setContentsMargins(0, 0, 0, 0);
auto* wobble_period_label = new QLabel(tr("Period (s)"), wobble_group);
wobble_layout->addWidget(wobble_period_label);
auto* wobble_period_slider = new QSlider(Qt::Horizontal, wobble_group);
wobble_period_slider->setRange(3, 100); // 0.3 to 10.0 seconds in 0.1 increments
wobble_period_slider->setValue(static_cast<int>(gl_widget->wobble_period() * 10.0));
auto* wobble_period_value = new QLabel(wobble_group);
wobble_period_value->setText(QString::number(gl_widget->wobble_period(), 'f', 1));
wobble_period_value->setMinimumWidth(36);
wobble_period_value->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
auto* wobble_period_row = new QHBoxLayout();
wobble_period_row->addWidget(wobble_period_slider);
wobble_period_row->addWidget(wobble_period_value);
wobble_layout->addLayout(wobble_period_row);
auto* wobble_amp_label = new QLabel(tr("Amplitude (deg)"), wobble_group);
wobble_layout->addWidget(wobble_amp_label);
auto* wobble_amp_slider = new QSlider(Qt::Horizontal, wobble_group);
wobble_amp_slider->setRange(1, 50); // 0.1 to 5.0 degrees
wobble_amp_slider->setValue(static_cast<int>(gl_widget->wobble_amplitude() * 10.0));
auto* wobble_amp_value = new QLabel(wobble_group);
wobble_amp_value->setText(QString::number(gl_widget->wobble_amplitude(), 'f', 1));
wobble_amp_value->setMinimumWidth(36);
wobble_amp_value->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
auto* wobble_amp_row = new QHBoxLayout();
wobble_amp_row->addWidget(wobble_amp_slider);
wobble_amp_row->addWidget(wobble_amp_value);
wobble_layout->addLayout(wobble_amp_row);
layout->addWidget(wobble_group);
// Show/hide settings based on type
auto update_visibility = [=](int type) {
orbit_group->setVisible(type == static_cast<int>(GLWidget::AnimType::Orbit));
wobble_group->setVisible(type == static_cast<int>(GLWidget::AnimType::Wobble));
};
update_visibility(static_cast<int>(GLWidget::AnimType::None));
wobble_group->hide();
orbit_group->hide();
// Connections
connect(type_combo, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
[this, type_combo, update_visibility](int /*idx*/) {
int t = type_combo->currentData().toInt();
update_visibility(t);
gl_widget->set_anim_type(t);
});
connect(orbit_period_slider, &QSlider::valueChanged, this, [this, orbit_period_value](int v) {
orbit_period_value->setNum(v);
gl_widget->set_orbit_period(static_cast<double>(v));
});
connect(wobble_period_slider, &QSlider::valueChanged, this, [this, wobble_period_value](int v) {
double secs = static_cast<double>(v) * 0.1;
wobble_period_value->setText(QString::number(secs, 'f', 1));
gl_widget->set_wobble_period(secs);
});
connect(wobble_amp_slider, &QSlider::valueChanged, this, [this, wobble_amp_value](int v) {
double deg = static_cast<double>(v) * 0.1;
wobble_amp_value->setText(QString::number(deg, 'f', 1));
gl_widget->set_wobble_amplitude(deg);
});
ui->verticalLayout->addWidget(panel);
}
void Main3DWindow::setup_point_cloud_panel() {
m_point_cloud_panel = new QGroupBox(tr("Point Cloud"), this);
auto* layout = new QVBoxLayout(m_point_cloud_panel);
auto* size_label = new QLabel(tr("Point radius (m)"), m_point_cloud_panel);
m_point_size_slider = new QSlider(Qt::Horizontal, m_point_cloud_panel);
m_point_size_slider->setRange(8, 1200);
m_point_size_slider->setValue(150);
m_point_size_value_label = new QLabel(m_point_cloud_panel);
m_point_size_value_label->setMinimumWidth(60);
m_point_size_value_label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
auto* size_row = new QHBoxLayout();
size_row->addWidget(m_point_size_slider);
size_row->addWidget(m_point_size_value_label);
layout->addWidget(size_label);
layout->addLayout(size_row);
auto* alpha_label = new QLabel(tr("Opacity"), m_point_cloud_panel);
m_point_alpha_slider = new QSlider(Qt::Horizontal, m_point_cloud_panel);
m_point_alpha_slider->setRange(0, 100);
m_point_alpha_slider->setValue(100);
m_point_alpha_value_label = new QLabel(m_point_cloud_panel);
m_point_alpha_value_label->setMinimumWidth(52);
m_point_alpha_value_label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
auto* alpha_row = new QHBoxLayout();
alpha_row->addWidget(m_point_alpha_slider);
alpha_row->addWidget(m_point_alpha_value_label);
layout->addWidget(alpha_label);
layout->addLayout(alpha_row);
auto* budget_label = new QLabel(tr("Stream budget (ms)"), m_point_cloud_panel);
m_point_stream_budget_slider = new QSlider(Qt::Horizontal, m_point_cloud_panel);
m_point_stream_budget_slider->setRange(8, 200);
m_point_stream_budget_slider->setValue(30);
m_point_stream_budget_slider->setToolTip(
tr("Target GPU time per streaming frame. Higher = denser points when zoomed out, slower."));
m_point_stream_budget_value_label = new QLabel(m_point_cloud_panel);
m_point_stream_budget_value_label->setMinimumWidth(52);
m_point_stream_budget_value_label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
auto* budget_row = new QHBoxLayout();
budget_row->addWidget(m_point_stream_budget_slider);
budget_row->addWidget(m_point_stream_budget_value_label);
layout->addWidget(budget_label);
layout->addLayout(budget_row);
auto* color_label = new QLabel(tr("Color mode"), m_point_cloud_panel);
m_point_color_mode_combo = new QComboBox(m_point_cloud_panel);
m_point_color_mode_combo->addItem(tr("From file"), static_cast<int>(PointColorMode::File));
m_point_color_mode_combo->addItem(tr("Classification"),
static_cast<int>(PointColorMode::Classification));
m_point_color_mode_combo->addItem(tr("Fixed color"), static_cast<int>(PointColorMode::Fixed));
layout->addWidget(color_label);
layout->addWidget(m_point_color_mode_combo);
m_point_fixed_color_button = new QPushButton(tr("Choose color..."), m_point_cloud_panel);
layout->addWidget(m_point_fixed_color_button);
ui->verticalLayout->addWidget(m_point_cloud_panel);
m_point_cloud_panel->hide();
connect(m_point_size_slider, &QSlider::valueChanged, this, [this](int) {
update_point_cloud_value_labels();
apply_point_cloud_style_from_ui();
});
connect(m_point_alpha_slider, &QSlider::valueChanged, this, [this](int) {
update_point_cloud_value_labels();
apply_point_cloud_style_from_ui();
});
connect(m_point_stream_budget_slider, &QSlider::valueChanged, this, [this](int) {
update_point_cloud_value_labels();
apply_point_cloud_style_from_ui();
});
update_point_cloud_value_labels();
connect(m_point_color_mode_combo, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
[this](int) { apply_point_cloud_style_from_ui(); });
connect(m_point_fixed_color_button, &QPushButton::clicked, this, [this] {
auto layer = m_active_las_layer.lock();
if (!layer) {
return;
}
const std::array<uint8_t, 3>& rgb = layer->fixed_point_color();
const QColor initial(rgb[0], rgb[1], rgb[2]);
const QColor chosen = QColorDialog::getColor(initial, this, tr("Point cloud color"),
QColorDialog::DontUseNativeDialog);
if (!chosen.isValid()) {
return;
}
layer->set_fixed_point_color(
{{static_cast<uint8_t>(chosen.red()), static_cast<uint8_t>(chosen.green()),
static_cast<uint8_t>(chosen.blue())}});
layer->set_point_color_mode(PointColorMode::Fixed);
m_updating_point_cloud_ui = true;
m_point_color_mode_combo->setCurrentIndex(
m_point_color_mode_combo->findData(static_cast<int>(PointColorMode::Fixed)));
m_updating_point_cloud_ui = false;
update_layer_panels_for_selection();
gl_widget->update();
});
}
void Main3DWindow::setup_surface_layer_panel() {
m_surface_layer_panel = new QGroupBox(tr("Surface Layer"), this);
auto* layout = new QVBoxLayout(m_surface_layer_panel);
auto* opacity_label = new QLabel(tr("Opacity"), m_surface_layer_panel);
m_surface_opacity_slider = new QSlider(Qt::Horizontal, m_surface_layer_panel);
m_surface_opacity_slider->setRange(0, 100);
m_surface_opacity_slider->setValue(100);
m_surface_opacity_value_label = new QLabel(m_surface_layer_panel);
m_surface_opacity_value_label->setMinimumWidth(52);
m_surface_opacity_value_label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
auto* opacity_row = new QHBoxLayout();
opacity_row->addWidget(m_surface_opacity_slider);
opacity_row->addWidget(m_surface_opacity_value_label);
layout->addWidget(opacity_label);
layout->addLayout(opacity_row);
auto* vertical_offset_label = new QLabel(tr("Vertical offset"), m_surface_layer_panel);
m_surface_vertical_offset_spin = new QDoubleSpinBox(m_surface_layer_panel);
m_surface_vertical_offset_spin->setRange(-10.0, 10.0);
m_surface_vertical_offset_spin->setDecimals(2);
m_surface_vertical_offset_spin->setSingleStep(0.05);
m_surface_vertical_offset_spin->setSuffix(tr(" m"));
layout->addWidget(vertical_offset_label);
layout->addWidget(m_surface_vertical_offset_spin);
ui->verticalLayout->addWidget(m_surface_layer_panel);
m_surface_layer_panel->hide();
connect(m_surface_opacity_slider, &QSlider::valueChanged, this, [this](int) {
update_surface_layer_value_labels();
apply_surface_layer_style_from_ui();
});
connect(m_surface_vertical_offset_spin, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
this, [this](double) { apply_surface_layer_style_from_ui(); });
update_surface_layer_value_labels();
}
void Main3DWindow::setup_point_details_panel() {
m_point_details_panel = new QGroupBox(tr("Selected Point"), this);
auto* layout = new QVBoxLayout(m_point_details_panel);
m_point_details_label =
new QLabel(tr("Click a point in the viewer to inspect it."), m_point_details_panel);
m_point_details_label->setWordWrap(true);
m_point_details_label->setTextInteractionFlags(Qt::TextSelectableByMouse);
layout->addWidget(m_point_details_label);
ui->verticalLayout->addWidget(m_point_details_panel);
}
void Main3DWindow::show_point_pick_details(const PointPickResult& pick) {
if (!m_point_details_label) {
return;
}
const OctreePoint& point = pick.point;
QString color_line;
if (point.has_file_rgb) {
color_line = tr("RGB: %1, %2, %3").arg(point.file_r).arg(point.file_g).arg(point.file_b);
} else {
color_line = tr("RGB: (not stored)");
}
m_point_details_label->setText(tr("Layer: %1\n"
"World X: %2\n"
"World Y: %3\n"
"World Z: %4\n"
"Intensity: %5\n"
"Classification: %6 (%7)\n"
"%8")
.arg(QString::fromStdString(pick.layer_name))
.arg(pick.world_x, 0, 'f', 3)
.arg(pick.world_y, 0, 'f', 3)
.arg(pick.world_z, 0, 'f', 3)
.arg(point.intensity)
.arg(point.classification)
.arg(format_classification_label(point.classification))
.arg(color_line));
}
void Main3DWindow::clear_point_pick_details() {
if (m_point_details_label) {
m_point_details_label->setText(tr("Click a point in the viewer to inspect it."));
}
}
void Main3DWindow::update_layer_panels_for_selection() {
update_point_cloud_panel_for_selection();
update_surface_layer_panel_for_selection();
}
void Main3DWindow::update_point_cloud_panel_for_selection() {
QTreeWidgetItem* item = ui->treeWidget->currentItem();
std::shared_ptr<LASLayer> las_layer;
if (item) {
const auto layer = item->data(0, Qt::UserRole).value<std::shared_ptr<Layer>>();
las_layer = std::dynamic_pointer_cast<LASLayer>(layer);
}
m_active_las_layer = las_layer;
if (!las_layer) {
m_point_cloud_panel->hide();
return;
}
m_updating_point_cloud_ui = true;
m_point_size_slider->setValue(
static_cast<int>(std::lround(las_layer->point_radius_m() * 1000.0f)));
m_point_alpha_slider->setValue(static_cast<int>(std::lround(las_layer->point_alpha() * 100.0f)));
m_point_stream_budget_slider->setValue(
static_cast<int>(std::lround(las_layer->point_stream_budget_ms())));
const int mode_index =
m_point_color_mode_combo->findData(static_cast<int>(las_layer->point_color_mode()));
if (mode_index >= 0) {
m_point_color_mode_combo->setCurrentIndex(mode_index);
}
const std::array<uint8_t, 3>& rgb = las_layer->fixed_point_color();
m_point_fixed_color_button->setStyleSheet(
QString("background-color: rgb(%1,%2,%3);").arg(rgb[0]).arg(rgb[1]).arg(rgb[2]));
m_point_fixed_color_button->setEnabled(las_layer->point_color_mode() == PointColorMode::Fixed);
m_point_cloud_panel->show();
update_point_cloud_value_labels();
m_updating_point_cloud_ui = false;
}
void Main3DWindow::update_surface_layer_panel_for_selection() {
QTreeWidgetItem* item = ui->treeWidget->currentItem();
std::shared_ptr<Layer> surface_layer;
if (item) {
const auto layer = item->data(0, Qt::UserRole).value<std::shared_ptr<Layer>>();
if (layer && is_surface_style_layer(layer->kind())) {
surface_layer = layer;
}
}
m_active_surface_layer = surface_layer;
if (!surface_layer) {
m_surface_layer_panel->hide();
return;
}
m_updating_surface_ui = true;
m_surface_opacity_slider->setValue(
static_cast<int>(std::lround(surface_layer->opacity() * 100.0f)));
m_surface_vertical_offset_spin->setValue(surface_layer->vertical_offset());
m_surface_layer_panel->show();
update_surface_layer_value_labels();
m_updating_surface_ui = false;
}
void Main3DWindow::update_surface_layer_value_labels() {
if (!m_surface_opacity_slider || !m_surface_opacity_value_label) {
return;
}
m_surface_opacity_value_label->setText(QString("%1%").arg(m_surface_opacity_slider->value()));
}
void Main3DWindow::apply_surface_layer_style_from_ui() {
if (m_updating_surface_ui) {
return;
}
auto layer = m_active_surface_layer.lock();
if (!layer || !m_surface_opacity_slider || !m_surface_vertical_offset_spin) {
return;
}
layer->set_opacity(static_cast<float>(m_surface_opacity_slider->value()) / 100.0f);
layer->set_vertical_offset(static_cast<float>(m_surface_vertical_offset_spin->value()));
gl_widget->update();
}
void Main3DWindow::update_point_cloud_value_labels() {
if (!m_point_size_value_label || !m_point_alpha_value_label ||
!m_point_stream_budget_value_label || !m_point_size_slider || !m_point_alpha_slider ||
!m_point_stream_budget_slider) {
return;
}
const double radius_m = m_point_size_slider->value() / 1000.0;
m_point_size_value_label->setText(radius_m >= 0.1 ? QString("%1 m").arg(radius_m, 0, 'f', 2)
: QString("%1 m").arg(radius_m, 0, 'f', 3));
m_point_alpha_value_label->setText(QString("%1%").arg(m_point_alpha_slider->value()));
m_point_stream_budget_value_label->setText(
tr("%1 ms").arg(m_point_stream_budget_slider->value()));
}
void Main3DWindow::apply_point_cloud_style_from_ui() {
if (m_updating_point_cloud_ui) {
return;
}
auto layer = m_active_las_layer.lock();
if (!layer || !m_point_size_slider || !m_point_alpha_slider || !m_point_stream_budget_slider ||
!m_point_color_mode_combo) {
return;
}
layer->set_point_radius_m(static_cast<float>(m_point_size_slider->value()) / 1000.0f);
layer->set_point_alpha(static_cast<float>(m_point_alpha_slider->value()) / 100.0f);
layer->set_point_stream_budget_ms(static_cast<float>(m_point_stream_budget_slider->value()));
const PointColorMode mode =
static_cast<PointColorMode>(m_point_color_mode_combo->currentData().toInt());
layer->set_point_color_mode(mode);
m_point_fixed_color_button->setEnabled(mode == PointColorMode::Fixed);
gl_widget->update();
}
AsyncProgressTracker Main3DWindow::add_progress_tracker() {
ProgressTrackerBar* bar = new ProgressTrackerBar(ui->statusBar);
ui->statusBar->addPermanentWidget(bar);
// Claimed by the next add_layer() and keyed to that layer.
m_pending_progress_bar = bar;
return bar->tracker();
}
void Main3DWindow::add_layer_to_tree(std::shared_ptr<Layer> layer) {
QTreeWidgetItem* item = new QTreeWidgetItem(ui->treeWidget);
item->setText(0, QString::fromStdString(layer->name()));
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(0, layer->visible() ? Qt::Checked : Qt::Unchecked);
item->setData(0, Qt::UserRole, QVariant::fromValue(layer));
ui->treeWidget->addTopLevelItem(item);
ui->treeWidget->resizeColumnToContents(0);
}
void Main3DWindow::add_layer(std::unique_ptr<Layer> layer, bool auto_zoom) {
auto shared = std::shared_ptr<Layer>(std::move(layer));
m_layers.push_back(shared);
if (m_pending_progress_bar) {
m_layer_progress_bars[shared.get()] = m_pending_progress_bar;
m_pending_progress_bar = nullptr;
}
add_layer_to_tree(shared);
gl_widget->add_layer(shared, auto_zoom);
connect(shared.get(), &Layer::data_updated, this, &Main3DWindow::maybe_exit_after_load);
if (auto* las_layer = dynamic_cast<LASLayer*>(shared.get())) {
connect(las_layer, &LASLayer::point_colors_changed, this,
&Main3DWindow::update_point_cloud_panel_for_selection);
}
update_render_mode();
maybe_exit_after_load();
}
void Main3DWindow::open_layer_file() {
std::string filename =
QFileDialog::getOpenFileName(this, "Open LAS file", "", "LIDAR Point Clouds (*.las, *.laz)")
.toStdString();
if (filename.empty()) {
return;
}
m_defer_render_until_loaded = false;
add_layer(std::make_unique<LASLayer>(filename, add_progress_tracker(), scene_reference_crs()));
}
void Main3DWindow::import_blaze_output() {
QString dir = QFileDialog::getExistingDirectory(this, "Import Blaze Output Folder", QString());
if (dir.isEmpty()) {
return;
}
import_blaze_output_from_path(dir.toStdString());
}
void Main3DWindow::open_lighting_settings() {
QDialog dialog(this);
dialog.setWindowTitle(tr("View Settings"));
dialog.resize(480, 280);
auto* root = new QVBoxLayout(&dialog);
auto* tabs = new QTabWidget(&dialog);
auto* lighting_tab = new QWidget(tabs);
auto* lighting_layout = new QVBoxLayout(lighting_tab);
auto make_slider_row = [&](const QString& label, int min_v, int max_v, int value,
const QString& suffix) {
auto* row = new QWidget(lighting_tab);
auto* row_layout = new QHBoxLayout(row);
row_layout->setContentsMargins(0, 0, 0, 0);
auto* text = new QLabel(label, row);
auto* slider = new QSlider(Qt::Horizontal, row);
auto* value_label = new QLabel(row);
value_label->setMinimumWidth(58);
value_label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
slider->setRange(min_v, max_v);
slider->setValue(value);
value_label->setText(QString("%1%2").arg(value).arg(suffix));
row_layout->addWidget(text);
row_layout->addWidget(slider, 1);
row_layout->addWidget(value_label);
lighting_layout->addWidget(row);
return std::tuple<QSlider*, QLabel*>(slider, value_label);
};
auto [azimuth_slider, azimuth_value] =
make_slider_row(tr("Azimuth"), -180, 180,
static_cast<int>(std::lround(gl_widget->camera_light_azimuth_deg())),
QString::fromLatin1(" deg"));
auto [elevation_slider, elevation_value] =
make_slider_row(tr("Elevation"), -89, 89,
static_cast<int>(std::lround(gl_widget->camera_light_elevation_deg())),
QString::fromLatin1(" deg"));
auto [ambient_slider, ambient_value] = make_slider_row(
tr("Ambient"), 0, 100, static_cast<int>(std::lround(gl_widget->ambient_light() * 100.0f)),
QString::fromLatin1("%"));
auto [point_ambient_slider, point_ambient_value] =
make_slider_row(tr("Point ambient"), 0, 100,
static_cast<int>(std::lround(gl_widget->point_ambient_light() * 100.0f)),
QString::fromLatin1("%"));
auto [diffuse_slider, diffuse_value] = make_slider_row(
tr("Direct light"), 0, 200,
static_cast<int>(std::lround(gl_widget->diffuse_light() * 100.0f)), QString::fromLatin1("%"));
auto apply_lighting = [this, azimuth_slider, elevation_slider, ambient_slider,
point_ambient_slider, diffuse_slider] {
gl_widget->set_camera_light_angles(static_cast<float>(azimuth_slider->value()),
static_cast<float>(elevation_slider->value()));
gl_widget->set_lighting_strength(static_cast<float>(ambient_slider->value()) / 100.0f,
static_cast<float>(diffuse_slider->value()) / 100.0f);
gl_widget->set_point_ambient_light(static_cast<float>(point_ambient_slider->value()) / 100.0f);
};
auto connect_slider = [this, apply_lighting](QSlider* slider, QLabel* value_label,
const QString& suffix) {
connect(slider, &QSlider::valueChanged, this, [apply_lighting, value_label, suffix](int v) {
value_label->setText(QString("%1%2").arg(v).arg(suffix));
apply_lighting();
});
};
connect_slider(azimuth_slider, azimuth_value, QString::fromLatin1(" deg"));
connect_slider(elevation_slider, elevation_value, QString::fromLatin1(" deg"));
connect_slider(ambient_slider, ambient_value, QString::fromLatin1("%"));
connect_slider(point_ambient_slider, point_ambient_value, QString::fromLatin1("%"));
connect_slider(diffuse_slider, diffuse_value, QString::fromLatin1("%"));
auto* reset_button = new QPushButton(tr("Reset to default"), lighting_tab);
connect(reset_button, &QPushButton::clicked, this,
[azimuth_slider, elevation_slider, ambient_slider, point_ambient_slider, diffuse_slider] {
azimuth_slider->setValue(-50);
elevation_slider->setValue(35);
ambient_slider->setValue(30);
point_ambient_slider->setValue(55);
diffuse_slider->setValue(100);
});
lighting_layout->addWidget(reset_button);
lighting_layout->addStretch(1);
tabs->addTab(lighting_tab, tr("Lighting"));
root->addWidget(tabs);
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Close, &dialog);
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
root->addWidget(buttons);
dialog.exec();
}
void Main3DWindow::import_blaze_output_from_path(const std::string& directory) {
m_defer_render_until_loaded = true;
m_zoom_completed = false;
if (m_exit_after_load) {
m_exit_after_load_fired = false;
}
const BlazeOutputDiscovery discovery = discover_blaze_output_with_info(directory);
const BlazeOutputSet& outputs = discovery.outputs;
if (!outputs.filled_dem && !outputs.contours) {
const std::string message = format_blaze_output_discovery_error(directory, discovery);
if (isVisible()) {
QMessageBox::warning(this, "No outputs found", QString::fromStdString(message));
} else {
std::cerr << message << std::endl;
exit(1);
}
return;
}
auto layers = load_blaze_outputs(
outputs, [this]() { return add_progress_tracker(); },
[this]() { return scene_reference_crs(); });
for (auto& layer : layers) {
add_layer(std::move(layer), false);
}
update_render_mode();
// Always zoom to newly imported Blaze output layers
m_zoom_after_load = true;
maybe_exit_after_load();
}
void Main3DWindow::set_exit_after_load(bool exit_after_load) {
m_exit_after_load = exit_after_load;
update_render_mode();
}
void Main3DWindow::set_defer_render_until_loaded(bool defer) {
m_defer_render_until_loaded = defer;
update_render_mode();
}
void Main3DWindow::update_render_mode() {
if (!gl_widget) {
return;
}
const bool all_ready = !m_layers.empty() && std::all_of(m_layers.begin(), m_layers.end(),
[](const std::shared_ptr<Layer>& layer) {
return layer_is_ready(*layer);
});
const char* platform = blaze::get_env("QT_QPA_PLATFORM");
const bool offscreen = platform != nullptr && std::string(platform) == "offscreen";
const bool load_only = offscreen || (m_defer_render_until_loaded && !all_ready) ||
(m_exit_after_load && !m_exit_after_render);
gl_widget->set_load_only_mode(load_only);
if (all_ready && !offscreen) {
gl_widget->update();
}
}
bool Main3DWindow::layer_is_ready(const Layer& layer) {
switch (layer.kind()) {
case LayerKind::DemSurface:
return static_cast<const DemLayer&>(layer).raster().ready();
case LayerKind::SlopeSurface:
return static_cast<const SlopeLayer&>(layer).raster().ready();
case LayerKind::TexturedDem:
return static_cast<const TexturedDemLayer&>(layer).raster().ready();
case LayerKind::Contours:
return static_cast<const ContourLayer&>(layer).ready();
case LayerKind::PointCloud:
return static_cast<const LASLayer&>(layer).las_data().load_complete();
}
return true;
}
void Main3DWindow::maybe_exit_after_load() {
const bool all_layers_ready =
!m_layers.empty() &&
std::all_of(m_layers.begin(), m_layers.end(),
[](const std::shared_ptr<Layer>& layer) { return layer_is_ready(*layer); });
if (all_layers_ready && m_zoom_after_load) {
m_zoom_after_load = false;
m_zoom_completed = true;
m_defer_render_until_loaded = false;
if (m_exit_after_load) {
m_exit_after_render = true;
}
update_render_mode();
gl_widget->zoom_to_all_layers();
gl_widget->repaint();
} else if (!m_zoom_completed) {
update_render_mode();
}
if (!m_exit_after_load || m_layers.empty()) {
return;
}
for (const auto& layer : m_layers) {
if (!layer_is_ready(*layer)) {
return;
}
}
if (!m_exit_after_render) {
m_exit_after_render = true;
update_render_mode();
}
connect(gl_widget.get(), &QOpenGLWidget::frameSwapped, this,
&Main3DWindow::finish_exit_after_load, Qt::SingleShotConnection);
gl_widget->update();
}
void Main3DWindow::finish_exit_after_load() {
if (!m_exit_after_load || m_exit_after_load_fired) return;
m_exit_after_load_fired = true;
std::cout << "All layers loaded successfully." << std::endl;
if (m_bench_mode) {
QTimer::singleShot(100, this, [this] { gl_widget->start_bench_orbit(10.0); });
return;
}
QApplication::quit();
}
void Main3DWindow::run_blaze_on_layers() {
const std::vector<fs::path> las_files = collect_open_las_files(m_layers);
if (las_files.empty()) {
QMessageBox::warning(this, "No LAS layers",
"Open one or more LAS/LAZ point cloud layers before running Blaze.");
return;
}
// Create a dialog with the ConfigEditor widget
QDialog* config_dialog = new QDialog(this);
config_dialog->setWindowTitle("Configure Blaze Processing");
config_dialog->resize(800, 600);
QVBoxLayout* layout = new QVBoxLayout(config_dialog);
// Embed the ConfigEditor widget
ConfigEditor* config_editor = new ConfigEditor(config_dialog);
config_editor->set_las_files(las_files);
layout->addWidget(config_editor);
// Add Run/Cancel buttons
QDialogButtonBox* button_box =
new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, config_dialog);
button_box->button(QDialogButtonBox::Ok)->setText("Run Blaze");
button_box->button(QDialogButtonBox::Ok)->setEnabled(config_editor->is_valid());
connect(config_editor, &ConfigEditor::config_changed, [=]() {
button_box->button(QDialogButtonBox::Ok)->setEnabled(config_editor->is_valid());
});
layout->addWidget(button_box);
connect(button_box, &QDialogButtonBox::accepted, config_dialog, &QDialog::accept);
connect(button_box, &QDialogButtonBox::rejected, config_dialog, &QDialog::reject);
// Show dialog and wait for user
if (config_dialog->exec() != QDialog::Accepted) {
config_dialog->deleteLater();
return;
}
// User clicked "Run" - get the config reference
// Keep the dialog alive until the task completes by capturing it in the lambdas
const Config& config = config_editor->get_config();
run_blaze_with_config(config, config_dialog);
}
void Main3DWindow::run_blaze_with_config(const Config& config, QDialog* config_dialog) {
// Show progress dialog
ProgressBox* progress_box = new ProgressBox(this);
progress_box->show();
const fs::path output_dir = config.output_path();
// Detach config_dialog from the main-window parent so that closing the
// main window during processing does not cascade-delete the dialog (and
// the ConfigEditor / Config it owns). The completion callbacks still
// call deleteLater() to reclaim it. Guard both heap objects with
// QPointer so the background thread can bail out if they disappear.
config_dialog->setParent(nullptr);
QPointer<ProgressBox> progress_guard(progress_box);
QPointer<QDialog> dialog_guard(config_dialog);
progress_box->start_task(
// Task: run Blaze in background thread
[&config, progress_guard, dialog_guard]() {
if (!progress_guard || !dialog_guard) return;
run_with_config(config, std::vector<fs::path>(), ProgressTracker(progress_guard.data()));
},
// On success: import the results and clean up dialog
[this, output_dir, config_dialog]() {
import_blaze_output_from_path(output_dir.string());
QMessageBox::information(this, "Success",
QString("Blaze processing complete!\n\nOutput imported from:\n%1")
.arg(QString::fromStdString(output_dir.string())));
if (config_dialog) {
config_dialog->deleteLater();
}
},
// On error: show error message and clean up dialog
[this, config_dialog](const QString& error_message) {
show_error_message(this, "Blaze Error",
QString("Processing failed:\n\n%1").arg(error_message));
if (config_dialog) {
config_dialog->deleteLater();
}
});
}
void Main3DWindow::on_treeWidget_itemChanged(QTreeWidgetItem* item, int column) {
if (column != 0) {
return;
}
const auto layer = item->data(0, Qt::UserRole).value<std::shared_ptr<Layer>>();
if (!layer) {
return;
}
const bool visible = item->checkState(0) == Qt::Checked;
layer->set_visible(visible);
gl_widget->set_layer_visible(layer.get(), visible);
if (visible && is_draped_surface_layer(layer->kind())) {
for (const std::shared_ptr<Layer>& other : m_layers) {
if (!other || other == layer || !is_draped_surface_layer(other->kind()) ||
!other->visible()) {
continue;
}
other->set_visible(false);
gl_widget->set_layer_visible(other.get(), false);
if (QTreeWidgetItem* other_item = find_tree_item_for_layer(other.get())) {
QSignalBlocker blocker(ui->treeWidget);
other_item->setCheckState(0, Qt::Unchecked);
}
}
}
}
QTreeWidgetItem* Main3DWindow::find_tree_item_for_layer(Layer* layer) const {
for (int i = 0; i < ui->treeWidget->topLevelItemCount(); ++i) {
QTreeWidgetItem* item = ui->treeWidget->topLevelItem(i);
const auto item_layer = item->data(0, Qt::UserRole).value<std::shared_ptr<Layer>>();
if (item_layer && item_layer.get() == layer) {
return item;
}
}
return nullptr;
}
void Main3DWindow::remove_layer(const std::shared_ptr<Layer>& layer) {
if (!layer) {
return;
}
const auto it =
std::find_if(m_layers.begin(), m_layers.end(),
[&layer](const std::shared_ptr<Layer>& ptr) { return ptr == layer; });
if (it == m_layers.end()) {
return;
}
disconnect(layer.get(), &Layer::data_updated, this, &Main3DWindow::maybe_exit_after_load);
if (QTreeWidgetItem* item = find_tree_item_for_layer(layer.get())) {
item->setData(0, Qt::UserRole, QVariant());
delete ui->treeWidget->takeTopLevelItem(ui->treeWidget->indexOfTopLevelItem(item));
}
gl_widget->remove_layer(layer.get());
m_layers.erase(it);
if (const auto bar_it = m_layer_progress_bars.find(layer.get());
bar_it != m_layer_progress_bars.end()) {
ui->statusBar->removeWidget(bar_it->second);
delete bar_it->second;
m_layer_progress_bars.erase(bar_it);
}
update_render_mode();
}
void Main3DWindow::remove_selected_layer() {
const QList<QTreeWidgetItem*> selected = ui->treeWidget->selectedItems();
if (selected.isEmpty()) {
return;
}
const auto layer = selected.front()->data(0, Qt::UserRole).value<std::shared_ptr<Layer>>();
remove_layer(layer);
}
void Main3DWindow::on_treeWidget_customContextMenuRequested(const QPoint& pos) {
QTreeWidgetItem* item = ui->treeWidget->itemAt(pos);
if (!item) {
return;
}
ui->treeWidget->setCurrentItem(item);
QMenu menu(this);
QAction* zoom_action = menu.addAction(tr("Zoom to Layer"));
menu.addSeparator();
QAction* remove_action = menu.addAction(tr("Remove Layer"));
QAction* chosen = menu.exec(ui->treeWidget->viewport()->mapToGlobal(pos));
if (chosen == zoom_action) {
const auto layer = item->data(0, Qt::UserRole).value<std::shared_ptr<Layer>>();
if (layer) {
gl_widget->zoom_to_layer(layer.get());
}
} else if (chosen == remove_action) {
remove_selected_layer();
}
}