#include "CloudViewMainWindow.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "VrLog.h" #include "LaserDataLoader.h" namespace { constexpr double kPi = 3.14159265358979323846; constexpr double kDegToRad = kPi / 180.0; bool parseMatrixText(const QString& text, QMatrix4x4& matrix, QString* errorMessage = nullptr) { 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', ' '); const QStringList parts = cleaned.split(' ', QString::SkipEmptyParts); QVector row; bool ok = true; for (const QString& part : parts) { const float value = part.toFloat(&ok); if (!ok) { break; } row.append(value); } if (!ok || row.size() != 4) { if (errorMessage) { *errorMessage = QString("矩阵格式无效,第 %1 行需要 4 个数值").arg(rows.size() + 1); } return false; } rows.append(row); } if (rows.size() != 4) { if (errorMessage) { *errorMessage = QString("矩阵需要 4 行数值,当前为 %1 行").arg(rows.size()); } return false; } matrix.setToIdentity(); for (int row = 0; row < 4; ++row) { for (int col = 0; col < 4; ++col) { matrix(row, col) = rows[row][col]; } } return true; } QString formatMatrixText(const QMatrix4x4& matrix) { QStringList lines; for (int row = 0; row < 4; ++row) { QStringList values; for (int col = 0; col < 4; ++col) { values.append(QString::number(matrix(row, col), 'f', 6)); } lines.append(values.join(" ")); } return lines.join("\n"); } // 从 EyeHandCalibMatrixInfo.ini 格式加载标定矩阵 // 返回 true 表示成功,matrixList 填充所有找到的矩阵及其描述信息 bool parseCalibIniFile(const QString& fileName, QVector>& matrixList, QString* errorMessage = nullptr) { QSettings settings(fileName, QSettings::IniFormat); const int nExist = settings.value("CommInfo/nExistMatrixNum", 0).toInt(); if (nExist <= 0) { if (errorMessage) *errorMessage = "INI 文件中未找到标定矩阵 (nExistMatrixNum=0)"; return false; } for (int i = 0; i < nExist; ++i) { const QString section = QString("CalibMatrixInfo_%1").arg(i); settings.beginGroup(section); QMatrix4x4 matrix; matrix.setToIdentity(); bool ok = true; for (int idx = 0; idx < 16; ++idx) { const QString key = QString("dCalibMatrix_%1").arg(idx); const double val = settings.value(key, 0.0).toDouble(&ok); if (!ok) break; const int row = idx / 4; const int col = idx % 4; matrix(row, col) = static_cast(val); } if (!ok) { settings.endGroup(); if (errorMessage) *errorMessage = QString("矩阵 %1 的数据格式无效").arg(i); return false; } const int posIdx = settings.value("nCalibPosIdx", i).toInt(); const QString posName = settings.value("sCalibPosName", "").toString(); const QString calibTime = settings.value("sCalibTime", "").toString(); QString desc = QString("矩阵 %1 (位置%2").arg(i).arg(posIdx); if (!posName.isEmpty()) desc += QString(", %1").arg(posName); if (!calibTime.isEmpty()) desc += QString(", %1").arg(calibTime); desc += ")"; settings.endGroup(); matrixList.append(qMakePair(desc, matrix)); } return true; } QMatrix4x4 makeAxisRotationMatrix(char axis, double angleRad) { QMatrix4x4 matrix; matrix.setToIdentity(); const float c = static_cast(std::cos(angleRad)); const float s = static_cast(std::sin(angleRad)); switch (axis) { case 'x': matrix(1, 1) = c; matrix(1, 2) = -s; matrix(2, 1) = s; matrix(2, 2) = c; break; case 'y': matrix(0, 0) = c; matrix(0, 2) = s; matrix(2, 0) = -s; matrix(2, 2) = c; break; case 'z': matrix(0, 0) = c; matrix(0, 1) = -s; matrix(1, 0) = s; matrix(1, 1) = c; break; default: break; } return matrix; } QMatrix4x4 buildRobotPoseMatrix(float x, float y, float z, float rxDeg, float ryDeg, float rzDeg) { const QMatrix4x4 rotation = makeAxisRotationMatrix('z', rzDeg * kDegToRad) * makeAxisRotationMatrix('y', ryDeg * kDegToRad) * makeAxisRotationMatrix('x', rxDeg * kDegToRad); QMatrix4x4 matrix = rotation; matrix(0, 3) = x; matrix(1, 3) = y; matrix(2, 3) = z; matrix(3, 0) = 0.0f; matrix(3, 1) = 0.0f; matrix(3, 2) = 0.0f; matrix(3, 3) = 1.0f; return matrix; } void transformPointCloud(PointCloudXYZ& cloud, const QMatrix4x4& matrix) { for (auto& point : cloud.points) { const QVector3D transformed = matrix.map(QVector3D(point.x, point.y, point.z)); point.x = transformed.x(); point.y = transformed.y(); point.z = transformed.z(); } } double clampValue(double value, double minValue, double maxValue) { return std::max(minValue, std::min(maxValue, value)); } QMatrix4x4 buildEulerRotationMatrix(EulerRotationOrder order, double rxDeg, double ryDeg, double rzDeg) { const QMatrix4x4 Rx = makeAxisRotationMatrix('x', rxDeg * kDegToRad); const QMatrix4x4 Ry = makeAxisRotationMatrix('y', ryDeg * kDegToRad); const QMatrix4x4 Rz = makeAxisRotationMatrix('z', rzDeg * kDegToRad); switch (order) { case EulerRotationOrder::ZYX: return Rz * Ry * Rx; case EulerRotationOrder::XYZ: return Rx * Ry * Rz; case EulerRotationOrder::ZXY: return Rz * Rx * Ry; case EulerRotationOrder::YXZ: return Ry * Rx * Rz; case EulerRotationOrder::XZY: return Rx * Rz * Ry; case EulerRotationOrder::YZX: return Ry * Rz * Rx; } QMatrix4x4 identity; identity.setToIdentity(); return identity; } QVector3D rotationMatrixToEuler(EulerRotationOrder order, const QMatrix4x4& m) { constexpr double kEps = 1e-8; double rx = 0.0; double ry = 0.0; double rz = 0.0; switch (order) { case EulerRotationOrder::ZYX: { const double cy = std::sqrt(m(0, 0) * m(0, 0) + m(1, 0) * m(1, 0)); if (cy > kEps) { rx = std::atan2(m(2, 1), m(2, 2)); ry = std::atan2(-m(2, 0), cy); rz = std::atan2(m(1, 0), m(0, 0)); } else { rx = std::atan2(-m(1, 2), m(1, 1)); ry = std::atan2(-m(2, 0), cy); rz = 0.0; } break; } case EulerRotationOrder::XYZ: { const double cy = std::sqrt(m(0, 0) * m(0, 0) + m(0, 1) * m(0, 1)); if (cy > kEps) { rx = std::atan2(-m(1, 2), m(2, 2)); ry = std::atan2(m(0, 2), cy); rz = std::atan2(-m(0, 1), m(0, 0)); } else { rx = std::atan2(m(2, 1), m(1, 1)); ry = std::atan2(m(0, 2), cy); rz = 0.0; } break; } case EulerRotationOrder::ZXY: { const double cx = std::sqrt(m(2, 0) * m(2, 0) + m(2, 2) * m(2, 2)); if (cx > kEps) { rx = std::atan2(m(2, 1), cx); ry = std::atan2(-m(2, 0), m(2, 2)); rz = std::atan2(-m(0, 1), m(1, 1)); } else { rx = std::atan2(m(2, 1), cx); ry = 0.0; rz = std::atan2(m(1, 0), m(0, 0)); } break; } case EulerRotationOrder::YXZ: { const double cx = std::sqrt(m(1, 0) * m(1, 0) + m(1, 1) * m(1, 1)); if (cx > kEps) { rx = std::atan2(-m(1, 2), cx); ry = std::atan2(m(0, 2), m(2, 2)); rz = std::atan2(m(1, 0), m(1, 1)); } else { rx = std::atan2(-m(1, 2), cx); ry = std::atan2(-m(2, 0), m(0, 0)); rz = 0.0; } break; } case EulerRotationOrder::XZY: { const double cz = std::sqrt(m(0, 0) * m(0, 0) + m(0, 2) * m(0, 2)); if (cz > kEps) { rx = std::atan2(m(2, 1), m(1, 1)); ry = std::atan2(m(0, 2), m(0, 0)); rz = std::atan2(-m(0, 1), cz); } else { rx = std::atan2(-m(1, 2), m(2, 2)); ry = 0.0; rz = std::atan2(-m(0, 1), cz); } break; } case EulerRotationOrder::YZX: { const double cz = std::sqrt(m(1, 1) * m(1, 1) + m(1, 2) * m(1, 2)); if (cz > kEps) { rx = std::atan2(-m(1, 2), m(1, 1)); ry = std::atan2(-m(2, 0), m(0, 0)); rz = std::atan2(m(1, 0), cz); } else { rx = 0.0; ry = std::atan2(m(0, 2), m(2, 2)); rz = std::atan2(m(1, 0), cz); } break; } } return QVector3D(static_cast(rx / kDegToRad), static_cast(ry / kDegToRad), static_cast(rz / kDegToRad)); } PointCloudXYZ generateBoxPointCloud(const QVector3D& center, const QVector3D& size, float spacing) { PointCloudXYZ cloud; const int xSteps = std::max(1, static_cast(std::ceil(size.x() / spacing))); const int ySteps = std::max(1, static_cast(std::ceil(size.y() / spacing))); const int zSteps = std::max(1, static_cast(std::ceil(size.z() / spacing))); const float stepX = size.x() / xSteps; const float stepY = size.y() / ySteps; const float stepZ = size.z() / zSteps; cloud.reserve(static_cast((xSteps + 1) * (ySteps + 1) * (zSteps + 1))); const float minX = center.x() - size.x() * 0.5f; const float minY = center.y() - size.y() * 0.5f; const float minZ = center.z() - size.z() * 0.5f; for (int ix = 0; ix <= xSteps; ++ix) { const bool onXBoundary = (ix == 0 || ix == xSteps); const float x = minX + stepX * ix; for (int iy = 0; iy <= ySteps; ++iy) { const bool onYBoundary = (iy == 0 || iy == ySteps); const float y = minY + stepY * iy; for (int iz = 0; iz <= zSteps; ++iz) { const bool onZBoundary = (iz == 0 || iz == zSteps); if (!(onXBoundary || onYBoundary || onZBoundary)) { continue; } const float z = minZ + stepZ * iz; cloud.push_back(Point3D(x, y, z)); } } } return cloud; } } // namespace 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() { } bool CloudViewMainWindow::eventFilter(QObject* obj, QEvent* event) { if (obj == m_glWidget && event->type() == QEvent::Resize) { auto* resizeEvt = static_cast(event); int w = resizeEvt->size().width(); int h = resizeEvt->size().height(); int btnW = m_btnZoomIn->width(); int btnH = m_btnZoomIn->height(); int margin = 8; int gap = 4; int x = w - btnW - margin; int y = (h - btnH * 2 - gap) / 2; m_btnZoomIn->move(x, y); m_btnZoomOut->move(x, y + btnH + gap); } return QMainWindow::eventFilter(obj, event); } void CloudViewMainWindow::setupUI() { // ── 文件菜单 ── QMenu* fileMenu = menuBar()->addMenu("文件"); QAction* actOpenFile = fileMenu->addAction("打开...\tCtrl+O"); actOpenFile->setShortcut(QKeySequence("Ctrl+O")); connect(actOpenFile, &QAction::triggered, this, &CloudViewMainWindow::onOpenFile); fileMenu->addSeparator(); QAction* actSaveCloud = fileMenu->addAction("保存txt点云...\tCtrl+S"); actSaveCloud->setShortcut(QKeySequence("Ctrl+S")); connect(actSaveCloud, &QAction::triggered, this, &CloudViewMainWindow::onSavePointCloud); QAction* actSavePlyPcd = fileMenu->addAction("保存ply/pcd点云...\tCtrl+Shift+S"); actSavePlyPcd->setShortcut(QKeySequence("Ctrl+Shift+S")); connect(actSavePlyPcd, &QAction::triggered, this, &CloudViewMainWindow::onSavePlyPcdCloud); // ── 点云菜单 ── QMenu* cloudMenu = menuBar()->addMenu("点云"); QAction* actGenerateCloud = cloudMenu->addAction("生成长方体点云..."); connect(actGenerateCloud, &QAction::triggered, this, &CloudViewMainWindow::onGeneratePointCloud); QAction* actRotateByZ = cloudMenu->addAction("点云旋转..."); connect(actRotateByZ, &QAction::triggered, this, &CloudViewMainWindow::onRotateCloudByZ); QMenu* toolMenu = menuBar()->addMenu("工具"); QAction* actEulerPose = toolMenu->addAction("欧拉角与方向向量矩阵互转..."); connect(actEulerPose, &QAction::triggered, this, &CloudViewMainWindow::onConvertEulerMatrix); QAction* actPoseComp = toolMenu->addAction("姿态补偿计算..."); connect(actPoseComp, &QAction::triggered, this, &CloudViewMainWindow::onPoseCompensation); QMenu* transformMenu = menuBar()->addMenu("矩阵变换"); QAction* actEyeToHand = transformMenu->addAction("眼在手外..."); connect(actEyeToHand, &QAction::triggered, this, &CloudViewMainWindow::onEyeToHandTransform); QAction* actEyeInHand = transformMenu->addAction("眼在手上..."); connect(actEyeInHand, &QAction::triggered, this, &CloudViewMainWindow::onEyeInHandTransform); // 创建中央控件 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* viewerContainer = new QWidget(splitter); QHBoxLayout* viewerLayout = new QHBoxLayout(viewerContainer); viewerLayout->setContentsMargins(0, 0, 0, 0); viewerLayout->setSpacing(2); // 视图方向工具栏(纵向排列,在3D视图左侧) QWidget* viewToolbar = createViewToolbar(); viewerLayout->addWidget(viewToolbar); // 点云显示区域 QWidget* viewerArea = createViewerArea(); viewerLayout->addWidget(viewerArea); splitter->addWidget(viewerContainer); // 右侧:控制面板 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); // 悬浮缩放按钮(父控件为 m_glWidget,悬浮在右侧中间) int zoomBtnSize = 28; QString zoomBtnStyle = "QToolButton {" " background-color: rgba(50, 50, 50, 180);" " color: white;" " border: 1px solid rgba(120, 120, 120, 150);" " border-radius: 4px;" " font-size: 32px;" " font-weight: bold;" "}" "QToolButton:hover {" " background-color: rgba(80, 80, 80, 220);" "}" "QToolButton:pressed {" " background-color: rgba(30, 30, 30, 240);" "}"; m_btnZoomIn = new QToolButton(m_glWidget); m_btnZoomIn->setText("+"); m_btnZoomIn->setFixedSize(zoomBtnSize, zoomBtnSize); m_btnZoomIn->setStyleSheet(zoomBtnStyle); m_btnZoomIn->setAutoRepeat(true); m_btnZoomIn->setAutoRepeatDelay(300); m_btnZoomIn->setAutoRepeatInterval(50); connect(m_btnZoomIn, &QToolButton::clicked, m_glWidget, &PointCloudGLWidget::zoomIn); m_btnZoomOut = new QToolButton(m_glWidget); m_btnZoomOut->setText("-"); m_btnZoomOut->setFixedSize(zoomBtnSize, zoomBtnSize); m_btnZoomOut->setStyleSheet(zoomBtnStyle); m_btnZoomOut->setAutoRepeat(true); m_btnZoomOut->setAutoRepeatDelay(300); m_btnZoomOut->setAutoRepeatInterval(50); connect(m_btnZoomOut, &QToolButton::clicked, m_glWidget, &PointCloudGLWidget::zoomOut); // 安装事件过滤器,在 GL 控件 resize 时更新按钮位置 m_glWidget->installEventFilter(this); // 连接信号 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()); // 创建 Tab 控件 QTabWidget* tabWidget = new QTabWidget(widget); tabWidget->addTab(createMeasurePage(), "选点测距"); tabWidget->addTab(createLinePage(), "选线"); 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_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_btnOpenBBox = new QPushButton("加载包围盒 (grasp.json)", group); m_btnOpenBBox->setMinimumHeight(24); m_btnOpenBBox->setMaximumHeight(24); connect(m_btnOpenBBox, &QPushButton::clicked, this, &CloudViewMainWindow::onOpenBBoxFile); layout->addWidget(m_btnOpenBBox); 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; } QWidget* CloudViewMainWindow::createViewToolbar() { QWidget* widget = new QWidget(this); QVBoxLayout* layout = new QVBoxLayout(widget); layout->setContentsMargins(2, 2, 2, 2); layout->setSpacing(3); // 视图预设:图标、提示、旋转角度 struct ViewPreset { const char* iconPath; const char* tooltip; float rotX; float rotY; float rotZ; }; // 坐标系定义:X向右,Y向下,Z朝后 ViewPreset presets[] = { {":/common/resource/view_front.png", "正视 (XY面)", 180.0f, 0.0f, 0.0f}, {":/common/resource/view_back.png", "后视", 180.0f, 180.0f, 0.0f}, {":/common/resource/view_left.png", "左视 (YZ面)", 180.0f, 90.0f, 0.0f}, {":/common/resource/view_right.png", "右视", 180.0f, -90.0f, 0.0f}, {":/common/resource/view_top.png", "俯视 (XZ面)", 90.0f, 0.0f, 0.0f}, {":/common/resource/view_bottom.png", "仰视", -90.0f, 0.0f, 0.0f}, {":/common/resource/view_robot.png", "机械臂", -90.0f, 90.0f, 0.0f}, }; int btnSize = 32; int iconSize = 24; for (const auto& preset : presets) { QToolButton* btn = new QToolButton(widget); btn->setIcon(QIcon(preset.iconPath)); btn->setIconSize(QSize(iconSize, iconSize)); btn->setFixedSize(btnSize, btnSize); btn->setToolTip(preset.tooltip); btn->setAutoRaise(true); float rx = preset.rotX; float ry = preset.rotY; float rz = preset.rotZ; connect(btn, &QToolButton::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)); }); layout->addWidget(btn); } layout->addStretch(); // 旋转角度输入(纵向紧凑布局) m_editRotX = new QLineEdit("180.0", widget); m_editRotY = new QLineEdit("0.0", widget); m_editRotZ = new QLineEdit("0.0", widget); auto makeAngleRow = [&](const QString& label, QLineEdit* edit) { QWidget* row = new QWidget(widget); QVBoxLayout* rowLayout = new QVBoxLayout(row); rowLayout->setContentsMargins(0, 0, 0, 0); rowLayout->setSpacing(0); QLabel* lbl = new QLabel(label, row); lbl->setStyleSheet("font-size: 10px; color: gray;"); rowLayout->addWidget(lbl); edit->setFixedWidth(btnSize); edit->setMaximumHeight(20); edit->setStyleSheet("font-size: 10px;"); rowLayout->addWidget(edit); return row; }; layout->addWidget(makeAngleRow("RX", m_editRotX)); layout->addWidget(makeAngleRow("RY", m_editRotY)); layout->addWidget(makeAngleRow("RZ", m_editRotZ)); // 应用按钮 QPushButton* btnApply = new QPushButton("GO", widget); btnApply->setFixedSize(btnSize, 20); btnApply->setStyleSheet("font-size: 10px;"); auto applyAngles = [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); } }; connect(btnApply, &QPushButton::clicked, this, applyAngles); connect(m_editRotX, &QLineEdit::returnPressed, this, applyAngles); connect(m_editRotY, &QLineEdit::returnPressed, this, applyAngles); connect(m_editRotZ, &QLineEdit::returnPressed, this, applyAngles); layout->addWidget(btnApply); widget->setFixedWidth(btnSize + 8); return widget; } 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); // 连接输入线段坐标框的回车信号 connect(m_editLineX1, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowInputLine); connect(m_editLineY1, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowInputLine); connect(m_editLineZ1, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowInputLine); connect(m_editLineX2, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowInputLine); connect(m_editLineY2, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowInputLine); connect(m_editLineZ2, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowInputLine); 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("--"); m_editPoint1X->setText("--"); m_editPoint1Y->setText("--"); m_editPoint1Z->setText("--"); m_editPoint2X->setText("--"); m_editPoint2Y->setText("--"); m_editPoint2Z->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* coord1Layout = new QHBoxLayout(); coord1Layout->setSpacing(5); coord1Layout->addWidget(new QLabel("X:", group)); m_editPoint1X = new QLineEdit("--", group); m_editPoint1X->setMaximumWidth(70); m_editPoint1X->setMaximumHeight(24); coord1Layout->addWidget(m_editPoint1X); coord1Layout->addWidget(new QLabel("Y:", group)); m_editPoint1Y = new QLineEdit("--", group); m_editPoint1Y->setMaximumWidth(70); m_editPoint1Y->setMaximumHeight(24); coord1Layout->addWidget(m_editPoint1Y); coord1Layout->addWidget(new QLabel("Z:", group)); m_editPoint1Z = new QLineEdit("--", group); m_editPoint1Z->setMaximumWidth(70); m_editPoint1Z->setMaximumHeight(24); coord1Layout->addWidget(m_editPoint1Z); coord1Layout->addStretch(); layout->addLayout(coord1Layout); // 连接回车信号 connect(m_editPoint1X, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onPoint1CoordChanged); connect(m_editPoint1Y, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onPoint1CoordChanged); connect(m_editPoint1Z, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onPoint1CoordChanged); // 点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); // 连接姿态输入框的回车信号 connect(m_editRx1, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowPose1); connect(m_editRy1, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowPose1); connect(m_editRz1, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowPose1); // 分隔线 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* coord2Layout = new QHBoxLayout(); coord2Layout->setSpacing(5); coord2Layout->addWidget(new QLabel("X:", group)); m_editPoint2X = new QLineEdit("--", group); m_editPoint2X->setMaximumWidth(70); m_editPoint2X->setMaximumHeight(24); coord2Layout->addWidget(m_editPoint2X); coord2Layout->addWidget(new QLabel("Y:", group)); m_editPoint2Y = new QLineEdit("--", group); m_editPoint2Y->setMaximumWidth(70); m_editPoint2Y->setMaximumHeight(24); coord2Layout->addWidget(m_editPoint2Y); coord2Layout->addWidget(new QLabel("Z:", group)); m_editPoint2Z = new QLineEdit("--", group); m_editPoint2Z->setMaximumWidth(70); m_editPoint2Z->setMaximumHeight(24); coord2Layout->addWidget(m_editPoint2Z); coord2Layout->addStretch(); layout->addLayout(coord2Layout); // 连接回车信号 connect(m_editPoint2X, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onPoint2CoordChanged); connect(m_editPoint2Y, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onPoint2CoordChanged); connect(m_editPoint2Z, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onPoint2CoordChanged); // 点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); // 连接姿态输入框的回车信号 connect(m_editRx2, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowPose2); connect(m_editRy2, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowPose2); connect(m_editRz2, &QLineEdit::returnPressed, this, &CloudViewMainWindow::onShowPose2); // 姿态坐标轴缩放输入 QHBoxLayout* scaleLayout = new QHBoxLayout(); scaleLayout->setSpacing(5); scaleLayout->addWidget(new QLabel("坐标轴长度:", group)); m_editPoseScale = new QLineEdit("10.0", group); m_editPoseScale->setMaximumWidth(60); scaleLayout->addWidget(m_editPoseScale); scaleLayout->addStretch(); layout->addLayout(scaleLayout); // 分隔线 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: 20px;"); 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); connect(m_lineNumberInput, &QLineEdit::returnPressed, 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() { // 如果已有打开的文件,提示是否清除 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(); } } QString fileName = QFileDialog::getOpenFileName( this, "打开文件", QString(), "所有支持格式 (*.pcd *.ply *.txt);;PCD 文件 (*.pcd);;PLY 文件 (*.ply);;TXT 文件 (*.txt);;所有文件 (*.*)" ); if (fileName.isEmpty()) { return; } // 同一文件中可能同时包含点云和线段,两者都尝试加载 bool cloudOk = loadPointCloudFile(fileName); bool segmentOk = loadSegmentFile(fileName); if (!cloudOk && !segmentOk) { QMessageBox::critical(this, "错误", "无法从文件中加载点云或线段数据"); statusBar()->showMessage("加载失败"); } } bool CloudViewMainWindow::loadPointCloudFile(const QString& fileName) { 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) { LOG_INFO("[CloudView] Load point cloud failed: %s\n", m_converter->getLastError().c_str()); return false; } // 保存原始完整点云 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(); if (ext == "pcd" || ext == "ply") { // PCD/PLY 文件始终走 XYZRGB 路径,避免被颜色轮换表染成蓝色等 if (!hadColor) { // 无 rgb 字段:默认灰色显示 for (size_t i = 0; i < rgbCloud.points.size(); ++i) { rgbCloud.points[i].r = 192; rgbCloud.points[i].g = 192; rgbCloud.points[i].b = 192; } } m_glWidget->addPointCloud(rgbCloud, cloudName); LOG_INFO("[CloudView] PCD/PLY loaded with XYZRGB path (hasRgbField=%d), points: %zu\n", m_converter->lastLoadHadColor(), rgbCloud.size()); } else 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(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 ? " (彩色)" : "")); return true; } bool CloudViewMainWindow::loadSegmentFile(const QString& fileName) { statusBar()->showMessage("正在加载线段..."); LaserDataLoader loader; std::vector> polyLines; int result = loader.LoadPolySegments(fileName.toStdString(), polyLines); if (result != 0) { LOG_INFO("[CloudView] Load segments failed: %s\n", loader.GetLastError().c_str()); return false; } if (polyLines.empty()) { return false; } // 将折线点转换为线段(使用点自带的颜色) QVector segments; 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 为 (A<<24)|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; uint8_t a = static_cast((p1.nRGB >> 24) & 0xFF); // A > 1 时作为线宽使用 float lineWidth = (a > 1) ? static_cast(a) : 0.0f; segments.append(LineSegment( p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, r, g, b, lineWidth)); } } if (segments.isEmpty()) { return false; } m_glWidget->addLineSegments(segments); // 添加到列表 QFileInfo fileInfo(fileName); int totalPolyCount = static_cast(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; } void CloudViewMainWindow::onOpenPoseFile() { QString fileName = QFileDialog::getOpenFileName( this, "打开姿态点文件", QString(), "文本文件 (*.txt);;所有文件 (*.*)" ); if (fileName.isEmpty()) { return; } statusBar()->showMessage("正在加载姿态点..."); QFile file(fileName); #if 0 if (false) { if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { QMessageBox::critical(this, "错误", QString("无法打开文件: %1").arg(fileName)); return; } QTextStream in(&file); const QString matrixText = in.readAll(); file.close(); QMatrix4x4 matrix; QString errorMessage; if (!parseMatrixText(matrixText, matrix, &errorMessage)) { QMessageBox::warning(this, "格式错误", errorMessage); return; } m_matrixEdit->setPlainText(formatMatrixText(matrix)); statusBar()->showMessage(QString("已从 %1 加载矩阵").arg(QFileInfo(fileName).fileName())); LOG_INFO("[CloudView] Loaded matrix from %s\n", fileName.toStdString().c_str()); return; } #endif 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; // 使用界面上配置的坐标轴长度 bool scaleOk = true; float scale = m_editPoseScale->text().toFloat(&scaleOk); if (!scaleOk || scale <= 0) 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::onOpenBBoxFile() { QString fileName = QFileDialog::getOpenFileName( this, "打开包围盒文件", QString(), "JSON 文件 (*.json);;所有文件 (*.*)" ); if (fileName.isEmpty()) { return; } if (!loadBoundingBoxFile(fileName)) { QMessageBox::critical(this, "错误", "无法加载包围盒数据"); statusBar()->showMessage("加载失败"); } } bool CloudViewMainWindow::loadBoundingBoxFile(const QString& fileName) { QFile file(fileName); if (!file.open(QIODevice::ReadOnly)) { LOG_INFO("[CloudView] Cannot open bbox file: %s\n", fileName.toStdString().c_str()); return false; } QByteArray data = file.readAll(); file.close(); QJsonParseError parseError; QJsonDocument doc = QJsonDocument::fromJson(data, &parseError); if (doc.isNull()) { LOG_INFO("[CloudView] JSON parse error: %s\n", parseError.errorString().toStdString().c_str()); return false; } QJsonObject root = doc.object(); QJsonArray records = root["records"].toArray(); if (records.isEmpty()) { LOG_INFO("[CloudView] No records in bbox file\n"); return false; } // 包围盒8个顶点的12条边索引 // 底面: 0-1, 1-2, 2-3, 3-0 // 顶面: 4-5, 5-6, 6-7, 7-4 // 立柱: 0-4, 1-5, 2-6, 3-7 static const int edgeIndices[][2] = { {0, 1}, {1, 2}, {2, 3}, {3, 0}, // 底面 {4, 5}, {5, 6}, {6, 7}, {7, 4}, // 顶面 {0, 4}, {1, 5}, {2, 6}, {3, 7} // 立柱 }; QVector segments; QVector graspPoses; int bboxCount = 0; // 为每个 record 使用不同的颜色 const float colors[][3] = { {0.0f, 1.0f, 0.0f}, // 绿色 {1.0f, 1.0f, 0.0f}, // 黄色 {0.0f, 1.0f, 1.0f}, // 青色 {1.0f, 0.5f, 0.0f}, // 橙色 {1.0f, 0.0f, 1.0f}, // 紫色 {0.5f, 1.0f, 0.5f}, // 浅绿 }; const int colorCount = sizeof(colors) / sizeof(colors[0]); // 使用界面上配置的坐标轴长度 bool poseScaleOk = true; float poseScale = m_editPoseScale->text().toFloat(&poseScaleOk); if (!poseScaleOk || poseScale <= 0) poseScale = 10.0f; for (int i = 0; i < records.size(); ++i) { QJsonObject record = records[i].toObject(); QJsonObject bbox = record["bounding_box"].toObject(); QJsonArray vertices = bbox["vertices"].toArray(); if (vertices.size() != 8) { LOG_WARN("[CloudView] Record %d: vertices count != 8, skip\n", i); continue; } // 解析8个顶点 float vx[8], vy[8], vz[8]; for (int j = 0; j < 8; ++j) { QJsonObject v = vertices[j].toObject(); vx[j] = static_cast(v["x"].toDouble()); vy[j] = static_cast(v["y"].toDouble()); vz[j] = static_cast(v["z"].toDouble()); } // 选择颜色 float r = colors[i % colorCount][0]; float g = colors[i % colorCount][1]; float b = colors[i % colorCount][2]; // 生成12条边的线段 for (const auto& edge : edgeIndices) { int a = edge[0]; int b_idx = edge[1]; segments.append(LineSegment( vx[a], vy[a], vz[a], vx[b_idx], vy[b_idx], vz[b_idx], r, g, b, 2.0f)); } bboxCount++; // 解析抓取点(右抓取点) if (record.contains("has_right_point") && record["has_right_point"].toBool()) { QJsonObject graspPt = record["right_grasp_point"].toObject(); QJsonObject pos = graspPt["position"].toObject(); QJsonObject ori = graspPt["orientation"].toObject(); float px = static_cast(pos["x"].toDouble()); float py = static_cast(pos["y"].toDouble()); float pz = static_cast(pos["z"].toDouble()); float roll = static_cast(ori["roll"].toDouble()); float pitch = static_cast(ori["pitch"].toDouble()); float yaw = static_cast(ori["yaw"].toDouble()); graspPoses.append(PosePoint(px, py, pz, roll, pitch, yaw, poseScale)); } // 解析左抓取点 if (record.contains("has_left_point") && record["has_left_point"].toBool()) { QJsonObject graspPt = record["left_grasp_point"].toObject(); QJsonObject pos = graspPt["position"].toObject(); QJsonObject ori = graspPt["orientation"].toObject(); float px = static_cast(pos["x"].toDouble()); float py = static_cast(pos["y"].toDouble()); float pz = static_cast(pos["z"].toDouble()); float roll = static_cast(ori["roll"].toDouble()); float pitch = static_cast(ori["pitch"].toDouble()); float yaw = static_cast(ori["yaw"].toDouble()); graspPoses.append(PosePoint(px, py, pz, roll, pitch, yaw, poseScale)); } } if (segments.isEmpty()) { return false; } m_glWidget->addLineSegments(segments); if (!graspPoses.isEmpty()) { m_glWidget->addPosePoints(graspPoses); } // 添加到列表 QFileInfo fileInfo(fileName); QString itemName = QString("BBox (%1)").arg(fileInfo.fileName()); QString itemText = QString("%1 - %2 个包围盒, %3 条边") .arg(itemName).arg(bboxCount).arg(segments.size()); if (!graspPoses.isEmpty()) { itemText += QString(", %1 个抓取点").arg(graspPoses.size()); } m_cloudList->addItem(itemText); statusBar()->showMessage(QString("已加载 %1 个包围盒").arg(bboxCount)); LOG_INFO("[CloudView] Loaded %d bounding boxes, %d grasp points from %s\n", bboxCount, graspPoses.size(), fileName.toStdString().c_str()); return true; } void CloudViewMainWindow::applyTransformToAllClouds(const QMatrix4x4& matrix) { m_glWidget->transformAllClouds(matrix); transformPointCloud(m_originalCloud, matrix); m_glWidget->clearSelectedPoints(); m_glWidget->clearSelectedLine(); m_glWidget->clearListHighlightPoint(); m_glWidget->clearLineSegments(); m_glWidget->clearPosePoints(); m_lblPoint1->setText("点1: --"); m_lblPoint2->setText("点2: --"); m_lblDistance->setText("--"); m_editPoint1X->setText("--"); m_editPoint1Y->setText("--"); m_editPoint1Z->setText("--"); m_editPoint2X->setText("--"); m_editPoint2Y->setText("--"); m_editPoint2Z->setText("--"); m_lblLineIndex->setText("--"); m_lblLinePointCount->setText("--"); m_currentLinePoints.clear(); if (m_linePointsDialog && m_linePointsTable) { updateLinePointsDialog(); } } void CloudViewMainWindow::addGeneratedCloud(const PointCloudXYZ& cloud, const QString& name) { m_glWidget->clearSelectedPoints(); m_glWidget->clearSelectedLine(); m_glWidget->clearListHighlightPoint(); const QString cloudName = QString("Cloud_%1 (%2)").arg(++m_cloudCount).arg(name); m_glWidget->addPointCloud(cloud, cloudName); m_originalCloud = cloud; m_currentLineNum = 0; m_currentLinePtNum = 0; m_cloudList->addItem(QString("%1 - %2 点 [生成]").arg(cloudName).arg(cloud.size())); statusBar()->showMessage(QString("已生成点云: %1,点数 %2").arg(name).arg(cloud.size())); LOG_INFO("[CloudView] Generated synthetic cloud: %s, points=%zu\n", cloudName.toStdString().c_str(), cloud.size()); } void CloudViewMainWindow::onClearAll() { m_glWidget->clearPointClouds(); m_glWidget->clearLineSegments(); m_glWidget->clearPosePoints(); 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_editPoint1X->setText("--"); m_editPoint1Y->setText("--"); m_editPoint1Z->setText("--"); m_editPoint2X->setText("--"); m_editPoint2Y->setText("--"); m_editPoint2Z->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("--"); m_editPoint1X->setText("--"); m_editPoint1Y->setText("--"); m_editPoint1Z->setText("--"); m_editPoint2X->setText("--"); m_editPoint2Y->setText("--"); m_editPoint2Z->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") .arg(selectedPoints[0].lineIndex) .arg(selectedPoints[0].pointIndexInLine); } else { text = QString("点1:"); } m_lblPoint1->setText(text); // 更新坐标编辑框 m_editPoint1X->setText(QString::number(selectedPoints[0].x, 'f', 3)); m_editPoint1Y->setText(QString::number(selectedPoints[0].y, 'f', 3)); m_editPoint1Z->setText(QString::number(selectedPoints[0].z, 'f', 3)); } else { m_lblPoint1->setText("点1: --"); m_editPoint1X->setText("--"); m_editPoint1Y->setText("--"); m_editPoint1Z->setText("--"); } if (selectedPoints.size() >= 2 && selectedPoints[1].valid) { QString text; if (selectedPoints[1].lineIndex >= 0) { text = QString("点2: 线号:%1 点序:%2") .arg(selectedPoints[1].lineIndex) .arg(selectedPoints[1].pointIndexInLine); } else { text = QString("点2:"); } m_lblPoint2->setText(text); // 更新坐标编辑框 m_editPoint2X->setText(QString::number(selectedPoints[1].x, 'f', 3)); m_editPoint2Y->setText(QString::number(selectedPoints[1].y, 'f', 3)); m_editPoint2Z->setText(QString::number(selectedPoints[1].z, 'f', 3)); } else { m_lblPoint2->setText("点2: --"); m_editPoint2X->setText("--"); m_editPoint2Y->setText("--"); m_editPoint2Z->setText("--"); } } 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() { // 从输入框读取点1坐标 bool ok = true; float x1 = m_editPoint1X->text().toFloat(&ok); if (!ok) { QMessageBox::warning(this, "错误", "点1 X 值无效"); return; } float y1 = m_editPoint1Y->text().toFloat(&ok); if (!ok) { QMessageBox::warning(this, "错误", "点1 Y 值无效"); return; } float z1 = m_editPoint1Z->text().toFloat(&ok); if (!ok) { QMessageBox::warning(this, "错误", "点1 Z 值无效"); return; } // 设置选中点(不存在则创建) m_glWidget->setSelectedPointCoord(0, x1, y1, z1); // 读取姿态参数 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; } // 读取坐标轴缩放 float scale = m_editPoseScale->text().toFloat(&ok); if (!ok || scale <= 0) scale = 25.0f; // 清除之前的姿态点 m_glWidget->clearPosePoints(); // 创建点1的姿态点 PosePoint pose1(x1, y1, z1, rx, ry, rz, scale); QVector poses; poses.append(pose1); // 如果点2输入框有值,也一起显示 bool ok2 = true; float x2 = m_editPoint2X->text().toFloat(&ok2); float y2 = ok2 ? m_editPoint2Y->text().toFloat(&ok2) : 0; float z2 = ok2 ? m_editPoint2Z->text().toFloat(&ok2) : 0; if (ok2 && !(x2 == 0 && y2 == 0 && z2 == 0 && m_editPoint2X->text().isEmpty())) { m_glWidget->setSelectedPointCoord(1, x2, y2, z2); bool okR = true; float rx2 = m_editRx2->text().toFloat(&okR); float ry2 = okR ? m_editRy2->text().toFloat(&okR) : 0; float rz2 = okR ? m_editRz2->text().toFloat(&okR) : 0; if (okR) { PosePoint pose2(x2, y2, z2, rx2, ry2, rz2, scale); poses.append(pose2); } } // 添加到显示 m_glWidget->addPosePoints(poses); statusBar()->showMessage(QString("已显示点1姿态 (%1, %2, %3) 旋转(%4°, %5°, %6°)") .arg(x1, 0, 'f', 3).arg(y1, 0, 'f', 3).arg(z1, 0, 'f', 3) .arg(rx, 0, 'f', 1).arg(ry, 0, 'f', 1).arg(rz, 0, 'f', 1)); LOG_INFO("[CloudView] Show pose1 at (%.3f, %.3f, %.3f) with rotation (%.1f, %.1f, %.1f)\n", x1, y1, z1, rx, ry, rz); } void CloudViewMainWindow::onShowPose2() { // 从输入框读取点2坐标 bool ok = true; float x2 = m_editPoint2X->text().toFloat(&ok); if (!ok) { QMessageBox::warning(this, "错误", "点2 X 值无效"); return; } float y2 = m_editPoint2Y->text().toFloat(&ok); if (!ok) { QMessageBox::warning(this, "错误", "点2 Y 值无效"); return; } float z2 = m_editPoint2Z->text().toFloat(&ok); if (!ok) { QMessageBox::warning(this, "错误", "点2 Z 值无效"); return; } // 设置选中点(不存在则创建) m_glWidget->setSelectedPointCoord(1, x2, y2, z2); // 读取姿态参数 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; } // 读取坐标轴缩放 float scale = m_editPoseScale->text().toFloat(&ok); if (!ok || scale <= 0) scale = 25.0f; // 清除之前的姿态点 m_glWidget->clearPosePoints(); QVector poses; // 如果点1输入框有值,也一起显示 bool ok1 = true; float x1 = m_editPoint1X->text().toFloat(&ok1); float y1 = ok1 ? m_editPoint1Y->text().toFloat(&ok1) : 0; float z1 = ok1 ? m_editPoint1Z->text().toFloat(&ok1) : 0; if (ok1 && !(x1 == 0 && y1 == 0 && z1 == 0 && m_editPoint1X->text().isEmpty())) { m_glWidget->setSelectedPointCoord(0, x1, y1, z1); bool okR = true; float rx1 = m_editRx1->text().toFloat(&okR); float ry1 = okR ? m_editRy1->text().toFloat(&okR) : 0; float rz1 = okR ? m_editRz1->text().toFloat(&okR) : 0; if (okR) { PosePoint pose1(x1, y1, z1, rx1, ry1, rz1, scale); poses.append(pose1); } } // 创建点2的姿态点 PosePoint pose2(x2, y2, z2, rx, ry, rz, scale); poses.append(pose2); // 添加到显示 m_glWidget->addPosePoints(poses); statusBar()->showMessage(QString("已显示点2姿态 (%1, %2, %3) 旋转(%4°, %5°, %6°)") .arg(x2, 0, 'f', 3).arg(y2, 0, 'f', 3).arg(z2, 0, 'f', 3) .arg(rx, 0, 'f', 1).arg(ry, 0, 'f', 1).arg(rz, 0, 'f', 1)); LOG_INFO("[CloudView] Show pose2 at (%.3f, %.3f, %.3f) with rotation (%.1f, %.1f, %.1f)\n", x2, y2, z2, 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, 0, 'f', 1).arg(y1, 0, 'f', 1).arg(z1, 0, 'f', 1) .arg(x2, 0, 'f', 1).arg(y2, 0, 'f', 1).arg(z2, 0, 'f', 1) .arg(distance, 0, 'f', 3)); 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(QString("已清除输入的线段")); } void CloudViewMainWindow::onEyeToHandTransform() { if (m_glWidget->getCloudCount() == 0) { QMessageBox::warning(this, "提示", "请先加载点云"); return; } QDialog dialog(this); dialog.setWindowTitle("眼在手外 - 矩阵变换"); dialog.setModal(true); QVBoxLayout* layout = new QVBoxLayout(&dialog); QLabel* tip = new QLabel("输入或从文件加载 4x4 变换矩阵(相机坐标系 -> 基坐标系),直接应用到所有点云。", &dialog); tip->setWordWrap(true); tip->setStyleSheet("color: gray; font-size: 10px;"); layout->addWidget(tip); QLabel* lblMatrix = new QLabel("变换矩阵 (4x4):", &dialog); lblMatrix->setStyleSheet("font-weight: bold;"); layout->addWidget(lblMatrix); QTextEdit* matrixEdit = new QTextEdit(&dialog); matrixEdit->setFont(QFont("Consolas", 10)); matrixEdit->setMinimumHeight(100); matrixEdit->setMaximumHeight(120); 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"); layout->addWidget(matrixEdit); QPushButton* btnLoad = new QPushButton("从文件加载矩阵", &dialog); connect(btnLoad, &QPushButton::clicked, [this, matrixEdit]() { const QString fileName = QFileDialog::getOpenFileName( this, "打开矩阵文件", QString(), "标定矩阵文件 (*.ini *.txt);;INI文件 (*.ini);;文本文件 (*.txt);;所有文件 (*.*)"); if (fileName.isEmpty()) return; if (fileName.endsWith(".ini", Qt::CaseInsensitive)) { QVector> matrixList; QString errorMessage; if (!parseCalibIniFile(fileName, matrixList, &errorMessage)) { QMessageBox::warning(this, "格式错误", errorMessage); return; } if (matrixList.size() == 1) { matrixEdit->setPlainText(formatMatrixText(matrixList[0].second)); return; } QDialog dlg(this); dlg.setWindowTitle("选择标定矩阵"); QVBoxLayout* l = new QVBoxLayout(&dlg); l->addWidget(new QLabel(QString("文件中包含 %1 个标定矩阵,请选择:").arg(matrixList.size()), &dlg)); QListWidget* lw = new QListWidget(&dlg); for (auto& m : matrixList) lw->addItem(m.first); lw->setCurrentRow(0); l->addWidget(lw); QDialogButtonBox* bb = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg); l->addWidget(bb); connect(bb, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); connect(bb, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); if (dlg.exec() == QDialog::Accepted && lw->currentRow() >= 0 && lw->currentRow() < matrixList.size()) matrixEdit->setPlainText(formatMatrixText(matrixList[lw->currentRow()].second)); return; } QFile file(fileName); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { QMessageBox::critical(this, "错误", QString("无法打开文件: %1").arg(fileName)); return; } QTextStream in(&file); QString text = in.readAll(); file.close(); QMatrix4x4 matrix; QString errorMessage; if (!parseMatrixText(text, matrix, &errorMessage)) { QMessageBox::warning(this, "格式错误", errorMessage); return; } matrixEdit->setPlainText(formatMatrixText(matrix)); }); layout->addWidget(btnLoad); QLabel* lblFormat = new QLabel("矩阵文件格式:4 行,每行 4 个数值,分隔符:空格 / Tab / 逗号", &dialog); lblFormat->setWordWrap(true); lblFormat->setStyleSheet("color: gray; font-size: 9px;"); layout->addWidget(lblFormat); QDialogButtonBox* btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); btnBox->button(QDialogButtonBox::Ok)->setText("应用变换"); btnBox->button(QDialogButtonBox::Ok)->setStyleSheet("QPushButton { background-color: #4CAF50; color: white; font-weight: bold; }" "QPushButton:hover { background-color: #45a049; }"); layout->addWidget(btnBox); connect(btnBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(btnBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); if (dialog.exec() != QDialog::Accepted) return; QMatrix4x4 matrix; QString errorMessage; if (!parseMatrixText(matrixEdit->toPlainText().trimmed(), matrix, &errorMessage)) { QMessageBox::warning(this, "格式错误", errorMessage); return; } if (matrix.isIdentity()) { QMessageBox::information(this, "提示", "当前矩阵为单位矩阵,无需变换"); return; } applyTransformToAllClouds(matrix); statusBar()->showMessage("已将眼在手外矩阵变换应用到所有点云"); LOG_INFO("[CloudView] Applied eye-to-hand matrix transform to all point clouds\n"); } void CloudViewMainWindow::onEyeInHandTransform() { if (m_glWidget->getCloudCount() == 0) { QMessageBox::warning(this, "提示", "请先加载点云"); return; } QDialog dialog(this); dialog.setWindowTitle("眼在手上 - 矩阵变换"); dialog.setModal(true); QVBoxLayout* layout = new QVBoxLayout(&dialog); QLabel* tip = new QLabel( "使用 4x4 矩阵作为“相机坐标系 -> 末端坐标系”的手眼矩阵。
" "输入当前机械臂位姿(单位:mm / deg)后,将所有点云转换到机械臂基坐标系。
" "角度组合方式:ZYX(Rz-Ry-Rx)。", &dialog); tip->setWordWrap(true); tip->setTextFormat(Qt::RichText); tip->setStyleSheet("color: gray; font-size: 10px;"); layout->addWidget(tip); QLabel* lblMatrix = new QLabel("手眼矩阵 (4x4):", &dialog); lblMatrix->setStyleSheet("font-weight: bold;"); layout->addWidget(lblMatrix); QTextEdit* matrixEdit = new QTextEdit(&dialog); matrixEdit->setFont(QFont("Consolas", 10)); matrixEdit->setMinimumHeight(100); matrixEdit->setMaximumHeight(120); 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"); layout->addWidget(matrixEdit); QPushButton* btnLoad = new QPushButton("从文件加载矩阵", &dialog); connect(btnLoad, &QPushButton::clicked, [this, matrixEdit]() { const QString fileName = QFileDialog::getOpenFileName( this, "打开矩阵文件", QString(), "标定矩阵文件 (*.ini *.txt);;INI文件 (*.ini);;文本文件 (*.txt);;所有文件 (*.*)"); if (fileName.isEmpty()) return; if (fileName.endsWith(".ini", Qt::CaseInsensitive)) { QVector> matrixList; QString errorMessage; if (!parseCalibIniFile(fileName, matrixList, &errorMessage)) { QMessageBox::warning(this, "格式错误", errorMessage); return; } if (matrixList.size() == 1) { matrixEdit->setPlainText(formatMatrixText(matrixList[0].second)); return; } QDialog dlg(this); dlg.setWindowTitle("选择标定矩阵"); QVBoxLayout* l = new QVBoxLayout(&dlg); l->addWidget(new QLabel(QString("文件中包含 %1 个标定矩阵,请选择:").arg(matrixList.size()), &dlg)); QListWidget* lw = new QListWidget(&dlg); for (auto& m : matrixList) lw->addItem(m.first); lw->setCurrentRow(0); l->addWidget(lw); QDialogButtonBox* bb = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg); l->addWidget(bb); connect(bb, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); connect(bb, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); if (dlg.exec() == QDialog::Accepted && lw->currentRow() >= 0 && lw->currentRow() < matrixList.size()) matrixEdit->setPlainText(formatMatrixText(matrixList[lw->currentRow()].second)); return; } QFile file(fileName); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { QMessageBox::critical(this, "错误", QString("无法打开文件: %1").arg(fileName)); return; } QTextStream in(&file); QString text = in.readAll(); file.close(); QMatrix4x4 matrix; QString errorMessage; if (!parseMatrixText(text, matrix, &errorMessage)) { QMessageBox::warning(this, "格式错误", errorMessage); return; } matrixEdit->setPlainText(formatMatrixText(matrix)); }); layout->addWidget(btnLoad); // 机械臂位姿输入 QGroupBox* poseGroup = new QGroupBox("机械臂位姿", &dialog); QGridLayout* poseLayout = new QGridLayout(poseGroup); poseLayout->setHorizontalSpacing(8); poseLayout->setVerticalSpacing(6); auto addPoseEdit = [&](const QString& label, int row, int col) -> QLineEdit* { QLabel* lbl = new QLabel(label, poseGroup); QLineEdit* edit = new QLineEdit("0.0", poseGroup); edit->setAlignment(Qt::AlignRight); edit->setFont(QFont("Consolas", 10)); poseLayout->addWidget(lbl, row, col * 2); poseLayout->addWidget(edit, row, col * 2 + 1); return edit; }; QLineEdit* editX = addPoseEdit("X:", 0, 0); QLineEdit* editY = addPoseEdit("Y:", 0, 1); QLineEdit* editZ = addPoseEdit("Z:", 0, 2); QLineEdit* editRx = addPoseEdit("RX:", 1, 0); QLineEdit* editRy = addPoseEdit("RY:", 1, 1); QLineEdit* editRz = addPoseEdit("RZ:", 1, 2); layout->addWidget(poseGroup); QDialogButtonBox* btnBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); btnBox->button(QDialogButtonBox::Ok)->setText("应用转换"); btnBox->button(QDialogButtonBox::Ok)->setStyleSheet("QPushButton { background-color: #1976D2; color: white; font-weight: bold; }" "QPushButton:hover { background-color: #1565C0; }"); layout->addWidget(btnBox); connect(btnBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(btnBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); if (dialog.exec() != QDialog::Accepted) return; QMatrix4x4 handEyeMatrix; QString errorMessage; if (!parseMatrixText(matrixEdit->toPlainText().trimmed(), handEyeMatrix, &errorMessage)) { QMessageBox::warning(this, "格式错误", QString("手眼矩阵无效:%1").arg(errorMessage)); return; } bool ok = false; float vals[6]; const char* labels[] = {"X", "Y", "Z", "RX", "RY", "RZ"}; QLineEdit* edits[] = {editX, editY, editZ, editRx, editRy, editRz}; for (int i = 0; i < 6; ++i) { vals[i] = edits[i]->text().trimmed().toFloat(&ok); if (!ok) { QMessageBox::warning(this, "格式错误", QString("机械臂 %1 位姿无效").arg(labels[i])); return; } } const QMatrix4x4 totalMatrix = buildRobotPoseMatrix(vals[0], vals[1], vals[2], vals[3], vals[4], vals[5]) * handEyeMatrix; if (totalMatrix.isIdentity()) { QMessageBox::information(this, "提示", "当前位姿和手眼矩阵组合为单位矩阵,无需变换"); return; } applyTransformToAllClouds(totalMatrix); statusBar()->showMessage( QString("已应用眼在手上转换,机械臂位姿: [%1, %2, %3, %4, %5, %6]") .arg(vals[0], 0, 'f', 3).arg(vals[1], 0, 'f', 3).arg(vals[2], 0, 'f', 3) .arg(vals[3], 0, 'f', 3).arg(vals[4], 0, 'f', 3).arg(vals[5], 0, 'f', 3)); LOG_INFO("[CloudView] Applied eye-in-hand transform, robot pose=(%.3f, %.3f, %.3f, %.3f, %.3f, %.3f)\n", vals[0], vals[1], vals[2], vals[3], vals[4], vals[5]); } void CloudViewMainWindow::onSavePointCloud() { if (m_glWidget->getCloudCount() == 0) { QMessageBox::warning(this, "提示", "请先加载点云"); return; } const QString fileName = QFileDialog::getSaveFileName( this, "保存 TXT 点云", QString(), "文本文件 (*.txt);;所有文件 (*.*)"); if (fileName.isEmpty()) return; std::vector> scanLines; const size_t totalPoints = m_glWidget->getAllCloudsByLines(scanLines); if (scanLines.empty() || totalPoints == 0) { QMessageBox::warning(this, "提示", "当前无可保存的点云数据"); return; } // 进度对话框 QProgressDialog progress("正在保存点云...", "取消", 0, 100, this); progress.setWindowTitle("保存点云"); progress.setWindowModality(Qt::WindowModal); progress.setMinimumDuration(0); progress.setAutoClose(true); progress.setAutoReset(true); // 回调实现 class SaveProgressCallback : public ISaveProgressCallback { public: SaveProgressCallback(QProgressDialog& dlg) : m_dlg(dlg) {} void onSaveProgress(float progress) override { m_dlg.setValue(static_cast(progress * 100)); QCoreApplication::processEvents(); } bool isSaveCancelled() const override { return m_dlg.wasCanceled(); } private: QProgressDialog& m_dlg; }; SaveProgressCallback callback(progress); LaserDataLoader loader; const int ret = loader.DebugSaveLaser(fileName.toStdString(), scanLines, &callback); progress.setValue(100); if (ret != 0) { if (callback.isSaveCancelled()) { statusBar()->showMessage("保存已取消"); } else { QMessageBox::critical(this, "保存失败", QString("保存点云失败: %1").arg(QString::fromStdString(loader.GetLastError()))); } return; } statusBar()->showMessage( QString("已保存点云: %1 条线, %2 个点 -> %3") .arg(scanLines.size()).arg(totalPoints).arg(QFileInfo(fileName).fileName())); LOG_INFO("[CloudView] Saved %zu lines, %zu points to %s\n", scanLines.size(), totalPoints, fileName.toStdString().c_str()); } void CloudViewMainWindow::onSavePlyPcdCloud() { if (m_glWidget->getCloudCount() == 0) { QMessageBox::warning(this, "提示", "请先加载点云"); return; } const QString fileName = QFileDialog::getSaveFileName( this, "保存 PLY/PCD 点云", QString(), "PLY 文件 (*.ply);;PCD 文件 (*.pcd);;所有文件 (*.*)"); if (fileName.isEmpty()) return; // 获取所有显示的点云,展平为 PointCloudXYZ(跳过零点) std::vector> scanLines; const size_t totalPoints = m_glWidget->getAllCloudsByLines(scanLines); if (scanLines.empty() || totalPoints == 0) { QMessageBox::warning(this, "提示", "当前无可保存的点云数据"); return; } PointCloudXYZ cloud; const float EPS = 1e-6f; for (const auto& line : scanLines) { for (const auto& pt : line) { // 跳过零点(网格填充的空位) if (std::fabs(pt.pt3D.x) < EPS && std::fabs(pt.pt3D.y) < EPS && std::fabs(pt.pt3D.z) < EPS) continue; cloud.points.emplace_back(pt.pt3D.x, pt.pt3D.y, pt.pt3D.z); } } if (cloud.empty()) { QMessageBox::warning(this, "提示", "当前无可保存的有效点云数据"); return; } // 根据扩展名选择保存格式 const std::string path = fileName.toStdString(); std::string ext; const auto dotPos = path.rfind('.'); if (dotPos != std::string::npos) ext = path.substr(dotPos + 1); for (auto& c : ext) c = static_cast(std::tolower(c)); int ret = -1; if (ext == "ply") { ret = m_converter->saveToPly(path, cloud); } else if (ext == "pcd") { ret = m_converter->saveToPcd(path, cloud); } else { // 用户手动输入了其他扩展名,默认用 PLY ret = m_converter->saveToPly(path, cloud); } if (ret != 0) { QMessageBox::critical(this, "保存失败", QString("保存点云失败: %1") .arg(QString::fromStdString(m_converter->getLastError()))); return; } statusBar()->showMessage( QString("已保存点云: %1 个点 -> %2") .arg(cloud.points.size()).arg(QFileInfo(fileName).fileName())); LOG_INFO("[CloudView] Saved %zu points to %s\n", cloud.points.size(), fileName.toStdString().c_str()); } void CloudViewMainWindow::onRotateCloudByZ() { if (m_glWidget->getCloudCount() == 0) { QMessageBox::warning(this, "提示", "请先加载点云"); return; } QDialog dialog(this); dialog.setWindowTitle("点云旋转"); dialog.setModal(true); QVBoxLayout* layout = new QVBoxLayout(&dialog); QLabel* tip = new QLabel("对 Z 值小于阈值的点绕指定轴旋转指定角度。", &dialog); tip->setWordWrap(true); tip->setStyleSheet("color: gray;"); layout->addWidget(tip); auto makeSpinBox = [&](double minValue, double maxValue, double value, double step = 1.0) { QDoubleSpinBox* spin = new QDoubleSpinBox(&dialog); spin->setDecimals(3); spin->setRange(minValue, maxValue); spin->setValue(value); spin->setSingleStep(step); spin->setAlignment(Qt::AlignRight); return spin; }; QGroupBox* thresholdGroup = new QGroupBox("Z 轴阈值", &dialog); QFormLayout* thresholdLayout = new QFormLayout(thresholdGroup); QDoubleSpinBox* zThreshold = makeSpinBox(-100000.0, 100000.0, 0.0, 10.0); thresholdLayout->addRow("Z < ", zThreshold); layout->addWidget(thresholdGroup); QGroupBox* rotateGroup = new QGroupBox("旋转角度 (度)", &dialog); QFormLayout* rotateLayout = new QFormLayout(rotateGroup); QDoubleSpinBox* rotX = makeSpinBox(-360.0, 360.0, 0.0, 5.0); QDoubleSpinBox* rotY = makeSpinBox(-360.0, 360.0, 0.0, 5.0); QDoubleSpinBox* rotZ = makeSpinBox(-360.0, 360.0, 0.0, 5.0); rotateLayout->addRow("绕 X 轴:", rotX); rotateLayout->addRow("绕 Y 轴:", rotY); rotateLayout->addRow("绕 Z 轴:", rotZ); layout->addWidget(rotateGroup); QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); layout->addWidget(buttonBox); if (dialog.exec() != QDialog::Accepted) { return; } // 构建旋转矩阵: Rz * Ry * Rx const QMatrix4x4 rotMatrix = makeAxisRotationMatrix('z', rotZ->value() * kDegToRad) * makeAxisRotationMatrix('y', rotY->value() * kDegToRad) * makeAxisRotationMatrix('x', rotX->value() * kDegToRad); const size_t count = m_glWidget->rotateCloudsByZThreshold(rotMatrix, static_cast(zThreshold->value())); if (count == 0) { QMessageBox::information(this, "提示", "没有满足条件的点 (Z < 阈值)"); return; } statusBar()->showMessage( QString("已旋转 %1 个点 (Z < %2, 绕X %3° Y %4° Z %5°)") .arg(count) .arg(zThreshold->value(), 0, 'f', 3) .arg(rotX->value(), 0, 'f', 1) .arg(rotY->value(), 0, 'f', 1) .arg(rotZ->value(), 0, 'f', 1)); LOG_INFO("[CloudView] Rotated %zu points with Z < %.3f by (%.1f, %.1f, %.1f) deg\n", count, zThreshold->value(), rotX->value(), rotY->value(), rotZ->value()); } void CloudViewMainWindow::onGeneratePointCloud() { QDialog dialog(this); dialog.setWindowTitle("生成长方体点云"); dialog.setModal(true); QVBoxLayout* layout = new QVBoxLayout(&dialog); QLabel* tip = new QLabel("根据中心点和 XYZ 尺寸生成轴对齐长方体表面点云。", &dialog); tip->setWordWrap(true); tip->setStyleSheet("color: gray;"); layout->addWidget(tip); auto makeSpinBox = [&](double minValue, double maxValue, double value) { QDoubleSpinBox* spin = new QDoubleSpinBox(&dialog); spin->setDecimals(3); spin->setRange(minValue, maxValue); spin->setValue(value); spin->setSingleStep(1.0); spin->setAlignment(Qt::AlignRight); return spin; }; QGroupBox* centerGroup = new QGroupBox("中心点", &dialog); QFormLayout* centerLayout = new QFormLayout(centerGroup); QDoubleSpinBox* centerX = makeSpinBox(-100000.0, 100000.0, 0.0); QDoubleSpinBox* centerY = makeSpinBox(-100000.0, 100000.0, 0.0); QDoubleSpinBox* centerZ = makeSpinBox(-100000.0, 100000.0, 0.0); centerLayout->addRow("X:", centerX); centerLayout->addRow("Y:", centerY); centerLayout->addRow("Z:", centerZ); layout->addWidget(centerGroup); QGroupBox* sizeGroup = new QGroupBox("三轴方向大小", &dialog); QFormLayout* sizeLayout = new QFormLayout(sizeGroup); QDoubleSpinBox* sizeX = makeSpinBox(0.001, 100000.0, 100.0); QDoubleSpinBox* sizeY = makeSpinBox(0.001, 100000.0, 100.0); QDoubleSpinBox* sizeZ = makeSpinBox(0.001, 100000.0, 100.0); QDoubleSpinBox* spacing = makeSpinBox(0.001, 10000.0, 5.0); sizeLayout->addRow("X 尺寸:", sizeX); sizeLayout->addRow("Y 尺寸:", sizeY); sizeLayout->addRow("Z 尺寸:", sizeZ); sizeLayout->addRow("点间距:", spacing); layout->addWidget(sizeGroup); QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); layout->addWidget(buttonBox); if (dialog.exec() != QDialog::Accepted) { return; } const QVector3D center(static_cast(centerX->value()), static_cast(centerY->value()), static_cast(centerZ->value())); const QVector3D size(static_cast(sizeX->value()), static_cast(sizeY->value()), static_cast(sizeZ->value())); const float pointSpacing = static_cast(spacing->value()); const int gridX = std::max(1, static_cast(std::ceil(size.x() / pointSpacing))) + 1; const int gridY = std::max(1, static_cast(std::ceil(size.y() / pointSpacing))) + 1; const int gridZ = std::max(1, static_cast(std::ceil(size.z() / pointSpacing))) + 1; const double estimatedGridCount = static_cast(gridX) * gridY * gridZ; if (estimatedGridCount > 3000000.0) { QMessageBox::warning(this, "提示", "点数预计过大,请增大点间距或减小尺寸。"); return; } const PointCloudXYZ cloud = generateBoxPointCloud(center, size, pointSpacing); if (cloud.empty()) { QMessageBox::warning(this, "提示", "生成点云失败,结果为空。"); return; } addGeneratedCloud(cloud, QString("GeneratedBox_%1x%2x%3") .arg(size.x(), 0, 'f', 1) .arg(size.y(), 0, 'f', 1) .arg(size.z(), 0, 'f', 1)); } void CloudViewMainWindow::onConvertEulerMatrix() { QDialog* dialog = new QDialog(this); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setWindowTitle("欧拉角与方向向量矩阵互转"); dialog->setWindowFlags(dialog->windowFlags() | Qt::Tool); QVBoxLayout* layout = new QVBoxLayout(dialog); QLabel* tip = new QLabel( "输入欧拉角(单位: 度)按所选旋转顺序得到 3x3 方向向量矩阵;也可以从矩阵反算欧拉角。\n" "方向向量矩阵采用列向量约定: R = [ X轴 | Y轴 | Z轴 ],每列为该坐标轴在参考系中的单位方向向量。", dialog); tip->setWordWrap(true); tip->setStyleSheet("color: gray;"); layout->addWidget(tip); QGroupBox* eulerGroup = new QGroupBox("欧拉角 (deg)", dialog); QGridLayout* eulerLayout = new QGridLayout(eulerGroup); auto makeAngleSpin = [](QWidget* parent) { QDoubleSpinBox* spin = new QDoubleSpinBox(parent); spin->setDecimals(6); spin->setRange(-3600.0, 3600.0); spin->setSingleStep(0.1); spin->setValue(0.0); spin->setAlignment(Qt::AlignRight); spin->setButtonSymbols(QAbstractSpinBox::NoButtons); return spin; }; QDoubleSpinBox* eulerRx = makeAngleSpin(eulerGroup); QDoubleSpinBox* eulerRy = makeAngleSpin(eulerGroup); QDoubleSpinBox* eulerRz = makeAngleSpin(eulerGroup); eulerLayout->addWidget(new QLabel("RX:", eulerGroup), 0, 0); eulerLayout->addWidget(eulerRx, 0, 1); eulerLayout->addWidget(new QLabel("RY:", eulerGroup), 1, 0); eulerLayout->addWidget(eulerRy, 1, 1); eulerLayout->addWidget(new QLabel("RZ:", eulerGroup), 2, 0); eulerLayout->addWidget(eulerRz, 2, 1); QComboBox* comboOrder = new QComboBox(eulerGroup); comboOrder->addItem("ZYX (Yaw-Pitch-Roll)", static_cast(EulerRotationOrder::ZYX)); comboOrder->addItem("XYZ (Roll-Pitch-Yaw)", static_cast(EulerRotationOrder::XYZ)); comboOrder->addItem("ZXY (Yaw-Roll-Pitch)", static_cast(EulerRotationOrder::ZXY)); comboOrder->addItem("YXZ (Pitch-Roll-Yaw)", static_cast(EulerRotationOrder::YXZ)); comboOrder->addItem("XZY (Roll-Yaw-Pitch)", static_cast(EulerRotationOrder::XZY)); comboOrder->addItem("YZX (Pitch-Yaw-Roll)", static_cast(EulerRotationOrder::YZX)); if (m_comboEulerOrder && m_comboEulerOrder->currentIndex() >= 0) { comboOrder->setCurrentIndex(m_comboEulerOrder->currentIndex()); } eulerLayout->addWidget(new QLabel("旋转顺序:", eulerGroup), 3, 0); eulerLayout->addWidget(comboOrder, 3, 1); QGroupBox* matrixGroup = new QGroupBox("方向向量矩阵 (3x3)", dialog); QGridLayout* matLayout = new QGridLayout(matrixGroup); auto makeHeader = [matrixGroup](const QString& text) { QLabel* label = new QLabel(text, matrixGroup); label->setAlignment(Qt::AlignCenter); label->setStyleSheet("font-weight: bold;"); return label; }; matLayout->addWidget(makeHeader("X轴"), 0, 1); matLayout->addWidget(makeHeader("Y轴"), 0, 2); matLayout->addWidget(makeHeader("Z轴"), 0, 3); matLayout->addWidget(new QLabel("X分量:", matrixGroup), 1, 0); matLayout->addWidget(new QLabel("Y分量:", matrixGroup), 2, 0); matLayout->addWidget(new QLabel("Z分量:", matrixGroup), 3, 0); std::array, 3> matCells{}; for (int c = 0; c < 3; ++c) { for (int r = 0; r < 3; ++r) { QDoubleSpinBox* spin = new QDoubleSpinBox(matrixGroup); spin->setDecimals(6); spin->setRange(-1.0, 1.0); spin->setSingleStep(0.01); spin->setAlignment(Qt::AlignRight); spin->setButtonSymbols(QAbstractSpinBox::NoButtons); spin->setValue(r == c ? 1.0 : 0.0); matCells[r][c] = spin; matLayout->addWidget(spin, r + 1, c + 1); } } layout->addWidget(eulerGroup); layout->addWidget(matrixGroup); QHBoxLayout* actionLayout = new QHBoxLayout(); QPushButton* eulerToMat = new QPushButton("欧拉角 -> 方向向量矩阵", dialog); QPushButton* matToEuler = new QPushButton("方向向量矩阵 -> 欧拉角", dialog); QPushButton* closeButton = new QPushButton("关闭", dialog); actionLayout->addWidget(eulerToMat); actionLayout->addWidget(matToEuler); actionLayout->addWidget(closeButton); layout->addLayout(actionLayout); connect(eulerToMat, &QPushButton::clicked, dialog, [=]() { const EulerRotationOrder order = static_cast(comboOrder->currentData().toInt()); const QMatrix4x4 rotation = buildEulerRotationMatrix(order, eulerRx->value(), eulerRy->value(), eulerRz->value()); for (int r = 0; r < 3; ++r) { for (int c = 0; c < 3; ++c) { matCells[r][c]->setValue(rotation(r, c)); } } }); connect(matToEuler, &QPushButton::clicked, dialog, [=]() { QMatrix4x4 rotation; rotation.setToIdentity(); for (int r = 0; r < 3; ++r) { for (int c = 0; c < 3; ++c) { rotation(r, c) = static_cast(matCells[r][c]->value()); } } const EulerRotationOrder order = static_cast(comboOrder->currentData().toInt()); const QVector3D euler = rotationMatrixToEuler(order, rotation); eulerRx->setValue(euler.x()); eulerRy->setValue(euler.y()); eulerRz->setValue(euler.z()); }); connect(closeButton, &QPushButton::clicked, dialog, &QDialog::close); dialog->show(); } void CloudViewMainWindow::onPoseCompensation() { QDialog* dialog = new QDialog(this); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setWindowTitle("姿态补偿计算"); dialog->setWindowFlags(dialog->windowFlags() | Qt::Tool); QVBoxLayout* layout = new QVBoxLayout(dialog); QLabel* tip = new QLabel( "输入当前欧拉角和期望欧拉角(单位: 度),计算出补偿角。\n" "分量相减:补偿 = 期望 − 当前(仅小角度差/单轴旋转时近似有效,结果与顺序无关)\n" "矩阵补偿:补偿 = 提取(R_cur^T × R_des)\n" " · 输入顺序:当前/期望欧拉角的旋转顺序(取决于机器人/算法侧约定)\n" " · 补偿输出顺序:反解 R_comp 用的旋转顺序,要与下游施加补偿的约定一致\n" " (DetectPresenter::ApplyAxesRotation 硬编码 R = Rx·Ry·Rz,即 XYZ 内旋)", dialog); tip->setWordWrap(true); tip->setStyleSheet("color: gray;"); layout->addWidget(tip); auto fillEulerOrderItems = [](QComboBox* combo) { combo->addItem("ZYX (Yaw-Pitch-Roll)", static_cast(EulerRotationOrder::ZYX)); combo->addItem("XYZ (Roll-Pitch-Yaw)", static_cast(EulerRotationOrder::XYZ)); combo->addItem("ZXY (Yaw-Roll-Pitch)", static_cast(EulerRotationOrder::ZXY)); combo->addItem("YXZ (Pitch-Roll-Yaw)", static_cast(EulerRotationOrder::YXZ)); combo->addItem("XZY (Roll-Yaw-Pitch)", static_cast(EulerRotationOrder::XZY)); combo->addItem("YZX (Pitch-Yaw-Roll)", static_cast(EulerRotationOrder::YZX)); }; QComboBox* comboInputOrder = new QComboBox(dialog); fillEulerOrderItems(comboInputOrder); if (m_comboEulerOrder && m_comboEulerOrder->currentIndex() >= 0) { comboInputOrder->setCurrentIndex(m_comboEulerOrder->currentIndex()); } QComboBox* comboOutputOrder = new QComboBox(dialog); fillEulerOrderItems(comboOutputOrder); // 默认 XYZ,匹配 DetectPresenter::ApplyAxesRotation 中 R = Rx·Ry·Rz 的硬编码 const int outputXyzIndex = comboOutputOrder->findData(static_cast(EulerRotationOrder::XYZ)); if (outputXyzIndex >= 0) { comboOutputOrder->setCurrentIndex(outputXyzIndex); } QHBoxLayout* orderLayout = new QHBoxLayout(); orderLayout->addWidget(new QLabel("输入顺序:", dialog)); orderLayout->addWidget(comboInputOrder); orderLayout->addSpacing(20); orderLayout->addWidget(new QLabel("补偿输出顺序:", dialog)); orderLayout->addWidget(comboOutputOrder); orderLayout->addStretch(); layout->addLayout(orderLayout); QComboBox* comboMethod = new QComboBox(dialog); comboMethod->addItem("分量相减(期望 − 当前)"); comboMethod->addItem("矩阵补偿(R_cur^T × R_des)"); QHBoxLayout* methodLayout = new QHBoxLayout(); methodLayout->addWidget(new QLabel("计算方法:", dialog)); methodLayout->addWidget(comboMethod); methodLayout->addStretch(); layout->addLayout(methodLayout); auto makeAngleSpin = [](QWidget* parent) { QDoubleSpinBox* spin = new QDoubleSpinBox(parent); spin->setDecimals(6); spin->setRange(-3600.0, 3600.0); spin->setSingleStep(0.1); spin->setValue(0.0); spin->setAlignment(Qt::AlignRight); spin->setButtonSymbols(QAbstractSpinBox::NoButtons); return spin; }; QGroupBox* currentGroup = new QGroupBox("当前欧拉角 (deg)", dialog); QGridLayout* currentLayout = new QGridLayout(currentGroup); QDoubleSpinBox* curRx = makeAngleSpin(currentGroup); QDoubleSpinBox* curRy = makeAngleSpin(currentGroup); QDoubleSpinBox* curRz = makeAngleSpin(currentGroup); currentLayout->addWidget(new QLabel("RX:", currentGroup), 0, 0); currentLayout->addWidget(curRx, 0, 1); currentLayout->addWidget(new QLabel("RY:", currentGroup), 1, 0); currentLayout->addWidget(curRy, 1, 1); currentLayout->addWidget(new QLabel("RZ:", currentGroup), 2, 0); currentLayout->addWidget(curRz, 2, 1); QGroupBox* desiredGroup = new QGroupBox("期望欧拉角 (deg)", dialog); QGridLayout* desiredLayout = new QGridLayout(desiredGroup); QDoubleSpinBox* desRx = makeAngleSpin(desiredGroup); QDoubleSpinBox* desRy = makeAngleSpin(desiredGroup); QDoubleSpinBox* desRz = makeAngleSpin(desiredGroup); desiredLayout->addWidget(new QLabel("RX:", desiredGroup), 0, 0); desiredLayout->addWidget(desRx, 0, 1); desiredLayout->addWidget(new QLabel("RY:", desiredGroup), 1, 0); desiredLayout->addWidget(desRy, 1, 1); desiredLayout->addWidget(new QLabel("RZ:", desiredGroup), 2, 0); desiredLayout->addWidget(desRz, 2, 1); QGroupBox* resultGroup = new QGroupBox("补偿欧拉角 (deg)", dialog); QGridLayout* resultLayout = new QGridLayout(resultGroup); QLineEdit* resultRx = new QLineEdit(resultGroup); QLineEdit* resultRy = new QLineEdit(resultGroup); QLineEdit* resultRz = new QLineEdit(resultGroup); resultRx->setReadOnly(true); resultRy->setReadOnly(true); resultRz->setReadOnly(true); resultRx->setAlignment(Qt::AlignRight); resultRy->setAlignment(Qt::AlignRight); resultRz->setAlignment(Qt::AlignRight); resultRx->setStyleSheet("background-color: #f0f0f0; font-weight: bold;"); resultRy->setStyleSheet("background-color: #f0f0f0; font-weight: bold;"); resultRz->setStyleSheet("background-color: #f0f0f0; font-weight: bold;"); resultLayout->addWidget(new QLabel("RX:", resultGroup), 0, 0); resultLayout->addWidget(resultRx, 0, 1); resultLayout->addWidget(new QLabel("RY:", resultGroup), 1, 0); resultLayout->addWidget(resultRy, 1, 1); resultLayout->addWidget(new QLabel("RZ:", resultGroup), 2, 0); resultLayout->addWidget(resultRz, 2, 1); layout->addWidget(currentGroup); layout->addWidget(desiredGroup); layout->addWidget(resultGroup); QHBoxLayout* actionLayout = new QHBoxLayout(); QPushButton* calcButton = new QPushButton("计算补偿", dialog); QPushButton* closeButton = new QPushButton("关闭", dialog); actionLayout->addStretch(); actionLayout->addWidget(calcButton); actionLayout->addWidget(closeButton); layout->addLayout(actionLayout); connect(calcButton, &QPushButton::clicked, dialog, [=]() { if (comboMethod->currentIndex() == 0) { // 分量相减 resultRx->setText(QString::number(desRx->value() - curRx->value(), 'f', 6)); resultRy->setText(QString::number(desRy->value() - curRy->value(), 'f', 6)); resultRz->setText(QString::number(desRz->value() - curRz->value(), 'f', 6)); } else { // 矩阵补偿 const EulerRotationOrder inputOrder = static_cast(comboInputOrder->currentData().toInt()); const EulerRotationOrder outputOrder = static_cast(comboOutputOrder->currentData().toInt()); // 输入欧拉角按 inputOrder 还原成旋转矩阵 const QMatrix4x4 curMat = buildEulerRotationMatrix(inputOrder, curRx->value(), curRy->value(), curRz->value()); const QMatrix4x4 desMat = buildEulerRotationMatrix(inputOrder, desRx->value(), desRy->value(), desRz->value()); // R_des = R_cur * R_comp(工具坐标系补偿,R_comp = R_cur^T * R_des) const QMatrix4x4 compMat = curMat.transposed() * desMat; // 补偿矩阵按 outputOrder 反解(要与下游施加补偿的旋转顺序一致) const QVector3D euler = rotationMatrixToEuler(outputOrder, compMat); resultRx->setText(QString::number(euler.x(), 'f', 6)); resultRy->setText(QString::number(euler.y(), 'f', 6)); resultRz->setText(QString::number(euler.z(), 'f', 6)); } }); connect(closeButton, &QPushButton::clicked, dialog, &QDialog::close); dialog->show(); } 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)); } void CloudViewMainWindow::onPoint1CoordChanged() { // 读取编辑框中的坐标 bool ok = true; float x = m_editPoint1X->text().toFloat(&ok); if (!ok) { QMessageBox::warning(this, "错误", "点1 X 值无效"); return; } float y = m_editPoint1Y->text().toFloat(&ok); if (!ok) { QMessageBox::warning(this, "错误", "点1 Y 值无效"); return; } float z = m_editPoint1Z->text().toFloat(&ok); if (!ok) { QMessageBox::warning(this, "错误", "点1 Z 值无效"); return; } // 直接设置选中点坐标(无论是否已有鼠标选点) m_glWidget->setSelectedPointCoord(0, x, y, z); updateSelectedPointsDisplay(); // 如果启用了测距且有两个点,重新计算距离 auto updatedPoints = m_glWidget->getSelectedPoints(); if (m_glWidget->isMeasureDistanceEnabled() && updatedPoints.size() >= 2 && updatedPoints[1].valid) { float distance = m_glWidget->calculateDistance(updatedPoints[0], updatedPoints[1]) * 2.0f; m_lblDistance->setText(QString("%1 mm").arg(distance, 0, 'f', 3)); statusBar()->showMessage(QString("点1坐标已更新,距离: %1 mm").arg(distance, 0, 'f', 3)); } else { statusBar()->showMessage(QString("点1坐标已更新为 (%1, %2, %3)").arg(x, 0, 'f', 3).arg(y, 0, 'f', 3).arg(z, 0, 'f', 3)); } LOG_INFO("[CloudView] Point1 coord updated to (%.3f, %.3f, %.3f)\n", x, y, z); } void CloudViewMainWindow::onPoint2CoordChanged() { // 读取编辑框中的坐标 bool ok = true; float x = m_editPoint2X->text().toFloat(&ok); if (!ok) { QMessageBox::warning(this, "错误", "点2 X 值无效"); return; } float y = m_editPoint2Y->text().toFloat(&ok); if (!ok) { QMessageBox::warning(this, "错误", "点2 Y 值无效"); return; } float z = m_editPoint2Z->text().toFloat(&ok); if (!ok) { QMessageBox::warning(this, "错误", "点2 Z 值无效"); return; } // 直接设置选中点坐标(无论是否已有鼠标选点) m_glWidget->setSelectedPointCoord(1, x, y, z); updateSelectedPointsDisplay(); // 如果启用了测距,重新计算距离 auto updatedPoints = m_glWidget->getSelectedPoints(); if (m_glWidget->isMeasureDistanceEnabled() && updatedPoints.size() >= 2 && updatedPoints[0].valid) { float distance = m_glWidget->calculateDistance(updatedPoints[0], updatedPoints[1]); m_lblDistance->setText(QString("%1 mm").arg(distance, 0, 'f', 3)); statusBar()->showMessage(QString("点2坐标已更新,距离: %1 mm").arg(distance, 0, 'f', 3)); } else { statusBar()->showMessage(QString("点2坐标已更新为 (%1, %2, %3)").arg(x, 0, 'f', 3).arg(y, 0, 'f', 3).arg(z, 0, 'f', 3)); } LOG_INFO("[CloudView] Point2 coord updated to (%.3f, %.3f, %.3f)\n", x, y, z); }