#include "CloudViewMainWindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "VrLog.h" CloudViewMainWindow::CloudViewMainWindow(QWidget* parent) : QMainWindow(parent) , m_glWidget(nullptr) , m_converter(std::make_unique()) , 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); m_btnOpenFile = new QPushButton("打开点云", group); m_btnOpenFile->setMinimumHeight(24); m_btnOpenFile->setMaximumHeight(24); connect(m_btnOpenFile, &QPushButton::clicked, this, &CloudViewMainWindow::onOpenFile); layout->addWidget(m_btnOpenFile); m_btnOpenSegment = new QPushButton("打开线段 {x,y,z}-{x,y,z}", group); m_btnOpenSegment->setMinimumHeight(24); m_btnOpenSegment->setMaximumHeight(24); connect(m_btnOpenSegment, &QPushButton::clicked, this, &CloudViewMainWindow::onOpenSegmentFile); layout->addWidget(m_btnOpenSegment); 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("左键旋转XY,Alt+左键或右键旋转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(EulerRotationOrder::ZYX)); m_comboEulerOrder->addItem("XYZ (Roll-Pitch-Yaw)", static_cast(EulerRotationOrder::XYZ)); m_comboEulerOrder->addItem("ZXY (Yaw-Roll-Pitch)", static_cast(EulerRotationOrder::ZXY)); m_comboEulerOrder->addItem("YXZ (Pitch-Roll-Yaw)", static_cast(EulerRotationOrder::YXZ)); m_comboEulerOrder->addItem("XZY (Roll-Yaw-Pitch)", static_cast(EulerRotationOrder::XZY)); m_comboEulerOrder->addItem("YZX (Pitch-Yaw-Roll)", static_cast(EulerRotationOrder::YZX)); m_comboEulerOrder->setCurrentIndex(0); m_comboEulerOrder->setMaximumHeight(24); connect(m_comboEulerOrder, QOverload::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); 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() { QString fileName = QFileDialog::getOpenFileName( this, "打开点云文件", QString(), "点云文件 (*.pcd *.txt);;PCD 文件 (*.pcd);;TXT 文件 (*.txt);;所有文件 (*.*)" ); if (fileName.isEmpty()) { return; } 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) { QMessageBox::critical(this, "错误", QString("加载点云失败: %1").arg(QString::fromStdString(m_converter->getLastError()))); statusBar()->showMessage("加载失败"); return; } // 保存原始完整点云 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) { // 有颜色数据:使用 addPointCloud(PointCloudXYZRGB) 显示原始颜色 m_glWidget->addPointCloud(rgbCloud, cloudName); LOG_INFO("[CloudView] Loaded with original color, points: %zu\n", rgbCloud.size()); } else { // 无颜色数据:使用 addPointCloud(PointCloudXYZ) 显示(颜色表轮换) 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(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 ? " (彩色)" : "")); } void CloudViewMainWindow::onOpenSegmentFile() { 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 segments; 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}-{x,y,z} QRegExp regex("\\{([^}]+)\\}-\\{([^}]+)\\}"); if (regex.indexIn(line) == -1) { LOG_WARN("[CloudView] Line %d: Invalid format, expected {x,y,z}-{x,y,z}\n", lineNum); continue; } QString point1Str = regex.cap(1); QString point2Str = regex.cap(2); QStringList p1 = point1Str.split(','); QStringList p2 = point2Str.split(','); if (p1.size() != 3 || p2.size() != 3) { LOG_WARN("[CloudView] Line %d: Invalid point format\n", lineNum); continue; } bool ok = true; float x1 = p1[0].toFloat(&ok); if (!ok) continue; float y1 = p1[1].toFloat(&ok); if (!ok) continue; float z1 = p1[2].toFloat(&ok); if (!ok) continue; float x2 = p2[0].toFloat(&ok); if (!ok) continue; float y2 = p2[1].toFloat(&ok); if (!ok) continue; float z2 = p2[2].toFloat(&ok); if (!ok) continue; // 默认白色 segments.append(LineSegment(x1, y1, z1, x2, y2, z2, 1.0f, 1.0f, 1.0f)); validCount++; } file.close(); if (segments.isEmpty()) { QMessageBox::warning(this, "警告", "文件中没有有效的线段数据"); statusBar()->showMessage("加载失败"); return; } m_glWidget->addLineSegments(segments); statusBar()->showMessage(QString("已加载 %1 条线段").arg(validCount)); LOG_INFO("[CloudView] Loaded %d line segments from %s\n", validCount, fileName.toStdString().c_str()); } 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 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(); 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 CloudViewMainWindow::getOriginalLinePoints(const SelectedLineInfo& lineInfo) { QVector 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(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, "提示", "请先选择点1(Ctrl+左键点击点云)"); 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 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 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 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> 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 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(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> 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 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(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)); }