Utils/CloudView/Src/CloudViewMainWindow.cpp

1665 lines
57 KiB
C++
Raw Normal View History

2026-02-19 00:45:00 +08:00
#include "CloudViewMainWindow.h"
#include <QFileInfo>
#include <QDialog>
#include <QTextEdit>
#include <QTableWidget>
#include <QHeaderView>
#include <QVector3D>
#include <QFile>
#include <QTextStream>
#include <QRegExp>
#include <QFrame>
#include <QTabWidget>
#include <QGridLayout>
#include <cmath>
#include "VrLog.h"
2026-02-22 16:54:45 +08:00
#include "LaserDataLoader.h"
2026-02-19 00:45:00 +08:00
CloudViewMainWindow::CloudViewMainWindow(QWidget* parent)
: QMainWindow(parent)
, m_glWidget(nullptr)
, m_converter(std::make_unique<PointCloudConverter>())
, m_cloudCount(0)
, m_currentLineNum(0)
, m_currentLinePtNum(0)
, m_linePointsDialog(nullptr)
, m_linePointsTable(nullptr)
{
setupUI();
LOG_INFO("CloudViewMainWindow initialized\n");
}
CloudViewMainWindow::~CloudViewMainWindow()
{
}
void CloudViewMainWindow::setupUI()
{
// 创建中央控件
QWidget* centralWidget = new QWidget(this);
setCentralWidget(centralWidget);
// 创建主布局
QHBoxLayout* mainLayout = new QHBoxLayout(centralWidget);
mainLayout->setContentsMargins(5, 5, 5, 5);
mainLayout->setSpacing(5);
// 创建分割器
QSplitter* splitter = new QSplitter(Qt::Horizontal, centralWidget);
// 左侧:点云显示区域
QWidget* viewerArea = createViewerArea();
splitter->addWidget(viewerArea);
// 右侧:控制面板
QWidget* controlPanel = createControlPanel();
splitter->addWidget(controlPanel);
// 设置分割器初始大小(左侧 70%,右侧 30%
splitter->setSizes({700, 300});
mainLayout->addWidget(splitter);
// 状态栏
statusBar()->showMessage("就绪");
}
QWidget* CloudViewMainWindow::createViewerArea()
{
QWidget* widget = new QWidget(this);
QVBoxLayout* layout = new QVBoxLayout(widget);
layout->setContentsMargins(0, 0, 0, 0);
m_glWidget = new PointCloudGLWidget(widget);
m_glWidget->setMinimumSize(400, 300); // 设置最小尺寸
m_glWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
layout->addWidget(m_glWidget);
// 连接信号
connect(m_glWidget, &PointCloudGLWidget::pointSelected,
this, &CloudViewMainWindow::onPointSelected);
connect(m_glWidget, &PointCloudGLWidget::twoPointsSelected,
this, &CloudViewMainWindow::onTwoPointsSelected);
connect(m_glWidget, &PointCloudGLWidget::lineSelected,
this, &CloudViewMainWindow::onLineSelected);
connect(m_glWidget, &PointCloudGLWidget::viewAnglesChanged,
this, &CloudViewMainWindow::onViewAnglesChanged);
return widget;
}
QWidget* CloudViewMainWindow::createControlPanel()
{
QWidget* widget = new QWidget(this);
widget->setMaximumWidth(350);
QVBoxLayout* layout = new QVBoxLayout(widget);
layout->setContentsMargins(5, 5, 5, 5);
layout->setSpacing(5);
// 文件操作组
layout->addWidget(createFileGroup());
// 视图方向组
layout->addWidget(createViewGroup());
// 创建 Tab 控件
QTabWidget* tabWidget = new QTabWidget(widget);
tabWidget->addTab(createMeasurePage(), "选点测距");
tabWidget->addTab(createLinePage(), "选线");
tabWidget->addTab(createTransformPage(), "矩阵变换");
layout->addWidget(tabWidget);
// 点云列表组
layout->addWidget(createCloudListGroup());
// 添加弹性空间
layout->addStretch();
return widget;
}
QGroupBox* CloudViewMainWindow::createFileGroup()
{
QGroupBox* group = new QGroupBox("文件操作", this);
group->setMaximumWidth(350);
QVBoxLayout* layout = new QVBoxLayout(group);
layout->setSpacing(3);
layout->setContentsMargins(5, 5, 5, 5);
2026-02-22 16:54:45 +08:00
m_btnOpenFile = new QPushButton("打开文件", group);
2026-02-19 00:45:00 +08:00
m_btnOpenFile->setMinimumHeight(24);
m_btnOpenFile->setMaximumHeight(24);
connect(m_btnOpenFile, &QPushButton::clicked, this, &CloudViewMainWindow::onOpenFile);
layout->addWidget(m_btnOpenFile);
m_btnOpenPose = new QPushButton("打开姿态点 {x,y,z}-{r,p,y}", group);
m_btnOpenPose->setMinimumHeight(24);
m_btnOpenPose->setMaximumHeight(24);
connect(m_btnOpenPose, &QPushButton::clicked, this, &CloudViewMainWindow::onOpenPoseFile);
layout->addWidget(m_btnOpenPose);
m_btnClearAll = new QPushButton("清除所有", group);
m_btnClearAll->setMinimumHeight(24);
m_btnClearAll->setMaximumHeight(24);
connect(m_btnClearAll, &QPushButton::clicked, this, &CloudViewMainWindow::onClearAll);
layout->addWidget(m_btnClearAll);
return group;
}
QGroupBox* CloudViewMainWindow::createViewGroup()
{
QGroupBox* group = new QGroupBox("视图方向", this);
group->setMaximumWidth(350);
QVBoxLayout* mainLayout = new QVBoxLayout(group);
mainLayout->setSpacing(4);
mainLayout->setContentsMargins(5, 5, 5, 5);
// 视角预设按钮,使用水平布局
struct ViewPreset {
const char* name;
float rotX;
float rotY;
float rotZ;
};
ViewPreset presets[] = {
{"正视", 0.0f, 0.0f, 0.0f},
{"后视", 0.0f, 180.0f, 0.0f},
{"左侧", 0.0f, -90.0f, 0.0f},
{"右侧", 0.0f, 90.0f, 0.0f},
{"俯视", -90.0f, 0.0f, 0.0f},
{"仰视", 90.0f, 0.0f, 0.0f},
};
QHBoxLayout* btnLayout = new QHBoxLayout();
btnLayout->setSpacing(3);
for (const auto& preset : presets) {
QPushButton* btn = new QPushButton(preset.name, group);
btn->setMinimumHeight(24);
btn->setMaximumHeight(24);
float rx = preset.rotX;
float ry = preset.rotY;
float rz = preset.rotZ;
connect(btn, &QPushButton::clicked, this, [this, rx, ry, rz]() {
m_glWidget->setViewAngles(rx, ry, rz);
// 更新显示的角度值
m_editRotX->setText(QString::number(rx, 'f', 1));
m_editRotY->setText(QString::number(ry, 'f', 1));
m_editRotZ->setText(QString::number(rz, 'f', 1));
});
btnLayout->addWidget(btn);
}
mainLayout->addLayout(btnLayout);
QLabel* lblTip2 = new QLabel("左键旋转XYAlt+左键或右键旋转Z轴中键拖动平移", group);
lblTip2->setWordWrap(true);
lblTip2->setStyleSheet("color: gray; font-size: 12px;");
mainLayout->addWidget(lblTip2);
// 旋转角度输入(三行布局)
QGridLayout* angleGrid = new QGridLayout();
angleGrid->setSpacing(5);
angleGrid->setContentsMargins(0, 0, 0, 0);
// RotX
QLabel* lblRotX = new QLabel("RotX:", group);
angleGrid->addWidget(lblRotX, 0, 0);
m_editRotX = new QLineEdit("0.0", group);
m_editRotX->setMaximumWidth(60);
m_editRotX->setMaximumHeight(24);
angleGrid->addWidget(m_editRotX, 0, 1);
// RotY
QLabel* lblRotY = new QLabel("RotY:", group);
angleGrid->addWidget(lblRotY, 0, 2);
m_editRotY = new QLineEdit("0.0", group);
m_editRotY->setMaximumWidth(60);
m_editRotY->setMaximumHeight(24);
angleGrid->addWidget(m_editRotY, 0, 3);
// RotZ
QLabel* lblRotZ = new QLabel("RotZ:", group);
angleGrid->addWidget(lblRotZ, 0, 4);
m_editRotZ = new QLineEdit("0.0", group);
m_editRotZ->setMaximumWidth(60);
m_editRotZ->setMaximumHeight(24);
angleGrid->addWidget(m_editRotZ, 0, 5);
// 应用按钮
QPushButton* btnApply = new QPushButton("应用", group);
btnApply->setMaximumWidth(50);
btnApply->setMaximumHeight(24);
connect(btnApply, &QPushButton::clicked, this, [this]() {
bool okX, okY, okZ;
float rotX = m_editRotX->text().toFloat(&okX);
float rotY = m_editRotY->text().toFloat(&okY);
float rotZ = m_editRotZ->text().toFloat(&okZ);
if (okX && okY && okZ) {
m_glWidget->setViewAngles(rotX, rotY, rotZ);
}
});
angleGrid->addWidget(btnApply, 0, 6);
mainLayout->addLayout(angleGrid);
return group;
}
QWidget* CloudViewMainWindow::createMeasurePage()
{
QWidget* page = new QWidget(this);
QVBoxLayout* layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 5, 0, 0);
layout->addWidget(createMeasureGroup());
layout->addStretch();
return page;
}
QWidget* CloudViewMainWindow::createLinePage()
{
QWidget* page = new QWidget(this);
QVBoxLayout* layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 5, 0, 0);
// 选线拟合组
layout->addWidget(createLineGroup());
// 输入线段组
QGroupBox* inputLineGroup = new QGroupBox("输入线段", page);
QVBoxLayout* inputLayout = new QVBoxLayout(inputLineGroup);
// 提示
QLabel* lblTip = new QLabel("输入两点坐标显示线段", inputLineGroup);
lblTip->setStyleSheet("color: gray; font-size: 10px;");
inputLayout->addWidget(lblTip);
// 点1坐标
QLabel* lblPoint1 = new QLabel("点1:", inputLineGroup);
lblPoint1->setStyleSheet("font-weight: bold;");
inputLayout->addWidget(lblPoint1);
QHBoxLayout* p1Layout = new QHBoxLayout();
p1Layout->addWidget(new QLabel("X:", inputLineGroup));
m_editLineX1 = new QLineEdit("0.0", inputLineGroup);
m_editLineX1->setMaximumWidth(70);
p1Layout->addWidget(m_editLineX1);
p1Layout->addWidget(new QLabel("Y:", inputLineGroup));
m_editLineY1 = new QLineEdit("0.0", inputLineGroup);
m_editLineY1->setMaximumWidth(70);
p1Layout->addWidget(m_editLineY1);
p1Layout->addWidget(new QLabel("Z:", inputLineGroup));
m_editLineZ1 = new QLineEdit("0.0", inputLineGroup);
m_editLineZ1->setMaximumWidth(70);
p1Layout->addWidget(m_editLineZ1);
inputLayout->addLayout(p1Layout);
// 点2坐标
QLabel* lblPoint2 = new QLabel("点2:", inputLineGroup);
lblPoint2->setStyleSheet("font-weight: bold;");
inputLayout->addWidget(lblPoint2);
QHBoxLayout* p2Layout = new QHBoxLayout();
p2Layout->addWidget(new QLabel("X:", inputLineGroup));
m_editLineX2 = new QLineEdit("100.0", inputLineGroup);
m_editLineX2->setMaximumWidth(70);
p2Layout->addWidget(m_editLineX2);
p2Layout->addWidget(new QLabel("Y:", inputLineGroup));
m_editLineY2 = new QLineEdit("100.0", inputLineGroup);
m_editLineY2->setMaximumWidth(70);
p2Layout->addWidget(m_editLineY2);
p2Layout->addWidget(new QLabel("Z:", inputLineGroup));
m_editLineZ2 = new QLineEdit("100.0", inputLineGroup);
m_editLineZ2->setMaximumWidth(70);
p2Layout->addWidget(m_editLineZ2);
inputLayout->addLayout(p2Layout);
// 按钮
QHBoxLayout* btnLayout = new QHBoxLayout();
m_btnShowLine = new QPushButton("显示线段", inputLineGroup);
connect(m_btnShowLine, &QPushButton::clicked, this, &CloudViewMainWindow::onShowInputLine);
btnLayout->addWidget(m_btnShowLine);
m_btnClearLine2 = new QPushButton("清除线段", inputLineGroup);
connect(m_btnClearLine2, &QPushButton::clicked, this, &CloudViewMainWindow::onClearInputLine);
btnLayout->addWidget(m_btnClearLine2);
inputLayout->addLayout(btnLayout);
layout->addWidget(inputLineGroup);
layout->addStretch();
return page;
}
QGroupBox* CloudViewMainWindow::createMeasureGroup()
{
QGroupBox* group = new QGroupBox("选点测距", this);
group->setMaximumWidth(350);
QVBoxLayout* layout = new QVBoxLayout(group);
layout->setSpacing(4);
layout->setContentsMargins(5, 5, 5, 5);
// 操作说明
QLabel* lblTip = new QLabel("Ctrl+左键点击点云选择点", group);
lblTip->setWordWrap(true);
lblTip->setStyleSheet("color: gray; font-size: 12px;");
layout->addWidget(lblTip);
// 测距复选框
m_cbMeasureDistance = new QCheckBox("启用测距", group);
m_cbMeasureDistance->setChecked(false);
connect(m_cbMeasureDistance, &QCheckBox::toggled, this, [this](bool checked) {
m_glWidget->setMeasureDistanceEnabled(checked);
m_glWidget->clearSelectedPoints();
m_lblPoint1->setText("点1: --");
m_lblPoint2->setText("点2: --");
m_lblDistance->setText("--");
});
layout->addWidget(m_cbMeasureDistance);
// 清除选点按钮
m_btnClearPoints = new QPushButton("清除选点", group);
m_btnClearPoints->setMinimumHeight(24);
connect(m_btnClearPoints, &QPushButton::clicked, this, &CloudViewMainWindow::onClearSelectedPoints);
layout->addWidget(m_btnClearPoints);
// 分隔线
QFrame* line1 = new QFrame(group);
line1->setFrameShape(QFrame::HLine);
line1->setFrameShadow(QFrame::Sunken);
layout->addWidget(line1);
// 点1信息坐标直接显示在标题后
m_lblPoint1 = new QLabel("点1: --", group);
m_lblPoint1->setWordWrap(true);
m_lblPoint1->setStyleSheet("font-weight: bold; font-size: 11px;");
layout->addWidget(m_lblPoint1);
// 点1姿态输入紧凑布局
QHBoxLayout* pose1Layout = new QHBoxLayout();
pose1Layout->setSpacing(5);
pose1Layout->addWidget(new QLabel("RX:", group));
m_editRx1 = new QLineEdit("0.0", group);
m_editRx1->setMaximumWidth(50);
pose1Layout->addWidget(m_editRx1);
pose1Layout->addWidget(new QLabel("RY:", group));
m_editRy1 = new QLineEdit("0.0", group);
m_editRy1->setMaximumWidth(50);
pose1Layout->addWidget(m_editRy1);
pose1Layout->addWidget(new QLabel("RZ:", group));
m_editRz1 = new QLineEdit("0.0", group);
m_editRz1->setMaximumWidth(50);
pose1Layout->addWidget(m_editRz1);
pose1Layout->addStretch();
layout->addLayout(pose1Layout);
m_btnShowPose1 = new QPushButton("显示点1姿态", group);
m_btnShowPose1->setMinimumHeight(24);
connect(m_btnShowPose1, &QPushButton::clicked, this, &CloudViewMainWindow::onShowPose1);
layout->addWidget(m_btnShowPose1);
// 分隔线
QFrame* line2 = new QFrame(group);
line2->setFrameShape(QFrame::HLine);
line2->setFrameShadow(QFrame::Sunken);
layout->addWidget(line2);
// 点2信息坐标直接显示在标题后
m_lblPoint2 = new QLabel("点2: --", group);
m_lblPoint2->setWordWrap(true);
m_lblPoint2->setStyleSheet("font-weight: bold; font-size: 11px;");
layout->addWidget(m_lblPoint2);
// 点2姿态输入紧凑布局
QHBoxLayout* pose2Layout = new QHBoxLayout();
pose2Layout->setSpacing(5);
pose2Layout->addWidget(new QLabel("RX:", group));
m_editRx2 = new QLineEdit("0.0", group);
m_editRx2->setMaximumWidth(50);
pose2Layout->addWidget(m_editRx2);
pose2Layout->addWidget(new QLabel("RY:", group));
m_editRy2 = new QLineEdit("0.0", group);
m_editRy2->setMaximumWidth(50);
pose2Layout->addWidget(m_editRy2);
pose2Layout->addWidget(new QLabel("RZ:", group));
m_editRz2 = new QLineEdit("0.0", group);
m_editRz2->setMaximumWidth(50);
pose2Layout->addWidget(m_editRz2);
pose2Layout->addStretch();
layout->addLayout(pose2Layout);
m_btnShowPose2 = new QPushButton("显示点2姿态", group);
m_btnShowPose2->setMinimumHeight(24);
connect(m_btnShowPose2, &QPushButton::clicked, this, &CloudViewMainWindow::onShowPose2);
layout->addWidget(m_btnShowPose2);
// 分隔线
QFrame* line3 = new QFrame(group);
line3->setFrameShape(QFrame::HLine);
line3->setFrameShadow(QFrame::Sunken);
layout->addWidget(line3);
// 欧拉角旋转顺序选择
QLabel* lblEulerOrder = new QLabel("欧拉角旋转顺序:", group);
lblEulerOrder->setStyleSheet("font-weight: bold; font-size: 10px;");
layout->addWidget(lblEulerOrder);
m_comboEulerOrder = new QComboBox(group);
m_comboEulerOrder->addItem("ZYX (Yaw-Pitch-Roll)", static_cast<int>(EulerRotationOrder::ZYX));
m_comboEulerOrder->addItem("XYZ (Roll-Pitch-Yaw)", static_cast<int>(EulerRotationOrder::XYZ));
m_comboEulerOrder->addItem("ZXY (Yaw-Roll-Pitch)", static_cast<int>(EulerRotationOrder::ZXY));
m_comboEulerOrder->addItem("YXZ (Pitch-Roll-Yaw)", static_cast<int>(EulerRotationOrder::YXZ));
m_comboEulerOrder->addItem("XZY (Roll-Yaw-Pitch)", static_cast<int>(EulerRotationOrder::XZY));
m_comboEulerOrder->addItem("YZX (Pitch-Yaw-Roll)", static_cast<int>(EulerRotationOrder::YZX));
m_comboEulerOrder->setCurrentIndex(0);
m_comboEulerOrder->setMaximumHeight(24);
connect(m_comboEulerOrder, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &CloudViewMainWindow::onEulerOrderChanged);
layout->addWidget(m_comboEulerOrder);
// 分隔线
QFrame* line4 = new QFrame(group);
line4->setFrameShape(QFrame::HLine);
line4->setFrameShadow(QFrame::Sunken);
layout->addWidget(line4);
// 距离信息
QHBoxLayout* distLayout = new QHBoxLayout();
QLabel* lblDistTitle = new QLabel("距离:", group);
lblDistTitle->setStyleSheet("font-size: 10px;");
m_lblDistance = new QLabel("--", group);
m_lblDistance->setStyleSheet("font-weight: bold; color: green; font-size: 10px;");
distLayout->addWidget(lblDistTitle);
distLayout->addWidget(m_lblDistance, 1);
layout->addLayout(distLayout);
return group;
}
QGroupBox* CloudViewMainWindow::createLineGroup()
{
QGroupBox* group = new QGroupBox("选线", this);
group->setMaximumWidth(400);
QVBoxLayout* layout = new QVBoxLayout(group);
layout->setSpacing(3);
layout->setContentsMargins(5, 5, 5, 5);
// 操作说明
QLabel* lblTip = new QLabel("Shift+左键点击点云选择线", group);
lblTip->setWordWrap(true);
lblTip->setStyleSheet("color: gray; font-size: 9px;");
layout->addWidget(lblTip);
// 选线模式选择
QHBoxLayout* modeLayout = new QHBoxLayout();
modeLayout->setSpacing(5);
m_rbVertical = new QRadioButton("纵向", group);
m_rbHorizontal = new QRadioButton("横向", group);
m_rbVertical->setChecked(true);
connect(m_rbVertical, &QRadioButton::toggled, this, &CloudViewMainWindow::onLineSelectModeChanged);
modeLayout->addWidget(m_rbVertical);
modeLayout->addWidget(m_rbHorizontal);
modeLayout->addStretch();
layout->addLayout(modeLayout);
// 输入索引选择
QHBoxLayout* inputLayout = new QHBoxLayout();
inputLayout->setSpacing(5);
m_lineNumberInput = new QLineEdit(group);
m_lineNumberInput->setPlaceholderText("输入索引");
m_lineNumberInput->setMaximumHeight(24);
m_btnSelectByNumber = new QPushButton("选择", group);
m_btnSelectByNumber->setMaximumHeight(24);
m_btnSelectByNumber->setMaximumWidth(50);
connect(m_btnSelectByNumber, &QPushButton::clicked, this, &CloudViewMainWindow::onSelectLineByNumber);
2026-02-22 16:54:45 +08:00
connect(m_lineNumberInput, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onSelectLineByNumber);
2026-02-19 00:45:00 +08:00
inputLayout->addWidget(m_lineNumberInput, 1);
inputLayout->addWidget(m_btnSelectByNumber);
layout->addLayout(inputLayout);
// 清除选线按钮
m_btnClearLine = new QPushButton("清除选线", group);
m_btnClearLine->setMinimumHeight(24);
connect(m_btnClearLine, &QPushButton::clicked, this, &CloudViewMainWindow::onClearLinePoints);
layout->addWidget(m_btnClearLine);
// 显示线上点按钮
m_btnShowLinePoints = new QPushButton("显示线上点", group);
m_btnShowLinePoints->setMinimumHeight(24);
connect(m_btnShowLinePoints, &QPushButton::clicked, this, &CloudViewMainWindow::onShowLinePoints);
layout->addWidget(m_btnShowLinePoints);
// 线索引信息
QHBoxLayout* indexLayout = new QHBoxLayout();
QLabel* lblIndexTitle = new QLabel("索引:", group);
lblIndexTitle->setStyleSheet("font-size: 10px;");
m_lblLineIndex = new QLabel("--", group);
m_lblLineIndex->setStyleSheet("font-size: 10px;");
indexLayout->addWidget(lblIndexTitle);
indexLayout->addWidget(m_lblLineIndex, 1);
layout->addLayout(indexLayout);
// 点数信息
QHBoxLayout* countLayout = new QHBoxLayout();
QLabel* lblCountTitle = new QLabel("点数:", group);
lblCountTitle->setStyleSheet("font-size: 10px;");
m_lblLinePointCount = new QLabel("--", group);
m_lblLinePointCount->setStyleSheet("font-size: 10px;");
countLayout->addWidget(lblCountTitle);
countLayout->addWidget(m_lblLinePointCount, 1);
layout->addLayout(countLayout);
return group;
}
QGroupBox* CloudViewMainWindow::createCloudListGroup()
{
QGroupBox* group = new QGroupBox("点云列表", this);
group->setMaximumWidth(350);
QVBoxLayout* layout = new QVBoxLayout(group);
layout->setSpacing(3);
layout->setContentsMargins(5, 5, 5, 5);
m_cloudList = new QListWidget(group);
m_cloudList->setMinimumHeight(70);
layout->addWidget(m_cloudList);
return group;
}
void CloudViewMainWindow::onOpenFile()
{
2026-02-22 16:54:45 +08:00
// 如果已有打开的文件,提示是否清除
if (m_cloudList->count() > 0) {
auto ret = QMessageBox::question(this, "提示",
"当前已有打开的文件,是否清除?",
QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel);
if (ret == QMessageBox::Cancel) {
return;
}
if (ret == QMessageBox::Yes) {
onClearAll();
}
}
2026-02-19 00:45:00 +08:00
QString fileName = QFileDialog::getOpenFileName(
this,
2026-02-22 16:54:45 +08:00
"打开文件",
2026-02-19 00:45:00 +08:00
QString(),
2026-02-22 16:54:45 +08:00
"所有支持格式 (*.pcd *.txt);;PCD 文件 (*.pcd);;TXT 文件 (*.txt);;所有文件 (*.*)"
2026-02-19 00:45:00 +08:00
);
if (fileName.isEmpty()) {
return;
}
2026-02-22 16:54:45 +08:00
// 同一文件中可能同时包含点云和线段,两者都尝试加载
bool cloudOk = loadPointCloudFile(fileName);
bool segmentOk = loadSegmentFile(fileName);
if (!cloudOk && !segmentOk) {
QMessageBox::critical(this, "错误", "无法从文件中加载点云或线段数据");
statusBar()->showMessage("加载失败");
}
}
bool CloudViewMainWindow::loadPointCloudFile(const QString& fileName)
{
2026-02-19 00:45:00 +08:00
statusBar()->showMessage("正在加载点云...");
QFileInfo fileInfo(fileName);
QString ext = fileInfo.suffix().toLower();
QString cloudName = QString("Cloud_%1 (%2)").arg(++m_cloudCount).arg(fileInfo.fileName());
// 统一使用 PointCloudXYZRGB 加载,支持带颜色和不带颜色的文件
PointCloudXYZRGB rgbCloud;
int result = m_converter->loadFromFile(fileName.toStdString(), rgbCloud);
if (result != 0) {
2026-02-22 16:54:45 +08:00
LOG_INFO("[CloudView] Load point cloud failed: %s\n", m_converter->getLastError().c_str());
return false;
2026-02-19 00:45:00 +08:00
}
// 保存原始完整点云 XYZ用于旋转/线上点等功能)
m_originalCloud.clear();
m_originalCloud.reserve(rgbCloud.size());
for (size_t i = 0; i < rgbCloud.points.size(); ++i) {
const auto& pt = rgbCloud.points[i];
Point3D xyzPt(pt.x, pt.y, pt.z);
int lineIdx = (i < rgbCloud.lineIndices.size()) ? rgbCloud.lineIndices[i] : 0;
m_originalCloud.push_back(xyzPt, lineIdx);
}
// 根据是否有颜色选择显示方式
bool hadColor = m_converter->lastLoadHadColor();
// PCD 文件:检查是否有非白色的颜色数据来判断
if (ext == "pcd") {
hadColor = false;
for (size_t i = 0; i < rgbCloud.points.size(); ++i) {
const auto& pt = rgbCloud.points[i];
if (pt.r != 255 || pt.g != 255 || pt.b != 255) {
hadColor = true;
break;
}
}
}
if (hadColor) {
m_glWidget->addPointCloud(rgbCloud, cloudName);
LOG_INFO("[CloudView] Loaded with original color, points: %zu\n", rgbCloud.size());
} else {
m_glWidget->addPointCloud(m_originalCloud, cloudName);
LOG_INFO("[CloudView] Loaded without color (color table), points: %zu\n", m_originalCloud.size());
}
// 保存线信息(用于旋转功能)
int lineCount = m_converter->getLoadedLineCount();
if (lineCount > 0) {
m_currentLineNum = lineCount;
m_currentLinePtNum = static_cast<int>(m_converter->getLoadedPointCount()) / lineCount;
} else {
m_currentLineNum = 0;
m_currentLinePtNum = 0;
}
// 添加到列表
QString itemText;
if (lineCount > 0) {
itemText = QString("%1 - %2 点, %3 线").arg(cloudName).arg(m_converter->getLoadedPointCount()).arg(lineCount);
} else {
itemText = QString("%1 - %2 点").arg(cloudName).arg(m_converter->getLoadedPointCount());
}
if (hadColor) {
itemText += " [彩色]";
}
m_cloudList->addItem(itemText);
statusBar()->showMessage(QString("已加载 %1 个点%2").arg(m_converter->getLoadedPointCount()).arg(hadColor ? " (彩色)" : ""));
2026-02-22 16:54:45 +08:00
return true;
2026-02-19 00:45:00 +08:00
}
2026-02-22 16:54:45 +08:00
bool CloudViewMainWindow::loadSegmentFile(const QString& fileName)
2026-02-19 00:45:00 +08:00
{
2026-02-22 16:54:45 +08:00
statusBar()->showMessage("正在加载线段...");
2026-02-19 00:45:00 +08:00
2026-02-22 16:54:45 +08:00
LaserDataLoader loader;
std::vector<std::vector<SVzNLPointXYZRGBA>> polyLines;
int result = loader.LoadPolySegments(fileName.toStdString(), polyLines);
2026-02-19 00:45:00 +08:00
2026-02-22 16:54:45 +08:00
if (result != 0) {
LOG_INFO("[CloudView] Load segments failed: %s\n", loader.GetLastError().c_str());
return false;
}
2026-02-19 00:45:00 +08:00
2026-02-22 16:54:45 +08:00
if (polyLines.empty()) {
return false;
2026-02-19 00:45:00 +08:00
}
2026-02-22 16:54:45 +08:00
// 将折线点转换为线段(使用点自带的颜色)
2026-02-19 00:45:00 +08:00
QVector<LineSegment> segments;
2026-02-22 16:54:45 +08:00
for (const auto& polyLine : polyLines) {
for (size_t i = 0; i + 1 < polyLine.size(); ++i) {
const auto& p1 = polyLine[i];
const auto& p2 = polyLine[i + 1];
// 取起点颜色作为线段颜色nRGB 为 BGR 打包格式
float r = ((p1.nRGB >> 0) & 0xFF) / 255.0f;
float g = ((p1.nRGB >> 8) & 0xFF) / 255.0f;
float b = ((p1.nRGB >> 16) & 0xFF) / 255.0f;
segments.append(LineSegment(
p1.x, p1.y, p1.z,
p2.x, p2.y, p2.z,
r, g, b));
2026-02-19 00:45:00 +08:00
}
}
if (segments.isEmpty()) {
2026-02-22 16:54:45 +08:00
return false;
2026-02-19 00:45:00 +08:00
}
m_glWidget->addLineSegments(segments);
2026-02-22 16:54:45 +08:00
// 添加到列表
QFileInfo fileInfo(fileName);
int totalPolyCount = static_cast<int>(polyLines.size());
QString itemName = QString("Segments (%1)").arg(fileInfo.fileName());
m_cloudList->addItem(QString("%1 - %2 条折线, %3 条线段")
.arg(itemName).arg(totalPolyCount).arg(segments.size()));
statusBar()->showMessage(QString("已加载 %1 条折线, %2 条线段").arg(totalPolyCount).arg(segments.size()));
LOG_INFO("[CloudView] Loaded %d polylines, %d segments from %s\n",
totalPolyCount, segments.size(), fileName.toStdString().c_str());
return true;
2026-02-19 00:45:00 +08:00
}
void CloudViewMainWindow::onOpenPoseFile()
{
QString fileName = QFileDialog::getOpenFileName(
this,
"打开姿态点文件",
QString(),
"文本文件 (*.txt);;所有文件 (*.*)"
);
if (fileName.isEmpty()) {
return;
}
statusBar()->showMessage("正在加载姿态点...");
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QMessageBox::critical(this, "错误", QString("无法打开文件: %1").arg(fileName));
statusBar()->showMessage("加载失败");
return;
}
QVector<PosePoint> poses;
QTextStream in(&file);
int lineNum = 0;
int validCount = 0;
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
lineNum++;
// 跳过空行和注释
if (line.isEmpty() || line.startsWith('#')) {
continue;
}
// 解析格式:{x,y,z}-{r,p,y}
QRegExp regex("\\{([^}]+)\\}-\\{([^}]+)\\}");
if (regex.indexIn(line) == -1) {
LOG_WARN("[CloudView] Line %d: Invalid format, expected {x,y,z}-{r,p,y}\n", lineNum);
continue;
}
QString posStr = regex.cap(1);
QString rotStr = regex.cap(2);
QStringList pos = posStr.split(',');
QStringList rot = rotStr.split(',');
if (pos.size() != 3 || rot.size() != 3) {
LOG_WARN("[CloudView] Line %d: Invalid point format\n", lineNum);
continue;
}
bool ok = true;
float x = pos[0].toFloat(&ok); if (!ok) continue;
float y = pos[1].toFloat(&ok); if (!ok) continue;
float z = pos[2].toFloat(&ok); if (!ok) continue;
float roll = rot[0].toFloat(&ok); if (!ok) continue;
float pitch = rot[1].toFloat(&ok); if (!ok) continue;
float yaw = rot[2].toFloat(&ok); if (!ok) continue;
// 固定大小为10
float scale = 10.0f;
poses.append(PosePoint(x, y, z, roll, pitch, yaw, scale));
validCount++;
}
file.close();
if (poses.isEmpty()) {
QMessageBox::warning(this, "警告", "文件中没有有效的姿态点数据");
statusBar()->showMessage("加载失败");
return;
}
m_glWidget->addPosePoints(poses);
statusBar()->showMessage(QString("已加载 %1 个姿态点").arg(validCount));
LOG_INFO("[CloudView] Loaded %d pose points from %s\n", validCount, fileName.toStdString().c_str());
}
void CloudViewMainWindow::onClearAll()
{
m_glWidget->clearPointClouds();
2026-02-22 16:54:45 +08:00
m_glWidget->clearLineSegments();
m_glWidget->clearPosePoints();
2026-02-19 00:45:00 +08:00
m_cloudList->clear();
m_cloudCount = 0;
m_currentLineNum = 0;
m_currentLinePtNum = 0;
m_originalCloud.clear();
// 清除选点信息
m_lblPoint1->setText("点1: --");
m_lblPoint2->setText("点2: --");
m_lblDistance->setText("--");
// 清除选线信息
m_lblLineIndex->setText("--");
m_lblLinePointCount->setText("--");
statusBar()->showMessage("已清除所有数据");
}
void CloudViewMainWindow::onResetView()
{
m_glWidget->resetView();
statusBar()->showMessage("视图已重置");
}
void CloudViewMainWindow::onClearSelectedPoints()
{
m_glWidget->clearSelectedPoints();
m_glWidget->clearPosePoints(); // 清除选点时也清除姿态
m_lblPoint1->setText("点1: --");
m_lblPoint2->setText("点2: --");
m_lblDistance->setText("--");
statusBar()->showMessage("已清除选中的点");
}
void CloudViewMainWindow::onPointSelected(const SelectedPointInfo& point)
{
if (!point.valid) {
return;
}
// 选择新点时清除之前的姿态显示
m_glWidget->clearPosePoints();
updateSelectedPointsDisplay();
// 状态栏显示:坐标、线号、索引号
QString statusMsg = QString("选中点: (%1, %2, %3)")
.arg(point.x, 0, 'f', 3)
.arg(point.y, 0, 'f', 3)
.arg(point.z, 0, 'f', 3);
if (point.lineIndex >= 0) {
statusMsg += QString(" | 线号: %1").arg(point.lineIndex);
if (point.pointIndexInLine >= 0) {
statusMsg += QString(" | 索引号: %1").arg(point.pointIndexInLine);
}
}
statusBar()->showMessage(statusMsg);
}
void CloudViewMainWindow::onTwoPointsSelected(const SelectedPointInfo& p1, const SelectedPointInfo& p2, float distance)
{
updateSelectedPointsDisplay();
m_lblDistance->setText(QString("%1 mm").arg(distance, 0, 'f', 3));
statusBar()->showMessage(QString("测量距离: %1 mm").arg(distance, 0, 'f', 3));
}
void CloudViewMainWindow::updateSelectedPointsDisplay()
{
auto selectedPoints = m_glWidget->getSelectedPoints();
if (selectedPoints.size() >= 1 && selectedPoints[0].valid) {
QString text;
if (selectedPoints[0].lineIndex >= 0) {
text = QString("点1: 线号:%1 点序:%2 x:%3 y:%4 z:%5")
.arg(selectedPoints[0].lineIndex)
.arg(selectedPoints[0].pointIndexInLine)
.arg(selectedPoints[0].x, 0, 'f', 3)
.arg(selectedPoints[0].y, 0, 'f', 3)
.arg(selectedPoints[0].z, 0, 'f', 3);
} else {
text = QString("点1: x:%1 y:%2 z:%3")
.arg(selectedPoints[0].x, 0, 'f', 3)
.arg(selectedPoints[0].y, 0, 'f', 3)
.arg(selectedPoints[0].z, 0, 'f', 3);
}
m_lblPoint1->setText(text);
} else {
m_lblPoint1->setText("点1: --");
}
if (selectedPoints.size() >= 2 && selectedPoints[1].valid) {
QString text;
if (selectedPoints[1].lineIndex >= 0) {
text = QString("点2: 线号:%1 点序:%2 x:%3 y:%4 z:%5")
.arg(selectedPoints[1].lineIndex)
.arg(selectedPoints[1].pointIndexInLine)
.arg(selectedPoints[1].x, 0, 'f', 3)
.arg(selectedPoints[1].y, 0, 'f', 3)
.arg(selectedPoints[1].z, 0, 'f', 3);
} else {
text = QString("点2: x:%1 y:%2 z:%3")
.arg(selectedPoints[1].x, 0, 'f', 3)
.arg(selectedPoints[1].y, 0, 'f', 3)
.arg(selectedPoints[1].z, 0, 'f', 3);
}
m_lblPoint2->setText(text);
} else {
m_lblPoint2->setText("点2: --");
}
}
void CloudViewMainWindow::onLineSelectModeChanged(bool checked)
{
if (checked) {
// 纵向模式
m_glWidget->setLineSelectMode(LineSelectMode::Vertical);
} else {
// 横向模式
m_glWidget->setLineSelectMode(LineSelectMode::Horizontal);
}
m_lineNumberInput->setPlaceholderText("输入索引");
}
void CloudViewMainWindow::onClearLinePoints()
{
m_glWidget->clearSelectedLine();
m_glWidget->clearListHighlightPoint(); // 清除列表选中的高亮点
m_lblLineIndex->setText("--");
m_lblLinePointCount->setText("--");
statusBar()->showMessage("已清除选线");
}
void CloudViewMainWindow::onLineSelected(const SelectedLineInfo& line)
{
// 重新选线时清除列表高亮点
m_glWidget->clearListHighlightPoint();
if (!line.valid) {
m_lblLineIndex->setText("--");
m_lblLinePointCount->setText("--");
return;
}
// 状态栏显示:线号/索引号、线点数
if (line.mode == LineSelectMode::Vertical) {
m_lblLineIndex->setText(QString::number(line.lineIndex));
statusBar()->showMessage(QString("选中线 | 线号: %1 | 线点数: %2")
.arg(line.lineIndex)
.arg(line.pointCount));
} else {
// 横向选线:显示索引号
m_lblLineIndex->setText(QString::number(line.pointIndex));
statusBar()->showMessage(QString("选中横向线 | 索引号: %1 | 线点数: %2")
.arg(line.pointIndex)
.arg(line.pointCount));
}
m_lblLinePointCount->setText(QString::number(line.pointCount));
// 如果线上点对话框已打开,刷新内容
updateLinePointsDialog();
}
void CloudViewMainWindow::onSelectLineByNumber()
{
if (m_glWidget->getCloudCount() == 0) {
QMessageBox::warning(this, "提示", "请先加载点云");
return;
}
QString text = m_lineNumberInput->text().trimmed();
if (text.isEmpty()) {
QMessageBox::warning(this, "提示", "请输入索引");
return;
}
bool ok;
int index = text.toInt(&ok);
if (!ok || index < 0) {
QMessageBox::warning(this, "提示", "请输入有效的索引(非负整数)");
return;
}
bool success = false;
if (m_rbVertical->isChecked()) {
// 纵向选线:直接使用索引
success = m_glWidget->selectLineByIndex(index);
if (!success) {
QMessageBox::warning(this, "提示", QString("索引 %1 不存在").arg(index));
}
} else {
// 横向选线:直接使用索引
success = m_glWidget->selectHorizontalLineByIndex(index);
if (!success) {
QMessageBox::warning(this, "提示", QString("索引 %1 不存在").arg(index));
}
}
}
QVector<QVector3D> CloudViewMainWindow::getOriginalLinePoints(const SelectedLineInfo& lineInfo)
{
QVector<QVector3D> points;
if (!lineInfo.valid || m_originalCloud.empty()) {
return points;
}
if (lineInfo.mode == LineSelectMode::Vertical) {
// 纵向选线:获取同一条扫描线上的所有点
for (size_t i = 0; i < m_originalCloud.points.size(); ++i) {
if (i < m_originalCloud.lineIndices.size() &&
m_originalCloud.lineIndices[i] == lineInfo.lineIndex) {
const auto& pt = m_originalCloud.points[i];
points.append(QVector3D(pt.x, pt.y, pt.z));
}
}
} else {
// 横向选线:获取所有线的相同索引点
if (m_currentLinePtNum > 0 && lineInfo.pointIndex >= 0) {
for (size_t i = 0; i < m_originalCloud.points.size(); ++i) {
int originalIdx = static_cast<int>(i);
if (originalIdx % m_currentLinePtNum == lineInfo.pointIndex) {
const auto& pt = m_originalCloud.points[i];
points.append(QVector3D(pt.x, pt.y, pt.z));
}
}
}
}
return points;
}
void CloudViewMainWindow::updateLinePointsDialog()
{
if (!m_linePointsDialog || !m_linePointsTable) {
return;
}
SelectedLineInfo lineInfo = m_glWidget->getSelectedLine();
if (!lineInfo.valid) {
m_linePointsTable->setRowCount(0);
m_linePointsDialog->setWindowTitle("线上点坐标");
m_currentLinePoints.clear();
return;
}
// 从原始数据获取线上点包含0,0,0
m_currentLinePoints = getOriginalLinePoints(lineInfo);
// 更新标题
QString title;
if (lineInfo.mode == LineSelectMode::Vertical) {
title = QString("线上点坐标 - 线号: %1 (共 %2 个点)")
.arg(lineInfo.lineIndex)
.arg(m_currentLinePoints.size());
} else {
title = QString("线上点坐标 - 索引号: %1 (共 %2 个点)")
.arg(lineInfo.pointIndex)
.arg(m_currentLinePoints.size());
}
m_linePointsDialog->setWindowTitle(title);
// 更新表格
m_linePointsTable->setRowCount(m_currentLinePoints.size());
// 斑马线颜色
QColor evenColor(245, 245, 245); // 浅灰色
QColor oddColor(255, 255, 255); // 白色
for (int i = 0; i < m_currentLinePoints.size(); ++i) {
const QVector3D& pt = m_currentLinePoints[i];
QColor rowColor = (i % 2 == 0) ? evenColor : oddColor;
// 序号
QTableWidgetItem* indexItem = new QTableWidgetItem(QString::number(i));
indexItem->setTextAlignment(Qt::AlignCenter);
indexItem->setBackground(rowColor);
indexItem->setFlags(indexItem->flags() & ~Qt::ItemIsEditable);
m_linePointsTable->setItem(i, 0, indexItem);
// X
QTableWidgetItem* xItem = new QTableWidgetItem(QString::number(pt.x(), 'f', 3));
xItem->setTextAlignment(Qt::AlignCenter);
xItem->setBackground(rowColor);
xItem->setFlags(xItem->flags() & ~Qt::ItemIsEditable);
m_linePointsTable->setItem(i, 1, xItem);
// Y
QTableWidgetItem* yItem = new QTableWidgetItem(QString::number(pt.y(), 'f', 3));
yItem->setTextAlignment(Qt::AlignCenter);
yItem->setBackground(rowColor);
yItem->setFlags(yItem->flags() & ~Qt::ItemIsEditable);
m_linePointsTable->setItem(i, 2, yItem);
// Z
QTableWidgetItem* zItem = new QTableWidgetItem(QString::number(pt.z(), 'f', 3));
zItem->setTextAlignment(Qt::AlignCenter);
zItem->setBackground(rowColor);
zItem->setFlags(zItem->flags() & ~Qt::ItemIsEditable);
m_linePointsTable->setItem(i, 3, zItem);
}
}
void CloudViewMainWindow::onShowLinePoints()
{
SelectedLineInfo lineInfo = m_glWidget->getSelectedLine();
if (!lineInfo.valid) {
QMessageBox::warning(this, "提示", "请先选择一条线");
return;
}
// 如果对话框已存在,刷新内容并显示
if (m_linePointsDialog) {
updateLinePointsDialog();
m_linePointsDialog->raise();
m_linePointsDialog->activateWindow();
return;
}
// 创建对话框
m_linePointsDialog = new QDialog(this);
m_linePointsDialog->resize(450, 500);
m_linePointsDialog->setAttribute(Qt::WA_DeleteOnClose);
// 对话框关闭时清理指针
connect(m_linePointsDialog, &QDialog::destroyed, this, [this]() {
m_linePointsDialog = nullptr;
m_linePointsTable = nullptr;
m_currentLinePoints.clear();
m_glWidget->clearListHighlightPoint();
});
QVBoxLayout* layout = new QVBoxLayout(m_linePointsDialog);
// 提示标签
QLabel* lblTip = new QLabel("点击行在3D视图中高亮显示", m_linePointsDialog);
lblTip->setStyleSheet("color: gray; font-size: 10px;");
layout->addWidget(lblTip);
// 创建表格控件
m_linePointsTable = new QTableWidget(m_linePointsDialog);
m_linePointsTable->setColumnCount(4);
m_linePointsTable->setHorizontalHeaderLabels({"序号", "X", "Y", "Z"});
m_linePointsTable->setFont(QFont("Consolas", 9));
m_linePointsTable->setSelectionBehavior(QAbstractItemView::SelectRows);
m_linePointsTable->setSelectionMode(QAbstractItemView::SingleSelection);
m_linePointsTable->verticalHeader()->setVisible(false);
// 设置列宽
m_linePointsTable->setColumnWidth(0, 60); // 序号
m_linePointsTable->setColumnWidth(1, 110); // X
m_linePointsTable->setColumnWidth(2, 110); // Y
m_linePointsTable->setColumnWidth(3, 110); // Z
// 表头样式
m_linePointsTable->horizontalHeader()->setStretchLastSection(true);
m_linePointsTable->horizontalHeader()->setDefaultAlignment(Qt::AlignCenter);
connect(m_linePointsTable, &QTableWidget::cellClicked,
this, &CloudViewMainWindow::onLinePointTableClicked);
layout->addWidget(m_linePointsTable);
// 关闭按钮
QPushButton* btnClose = new QPushButton("关闭", m_linePointsDialog);
connect(btnClose, &QPushButton::clicked, m_linePointsDialog, &QDialog::close);
layout->addWidget(btnClose);
// 填充数据
updateLinePointsDialog();
m_linePointsDialog->show();
}
void CloudViewMainWindow::onLinePointTableClicked(int row, int column)
{
Q_UNUSED(column);
if (row >= 0 && row < m_currentLinePoints.size()) {
const QVector3D& pt = m_currentLinePoints[row];
m_glWidget->setListHighlightPoint(pt);
// 在状态栏显示选中点信息
statusBar()->showMessage(QString("列表选中点 %1: (%2, %3, %4)")
.arg(row)
.arg(pt.x(), 0, 'f', 3)
.arg(pt.y(), 0, 'f', 3)
.arg(pt.z(), 0, 'f', 3));
}
}
void CloudViewMainWindow::onShowPose1()
{
auto selectedPoints = m_glWidget->getSelectedPoints();
if (selectedPoints.isEmpty() || !selectedPoints[0].valid) {
QMessageBox::warning(this, "提示", "请先选择点1Ctrl+左键点击点云)");
return;
}
const auto& point = selectedPoints[0];
// 读取姿态参数
bool ok = true;
float rx = m_editRx1->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点1 RX 值无效");
return;
}
float ry = m_editRy1->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点1 RY 值无效");
return;
}
float rz = m_editRz1->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点1 RZ 值无效");
return;
}
// 固定大小为10
float scale = 10.0f;
// 清除之前的姿态点
m_glWidget->clearPosePoints();
// 创建点1的姿态点
PosePoint pose1(point.x, point.y, point.z, rx, ry, rz, scale);
QVector<PosePoint> poses;
poses.append(pose1);
// 如果点2也存在添加点2的姿态
if (selectedPoints.size() >= 2 && selectedPoints[1].valid) {
const auto& point2 = selectedPoints[1];
float rx2 = m_editRx2->text().toFloat(&ok);
float ry2 = m_editRy2->text().toFloat(&ok);
float rz2 = m_editRz2->text().toFloat(&ok);
if (ok) {
PosePoint pose2(point2.x, point2.y, point2.z, rx2, ry2, rz2, scale);
poses.append(pose2);
}
}
// 添加到显示
m_glWidget->addPosePoints(poses);
statusBar()->showMessage(QString("已显示点1姿态 (%.3f, %.3f, %.3f) 旋转(%.1f°, %.1f°, %.1f°)")
.arg(point.x).arg(point.y).arg(point.z)
.arg(rx).arg(ry).arg(rz));
LOG_INFO("[CloudView] Show pose1 at (%.3f, %.3f, %.3f) with rotation (%.1f, %.1f, %.1f)\n",
point.x, point.y, point.z, rx, ry, rz);
}
void CloudViewMainWindow::onShowPose2()
{
auto selectedPoints = m_glWidget->getSelectedPoints();
if (selectedPoints.size() < 2 || !selectedPoints[1].valid) {
QMessageBox::warning(this, "提示", "请先选择点2启用测距后Ctrl+左键点击第二个点)");
return;
}
const auto& point = selectedPoints[1];
// 读取姿态参数
bool ok = true;
float rx = m_editRx2->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点2 RX 值无效");
return;
}
float ry = m_editRy2->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点2 RY 值无效");
return;
}
float rz = m_editRz2->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点2 RZ 值无效");
return;
}
// 固定大小为10
float scale = 10.0f;
// 清除之前的姿态点
m_glWidget->clearPosePoints();
// 创建点2的姿态点
PosePoint pose2(point.x, point.y, point.z, rx, ry, rz, scale);
QVector<PosePoint> poses;
// 如果点1也存在添加点1的姿态
if (selectedPoints[0].valid) {
const auto& point1 = selectedPoints[0];
float rx1 = m_editRx1->text().toFloat(&ok);
float ry1 = m_editRy1->text().toFloat(&ok);
float rz1 = m_editRz1->text().toFloat(&ok);
if (ok) {
PosePoint pose1(point1.x, point1.y, point1.z, rx1, ry1, rz1, scale);
poses.append(pose1);
}
}
poses.append(pose2);
// 添加到显示
m_glWidget->addPosePoints(poses);
statusBar()->showMessage(QString("已显示点2姿态 (%.3f, %.3f, %.3f) 旋转(%.1f°, %.1f°, %.1f°)")
.arg(point.x).arg(point.y).arg(point.z)
.arg(rx).arg(ry).arg(rz));
LOG_INFO("[CloudView] Show pose2 at (%.3f, %.3f, %.3f) with rotation (%.1f, %.1f, %.1f)\n",
point.x, point.y, point.z, rx, ry, rz);
}
void CloudViewMainWindow::onShowInputLine()
{
// 读取点1坐标
bool ok = true;
float x1 = m_editLineX1->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点1 X 值无效");
return;
}
float y1 = m_editLineY1->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点1 Y 值无效");
return;
}
float z1 = m_editLineZ1->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点1 Z 值无效");
return;
}
// 读取点2坐标
float x2 = m_editLineX2->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点2 X 值无效");
return;
}
float y2 = m_editLineY2->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点2 Y 值无效");
return;
}
float z2 = m_editLineZ2->text().toFloat(&ok);
if (!ok) {
QMessageBox::warning(this, "错误", "点2 Z 值无效");
return;
}
// 清除之前的线段
m_glWidget->clearLineSegments();
// 创建线段(红色)
LineSegment segment(x1, y1, z1, x2, y2, z2, 1.0f, 0.0f, 0.0f);
QVector<LineSegment> segments;
segments.append(segment);
// 添加到显示
m_glWidget->addLineSegments(segments);
// 计算距离
float dx = x2 - x1;
float dy = y2 - y1;
float dz = z2 - z1;
float distance = std::sqrt(dx * dx + dy * dy + dz * dz);
statusBar()->showMessage(QString("已显示线段 (%1,%2,%3) → (%4,%5,%6) 长度: %7")
.arg(x1).arg(y1).arg(z1)
.arg(x2).arg(y2).arg(z2)
.arg(distance));
LOG_INFO("[CloudView] Show input line from (%.3f, %.3f, %.3f) to (%.3f, %.3f, %.3f) length=%.3f\n",
x1, y1, z1, x2, y2, z2, distance);
}
void CloudViewMainWindow::onClearInputLine()
{
m_glWidget->clearLineSegments();
statusBar()->showMessage("已清除输入的线段");
}
QWidget* CloudViewMainWindow::createTransformPage()
{
QWidget* page = new QWidget(this);
QVBoxLayout* layout = new QVBoxLayout(page);
layout->setContentsMargins(0, 5, 0, 0);
QGroupBox* group = new QGroupBox("矩阵变换", page);
group->setMaximumWidth(400);
QVBoxLayout* groupLayout = new QVBoxLayout(group);
// 操作说明
QLabel* lblTip = new QLabel("输入或从文件加载 4x4 变换矩阵,应用到所有点云", group);
lblTip->setWordWrap(true);
lblTip->setStyleSheet("color: gray; font-size: 10px;");
groupLayout->addWidget(lblTip);
// 矩阵编辑区域
QLabel* lblMatrix = new QLabel("变换矩阵 (4x4):", group);
lblMatrix->setStyleSheet("font-weight: bold;");
groupLayout->addWidget(lblMatrix);
m_matrixEdit = new QTextEdit(group);
m_matrixEdit->setFont(QFont("Consolas", 10));
m_matrixEdit->setMinimumHeight(100);
m_matrixEdit->setMaximumHeight(120);
// 初始化为单位矩阵
m_matrixEdit->setPlainText(
"1.0 0.0 0.0 0.0\n"
"0.0 1.0 0.0 0.0\n"
"0.0 0.0 1.0 0.0\n"
"0.0 0.0 0.0 1.0"
);
groupLayout->addWidget(m_matrixEdit);
// 从文件加载按钮
m_btnLoadMatrix = new QPushButton("从文件加载矩阵", group);
m_btnLoadMatrix->setMinimumHeight(30);
connect(m_btnLoadMatrix, &QPushButton::clicked, this, &CloudViewMainWindow::onLoadMatrix);
groupLayout->addWidget(m_btnLoadMatrix);
// 按钮行
QHBoxLayout* btnLayout = new QHBoxLayout();
m_btnApplyMatrix = new QPushButton("应用变换", group);
m_btnApplyMatrix->setMinimumHeight(30);
m_btnApplyMatrix->setStyleSheet("QPushButton { background-color: #4CAF50; color: white; font-weight: bold; }"
"QPushButton:hover { background-color: #45a049; }");
connect(m_btnApplyMatrix, &QPushButton::clicked, this, &CloudViewMainWindow::onApplyMatrix);
btnLayout->addWidget(m_btnApplyMatrix);
m_btnResetMatrix = new QPushButton("重置矩阵", group);
connect(m_btnResetMatrix, &QPushButton::clicked, this, &CloudViewMainWindow::onResetMatrix);
btnLayout->addWidget(m_btnResetMatrix);
groupLayout->addLayout(btnLayout);
// 文件格式说明
QLabel* lblFormat = new QLabel(
"矩阵文件格式4行每行4个数值\n"
"分隔符:空格/Tab/逗号\n"
"#开头的行为注释", group);
lblFormat->setWordWrap(true);
lblFormat->setStyleSheet("color: gray; font-size: 9px;");
groupLayout->addWidget(lblFormat);
layout->addWidget(group);
layout->addStretch();
return page;
}
void CloudViewMainWindow::onLoadMatrix()
{
QString fileName = QFileDialog::getOpenFileName(
this,
"打开矩阵文件",
QString(),
"文本文件 (*.txt);;所有文件 (*.*)"
);
if (fileName.isEmpty()) {
return;
}
QFile file(fileName);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QMessageBox::critical(this, "错误", QString("无法打开文件: %1").arg(fileName));
return;
}
QTextStream in(&file);
QVector<QVector<float>> rows;
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
// 跳过空行和注释
if (line.isEmpty() || line.startsWith('#')) {
continue;
}
// 将逗号替换为空格,统一分隔符
line.replace(',', ' ');
line.replace('\t', ' ');
QStringList parts = line.split(' ', QString::SkipEmptyParts);
QVector<float> row;
bool ok = true;
for (const QString& part : parts) {
float val = part.toFloat(&ok);
if (!ok) break;
row.append(val);
}
if (!ok || row.size() != 4) {
QMessageBox::warning(this, "格式错误",
QString("第 %1 行格式无效需要4个数值").arg(rows.size() + 1));
file.close();
return;
}
rows.append(row);
if (rows.size() == 4) {
break;
}
}
file.close();
if (rows.size() != 4) {
QMessageBox::warning(this, "格式错误",
QString("矩阵需要4行数据当前只有 %1 行").arg(rows.size()));
return;
}
// 将矩阵显示到编辑区域
QString matrixText;
for (int r = 0; r < 4; ++r) {
QStringList vals;
for (int c = 0; c < 4; ++c) {
vals.append(QString::number(static_cast<double>(rows[r][c]), 'f', 6));
}
matrixText += vals.join(" ");
if (r < 3) matrixText += "\n";
}
m_matrixEdit->setPlainText(matrixText);
statusBar()->showMessage(QString("已从 %1 加载矩阵").arg(QFileInfo(fileName).fileName()));
LOG_INFO("[CloudView] Loaded matrix from %s\n", fileName.toStdString().c_str());
}
void CloudViewMainWindow::onApplyMatrix()
{
if (m_glWidget->getCloudCount() == 0) {
QMessageBox::warning(this, "提示", "请先加载点云");
return;
}
// 解析编辑区域中的矩阵
QString text = m_matrixEdit->toPlainText().trimmed();
QStringList lines = text.split('\n', QString::SkipEmptyParts);
QVector<QVector<float>> rows;
for (const QString& line : lines) {
QString cleaned = line.trimmed();
if (cleaned.isEmpty() || cleaned.startsWith('#')) {
continue;
}
cleaned.replace(',', ' ');
cleaned.replace('\t', ' ');
QStringList parts = cleaned.split(' ', QString::SkipEmptyParts);
QVector<float> row;
bool ok = true;
for (const QString& part : parts) {
float val = part.toFloat(&ok);
if (!ok) break;
row.append(val);
}
if (!ok || row.size() != 4) {
QMessageBox::warning(this, "格式错误", "矩阵格式无效需要4行4列数值");
return;
}
rows.append(row);
}
if (rows.size() != 4) {
QMessageBox::warning(this, "格式错误",
QString("矩阵需要4行数据当前 %1 行").arg(rows.size()));
return;
}
// 构造 QMatrix4x4按行优先存储
float values[16];
for (int r = 0; r < 4; ++r) {
for (int c = 0; c < 4; ++c) {
values[r * 4 + c] = rows[r][c];
}
}
QMatrix4x4 matrix(values);
// 检查是否为单位矩阵
if (matrix.isIdentity()) {
QMessageBox::information(this, "提示", "当前矩阵为单位矩阵,无需变换");
return;
}
// 应用变换
m_glWidget->transformAllClouds(matrix);
statusBar()->showMessage("已应用矩阵变换到所有点云");
LOG_INFO("[CloudView] Applied matrix transform to all point clouds\n");
}
void CloudViewMainWindow::onResetMatrix()
{
m_matrixEdit->setPlainText(
"1.0 0.0 0.0 0.0\n"
"0.0 1.0 0.0 0.0\n"
"0.0 0.0 1.0 0.0\n"
"0.0 0.0 0.0 1.0"
);
statusBar()->showMessage("矩阵已重置为单位矩阵");
}
void CloudViewMainWindow::onEulerOrderChanged(int index)
{
if (!m_glWidget) {
return;
}
EulerRotationOrder order = static_cast<EulerRotationOrder>(m_comboEulerOrder->itemData(index).toInt());
m_glWidget->setEulerRotationOrder(order);
// 如果有姿态点,刷新显示
m_glWidget->update();
QString orderName = m_comboEulerOrder->currentText();
statusBar()->showMessage(QString("欧拉角旋转顺序已切换为: %1").arg(orderName));
LOG_INFO("[CloudView] Euler rotation order changed to: %s\n", orderName.toStdString().c_str());
}
void CloudViewMainWindow::onViewAnglesChanged(float rotX, float rotY, float rotZ)
{
// 更新显示的角度值
m_editRotX->setText(QString::number(rotX, 'f', 1));
m_editRotY->setText(QString::number(rotY, 'f', 1));
m_editRotZ->setText(QString::number(rotZ, 'f', 1));
}