#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 #include #include #include #include "VrLog.h" #include "LaserDataLoader.h" namespace { constexpr double kPi = 3.14159265358979323846; constexpr double kDegToRad = kPi / 180.0; constexpr float kCircleR = 1.0f; constexpr float kCircleG = 0.85f; constexpr float kCircleB = 0.0f; constexpr float kRectangleR = 1.0f; constexpr float kRectangleG = 0.25f; constexpr float kRectangleB = 0.9f; constexpr float kSurfaceR = 0.0f; constexpr float kSurfaceG = 0.8f; constexpr float kSurfaceB = 1.0f; constexpr float kSurfaceA = 0.5f; constexpr float kShapeCenterR = 1.0f; constexpr float kShapeCenterG = 0.1f; constexpr float kShapeCenterB = 0.1f; constexpr float kShapeCenterPointSize = 9.0f; 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; } void getShapeFrame(const PointCloudXYZ& cloud, QVector3D& center, QVector3D& size) { center = QVector3D(0.0f, 0.0f, 0.0f); size = QVector3D(100.0f, 100.0f, 100.0f); if (cloud.empty()) { return; } QVector3D minBound(FLT_MAX, FLT_MAX, FLT_MAX); QVector3D maxBound(-FLT_MAX, -FLT_MAX, -FLT_MAX); for (const auto& point : cloud.points) { minBound.setX(std::min(minBound.x(), point.x)); minBound.setY(std::min(minBound.y(), point.y)); minBound.setZ(std::min(minBound.z(), point.z)); maxBound.setX(std::max(maxBound.x(), point.x)); maxBound.setY(std::max(maxBound.y(), point.y)); maxBound.setZ(std::max(maxBound.z(), point.z)); } center = (minBound + maxBound) * 0.5f; size = maxBound - minBound; } QDoubleSpinBox* createShapeSpinBox(QWidget* parent, double value, double minValue = -1000000000.0, double maxValue = 1000000000.0) { QDoubleSpinBox* spin = new QDoubleSpinBox(parent); spin->setDecimals(6); spin->setRange(minValue, maxValue); spin->setValue(value); spin->setSingleStep(1.0); spin->setAlignment(Qt::AlignRight); spin->setButtonSymbols(QAbstractSpinBox::NoButtons); return spin; } EulerRotationOrder currentShapeEulerOrder(const QComboBox* combo) { if (combo && combo->currentIndex() >= 0) { return static_cast(combo->currentData().toInt()); } return EulerRotationOrder::ZYX; } QVector3D transformShapePoint(const QVector3D& center, const QMatrix4x4& rotation, const QVector3D& localPoint) { return center + rotation.map(localPoint); } QVector makeClosedShapeSegments(const QVector& points, float r, float g, float b) { QVector segments; if (points.size() < 2) { return segments; } segments.reserve(points.size()); for (int i = 0; i < points.size(); ++i) { const QVector3D& p1 = points[i]; const QVector3D& p2 = points[(i + 1) % points.size()]; segments.append(LineSegment(p1.x(), p1.y(), p1.z(), p2.x(), p2.y(), p2.z(), r, g, b, 3.0f)); } return segments; } QVector makeShapeCenterPoint(const QVector3D& center) { QVector points; points.append(BasicShapePoint(center.x(), center.y(), center.z(), kShapeCenterR, kShapeCenterG, kShapeCenterB, kShapeCenterPointSize)); return points; } QVector makeCircleSegments(const QVector3D& center, double rxDeg, double ryDeg, double rzDeg, float radius, EulerRotationOrder order) { QVector segments; if (radius <= 0.0f) { return segments; } const QMatrix4x4 rotation = buildEulerRotationMatrix(order, rxDeg, ryDeg, rzDeg); const int segmentCount = 96; segments.reserve(segmentCount); for (int i = 0; i < segmentCount; ++i) { const float a1 = static_cast(2.0 * kPi * i / segmentCount); const float a2 = static_cast(2.0 * kPi * (i + 1) / segmentCount); const QVector3D p1 = transformShapePoint( center, rotation, QVector3D(radius * std::cos(a1), radius * std::sin(a1), 0.0f)); const QVector3D p2 = transformShapePoint( center, rotation, QVector3D(radius * std::cos(a2), radius * std::sin(a2), 0.0f)); segments.append(LineSegment( p1.x(), p1.y(), p1.z(), p2.x(), p2.y(), p2.z(), kCircleR, kCircleG, kCircleB, 3.0f)); } return segments; } QVector makeRectangleSegments(const QVector3D& center, double rxDeg, double ryDeg, double rzDeg, float width, float height, EulerRotationOrder order) { if (width <= 0.0f || height <= 0.0f) { return {}; } QVector points; points.reserve(4); const QMatrix4x4 rotation = buildEulerRotationMatrix(order, rxDeg, ryDeg, rzDeg); const float halfWidth = width * 0.5f; const float halfHeight = height * 0.5f; points.append(transformShapePoint(center, rotation, QVector3D(-halfWidth, -halfHeight, 0.0f))); points.append(transformShapePoint(center, rotation, QVector3D( halfWidth, -halfHeight, 0.0f))); points.append(transformShapePoint(center, rotation, QVector3D( halfWidth, halfHeight, 0.0f))); points.append(transformShapePoint(center, rotation, QVector3D(-halfWidth, halfHeight, 0.0f))); return makeClosedShapeSegments(points, kRectangleR, kRectangleG, kRectangleB); } QVector makeSurfacePoints(const QVector3D& center, double rxDeg, double ryDeg, double rzDeg, float length, float width, EulerRotationOrder order) { QVector points; if (length <= 0.0f || width <= 0.0f) { return points; } const QMatrix4x4 rotation = buildEulerRotationMatrix(order, rxDeg, ryDeg, rzDeg); const float halfLength = length * 0.5f; const float halfWidth = width * 0.5f; points.reserve(4); points.append(transformShapePoint(center, rotation, QVector3D(-halfLength, -halfWidth, 0.0f))); points.append(transformShapePoint(center, rotation, QVector3D( halfLength, -halfWidth, 0.0f))); points.append(transformShapePoint(center, rotation, QVector3D( halfLength, halfWidth, 0.0f))); points.append(transformShapePoint(center, rotation, QVector3D(-halfLength, halfWidth, 0.0f))); return points; } QVector makeSurfaceQuads(const QVector3D& center, double rxDeg, double ryDeg, double rzDeg, float length, float width, EulerRotationOrder order) { QVector surfaces; const QVector points = makeSurfacePoints(center, rxDeg, ryDeg, rzDeg, length, width, order); if (points.size() != 4) { return surfaces; } surfaces.append(SurfaceQuad(points[0], points[1], points[2], points[3], kSurfaceR, kSurfaceG, kSurfaceB, kSurfaceA)); return surfaces; } QVector makeSurfaceOutlineSegments(const QVector3D& center, double rxDeg, double ryDeg, double rzDeg, float length, float width, EulerRotationOrder order) { const QVector points = makeSurfacePoints(center, rxDeg, ryDeg, rzDeg, length, width, order); return makeClosedShapeSegments(points, kSurfaceR, kSurfaceG, kSurfaceB); } void appendLineSegments(QVector& target, const QVector& source) { for (const LineSegment& segment : source) { target.append(segment); } } void appendBasicShapePoints(QVector& target, const QVector& source) { for (const BasicShapePoint& point : source) { target.append(point); } } void appendSurfaceQuads(QVector& target, const QVector& source) { for (const SurfaceQuad& surface : source) { target.append(surface); } } struct NumericRow { int lineNumber; QVector values; }; QVector parseNumericValuesFromLine(QString line) { const int hashIndex = line.indexOf('#'); if (hashIndex >= 0) { line = line.left(hashIndex); } const int slashIndex = line.indexOf("//"); if (slashIndex >= 0) { line = line.left(slashIndex); } line.replace(QRegExp("[,;:\\{\\}\\[\\]\\(\\)\"]"), " "); QVector values; const QStringList parts = line.split(QRegExp("\\s+"), QString::SkipEmptyParts); for (const QString& part : parts) { bool ok = false; const double value = part.toDouble(&ok); if (ok) { values.append(value); } } return values; } bool loadNumericRowsFromFile(const QString& fileName, QVector& rows, QString& errorMessage) { rows.clear(); QFile file(fileName); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { errorMessage = QString("无法打开文件: %1").arg(file.errorString()); return false; } QTextStream in(&file); int lineNumber = 0; while (!in.atEnd()) { ++lineNumber; const QVector values = parseNumericValuesFromLine(in.readLine()); if (values.isEmpty()) { continue; } rows.append(NumericRow{lineNumber, values}); } if (rows.isEmpty()) { errorMessage = "文件中没有可读取的数值"; return false; } return true; } bool makeCircleSegmentsFromRows(const QVector& rows, EulerRotationOrder order, QVector& segments, QVector& centerPoints, int& shapeCount, QString& errorMessage) { constexpr int kRecordSize = 7; segments.clear(); centerPoints.clear(); shapeCount = 0; if (rows.isEmpty()) { errorMessage = "圆数据格式应为每行一个 {x,y,z}-{r,p,y}-{r}"; return false; } for (const NumericRow& row : rows) { const QVector& values = row.values; if (values.size() != kRecordSize) { errorMessage = QString("第 %1 行圆数据格式应为 {x,y,z}-{r,p,y}-{r},共 7 个数值").arg(row.lineNumber); return false; } if (values[6] <= 0.0) { errorMessage = QString("第 %1 行圆半径必须大于 0").arg(row.lineNumber); return false; } const QVector3D center(static_cast(values[0]), static_cast(values[1]), static_cast(values[2])); appendLineSegments(segments, makeCircleSegments(center, values[3], values[4], values[5], static_cast(values[6]), order)); appendBasicShapePoints(centerPoints, makeShapeCenterPoint(center)); ++shapeCount; } return shapeCount > 0; } bool makeRectangleSegmentsFromRows(const QVector& rows, EulerRotationOrder order, QVector& segments, QVector& centerPoints, int& shapeCount, QString& errorMessage) { constexpr int kRecordSize = 8; segments.clear(); centerPoints.clear(); shapeCount = 0; if (rows.isEmpty()) { errorMessage = "矩形数据格式应为每行一个 {x,y,z}-{r,p,y}-{w,h}"; return false; } for (const NumericRow& row : rows) { const QVector& values = row.values; if (values.size() != kRecordSize) { errorMessage = QString("第 %1 行矩形数据格式应为 {x,y,z}-{r,p,y}-{w,h},共 8 个数值").arg(row.lineNumber); return false; } if (values[6] <= 0.0 || values[7] <= 0.0) { errorMessage = QString("第 %1 行矩形宽高必须大于 0").arg(row.lineNumber); return false; } const QVector3D center(static_cast(values[0]), static_cast(values[1]), static_cast(values[2])); appendLineSegments(segments, makeRectangleSegments(center, values[3], values[4], values[5], static_cast(values[6]), static_cast(values[7]), order)); appendBasicShapePoints(centerPoints, makeShapeCenterPoint(center)); ++shapeCount; } return shapeCount > 0; } bool makeSurfaceDataFromRows(const QVector& rows, EulerRotationOrder order, QVector& surfaces, QVector& outlines, int& shapeCount, QString& errorMessage) { surfaces.clear(); outlines.clear(); shapeCount = 0; if (rows.isEmpty()) { errorMessage = "面数据格式应为每行一个 {x,y,z}-{l,w} 或 {x,y,z}-{r,p,y}-{l,w}"; return false; } for (const NumericRow& row : rows) { const QVector& values = row.values; if (values.size() != 5 && values.size() != 8) { errorMessage = QString("第 %1 行面数据格式应为 {x,y,z}-{l,w} 或 {x,y,z}-{r,p,y}-{l,w}").arg(row.lineNumber); return false; } const bool hasPose = values.size() == 8; const int sizeIndex = hasPose ? 6 : 3; if (values[sizeIndex] <= 0.0 || values[sizeIndex + 1] <= 0.0) { errorMessage = QString("第 %1 行面长宽必须大于 0").arg(row.lineNumber); return false; } const QVector3D center(static_cast(values[0]), static_cast(values[1]), static_cast(values[2])); const double rx = hasPose ? values[3] : 0.0; const double ry = hasPose ? values[4] : 0.0; const double rz = hasPose ? values[5] : 0.0; const float length = static_cast(values[sizeIndex]); const float width = static_cast(values[sizeIndex + 1]); appendSurfaceQuads(surfaces, makeSurfaceQuads(center, rx, ry, rz, length, width, order)); appendLineSegments(outlines, makeSurfaceOutlineSegments(center, rx, ry, rz, length, width, order)); ++shapeCount; } return shapeCount > 0; } bool makeTriangleSegmentsFromRows(const QVector& rows, QVector& segments, int& shapeCount, QString& errorMessage) { constexpr int kRecordSize = 9; segments.clear(); shapeCount = 0; if (rows.isEmpty()) { errorMessage = "三角形数据格式应为每行一个 {x,y,z}-{x,y,z}-{x,y,z}"; return false; } for (const NumericRow& row : rows) { const QVector& values = row.values; if (values.size() != kRecordSize) { errorMessage = QString("第 %1 行三角形数据格式应为 {x,y,z}-{x,y,z}-{x,y,z},共 9 个数值").arg(row.lineNumber); return false; } QVector points; points.reserve(3); for (int i = 0; i < 3; ++i) { points.append(QVector3D(static_cast(values[i * 3]), static_cast(values[i * 3 + 1]), static_cast(values[i * 3 + 2]))); } appendLineSegments(segments, makeClosedShapeSegments(points, 0.2f, 1.0f, 0.2f)); ++shapeCount; } return shapeCount > 0; } } // namespace CloudViewMainWindow::CloudViewMainWindow(QWidget* parent) : QMainWindow(parent) , m_glWidget(nullptr) , m_converter(std::make_unique()) , m_lblDeltaXYZ(nullptr) , m_cloudCount(0) , m_currentLineNum(0) , m_currentLinePtNum(0) , m_linePointsDialog(nullptr) , m_linePointsTable(nullptr) , m_startupSequenceIndex(-1) , m_startupSequentialMode(false) { setupUI(); LOG_INFO("CloudViewMainWindow initialized\n"); } CloudViewMainWindow::~CloudViewMainWindow() { } void CloudViewMainWindow::buildStartupSequence(const StartupFiles& files) { m_startupSequence.clear(); m_startupSequenceIndex = -1; m_startupSequentialMode = false; auto appendFiles = [this](StartupFileType type, const QStringList& fileNames) { for (const QString& fileName : fileNames) { m_startupSequence.append(StartupItem{type, fileName}); } }; appendFiles(StartupFileType::Display, files.displayFiles); appendFiles(StartupFileType::BBox, files.bboxFiles); appendFiles(StartupFileType::Circle, files.circleFiles); appendFiles(StartupFileType::Rectangle, files.rectangleFiles); appendFiles(StartupFileType::Surface, files.surfaceFiles); appendFiles(StartupFileType::Triangle, files.triangleFiles); } bool CloudViewMainWindow::loadStartupItem(const StartupItem& item) { switch (item.type) { case StartupFileType::Display: return loadDisplayFile(item.fileName); case StartupFileType::BBox: return loadBoundingBoxFile(item.fileName); case StartupFileType::Circle: return loadCircleShapeFile(item.fileName); case StartupFileType::Rectangle: return loadRectangleShapeFile(item.fileName); case StartupFileType::Surface: return loadSurfaceShapeFile(item.fileName); case StartupFileType::Triangle: return loadTriangleShapeFile(item.fileName); } return false; } bool CloudViewMainWindow::loadCurrentStartupSequenceItem() { if (m_startupSequenceIndex < 0 || m_startupSequenceIndex >= m_startupSequence.size()) { return false; } const StartupItem& item = m_startupSequence[m_startupSequenceIndex]; const bool ok = loadStartupItem(item); if (ok) { fitViewToContent(); } else { LOG_WARN("[CloudView] Startup sequence load failed: %s\n", item.fileName.toStdString().c_str()); } statusBar()->showMessage(QString("序列显示 %1/%2:%3%4") .arg(m_startupSequenceIndex + 1) .arg(m_startupSequence.size()) .arg(QFileInfo(item.fileName).fileName()) .arg(ok ? "" : " 加载失败")); return ok; } bool CloudViewMainWindow::loadStartupFiles(const StartupFiles& files) { if (files.sequential) { buildStartupSequence(files); if (m_startupSequence.isEmpty()) { return true; } m_startupSequentialMode = true; m_startupSequenceIndex = 0; return loadCurrentStartupSequenceItem(); } const int requestedCount = files.displayFiles.size() + files.bboxFiles.size() + files.circleFiles.size() + files.rectangleFiles.size() + files.surfaceFiles.size() + files.triangleFiles.size(); if (requestedCount == 0) { return true; } int loadedCount = 0; QStringList failedFiles; auto recordLoadResult = [&](const QString& fileName, bool ok) { if (ok) { ++loadedCount; } else { failedFiles.append(fileName); } }; for (const QString& fileName : files.displayFiles) { recordLoadResult(fileName, loadDisplayFile(fileName)); } for (const QString& fileName : files.bboxFiles) { recordLoadResult(fileName, loadBoundingBoxFile(fileName)); } for (const QString& fileName : files.circleFiles) { recordLoadResult(fileName, loadCircleShapeFile(fileName)); } for (const QString& fileName : files.rectangleFiles) { recordLoadResult(fileName, loadRectangleShapeFile(fileName)); } for (const QString& fileName : files.surfaceFiles) { recordLoadResult(fileName, loadSurfaceShapeFile(fileName)); } for (const QString& fileName : files.triangleFiles) { recordLoadResult(fileName, loadTriangleShapeFile(fileName)); } if (loadedCount > 0) { fitViewToContent(); } if (!failedFiles.isEmpty()) { for (const QString& fileName : failedFiles) { LOG_WARN("[CloudView] Startup load failed: %s\n", fileName.toStdString().c_str()); } statusBar()->showMessage(QString("启动参数加载完成:%1/%2 成功,%3 失败") .arg(loadedCount) .arg(requestedCount) .arg(failedFiles.size())); return false; } statusBar()->showMessage(QString("启动参数加载完成:%1 个文件").arg(loadedCount)); return true; } bool CloudViewMainWindow::loadDisplayFile(const QString& fileName) { // 同一文件中可能同时包含点云和线段,两者都尝试加载。 const bool cloudOk = loadPointCloudFile(fileName); const bool segmentOk = loadSegmentFile(fileName); if (!cloudOk && !segmentOk) { statusBar()->showMessage(QString("加载失败:%1").arg(QFileInfo(fileName).fileName())); LOG_WARN("[CloudView] Failed to load display file: %s\n", fileName.toStdString().c_str()); return false; } return true; } bool CloudViewMainWindow::loadCircleShapeFile(const QString& fileName) { QVector rows; QString errorMessage; if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { statusBar()->showMessage(QString("加载圆数据失败:%1").arg(errorMessage)); LOG_WARN("[CloudView] Load circle file failed: %s, %s\n", fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); return false; } QVector segments; QVector centerPoints; int shapeCount = 0; if (!makeCircleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, centerPoints, shapeCount, errorMessage)) { statusBar()->showMessage(QString("加载圆数据失败:%1").arg(errorMessage)); LOG_WARN("[CloudView] Parse circle file failed: %s, %s\n", fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); return false; } showBasicShape(QString("%1 个圆").arg(shapeCount), segments, centerPoints); m_cloudList->addItem(QString("Circle (%1) - %2 个").arg(QFileInfo(fileName).fileName()).arg(shapeCount)); return true; } bool CloudViewMainWindow::loadRectangleShapeFile(const QString& fileName) { QVector rows; QString errorMessage; if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { statusBar()->showMessage(QString("加载矩形数据失败:%1").arg(errorMessage)); LOG_WARN("[CloudView] Load rectangle file failed: %s, %s\n", fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); return false; } QVector segments; QVector centerPoints; int shapeCount = 0; if (!makeRectangleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, centerPoints, shapeCount, errorMessage)) { statusBar()->showMessage(QString("加载矩形数据失败:%1").arg(errorMessage)); LOG_WARN("[CloudView] Parse rectangle file failed: %s, %s\n", fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); return false; } showBasicShape(QString("%1 个矩形").arg(shapeCount), segments, centerPoints); m_cloudList->addItem(QString("Rectangle (%1) - %2 个").arg(QFileInfo(fileName).fileName()).arg(shapeCount)); return true; } bool CloudViewMainWindow::loadSurfaceShapeFile(const QString& fileName) { QVector rows; QString errorMessage; if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { statusBar()->showMessage(QString("加载面数据失败:%1").arg(errorMessage)); LOG_WARN("[CloudView] Load surface file failed: %s, %s\n", fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); return false; } QVector surfaces; QVector outlines; int shapeCount = 0; if (!makeSurfaceDataFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), surfaces, outlines, shapeCount, errorMessage)) { statusBar()->showMessage(QString("加载面数据失败:%1").arg(errorMessage)); LOG_WARN("[CloudView] Parse surface file failed: %s, %s\n", fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); return false; } showBasicSurface(QString("%1 个面").arg(shapeCount), surfaces, outlines); m_cloudList->addItem(QString("Surface (%1) - %2 个").arg(QFileInfo(fileName).fileName()).arg(shapeCount)); return true; } bool CloudViewMainWindow::loadTriangleShapeFile(const QString& fileName) { QVector rows; QString errorMessage; if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { statusBar()->showMessage(QString("加载三角形数据失败:%1").arg(errorMessage)); LOG_WARN("[CloudView] Load triangle file failed: %s, %s\n", fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); return false; } QVector segments; int shapeCount = 0; if (!makeTriangleSegmentsFromRows(rows, segments, shapeCount, errorMessage)) { statusBar()->showMessage(QString("加载三角形数据失败:%1").arg(errorMessage)); LOG_WARN("[CloudView] Parse triangle file failed: %s, %s\n", fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); return false; } showBasicShape(QString("%1 个三角形").arg(shapeCount), segments); m_cloudList->addItem(QString("Triangle (%1) - %2 个").arg(QFileInfo(fileName).fileName()).arg(shapeCount)); return true; } void CloudViewMainWindow::fitViewToContent() { if (m_glWidget) { m_glWidget->fitToContent(); } } 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::closeEvent(QCloseEvent* event) { if (m_startupSequentialMode && m_startupSequenceIndex + 1 < m_startupSequence.size()) { event->ignore(); ++m_startupSequenceIndex; onClearAll(); loadCurrentStartupSequenceItem(); return; } m_startupSequentialMode = false; QMainWindow::closeEvent(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); fileMenu->addSeparator(); QAction* actBatchConvert = fileMenu->addAction("批量转换txt->ply..."); connect(actBatchConvert, &QAction::triggered, this, &CloudViewMainWindow::onBatchConvertTxtToPly); // ── 点云菜单 ── 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* imageToolMenu = menuBar()->addMenu("图形工具"); QAction* actOpenPosePoint = imageToolMenu->addAction("点(带姿态)"); connect(actOpenPosePoint, &QAction::triggered, this, &CloudViewMainWindow::onOpenPoseFile); imageToolMenu->addSeparator(); QAction* actCircleShape = imageToolMenu->addAction("圆"); connect(actCircleShape, &QAction::triggered, this, &CloudViewMainWindow::onShowCircleShape); QAction* actRectangleShape = imageToolMenu->addAction("矩形"); connect(actRectangleShape, &QAction::triggered, this, &CloudViewMainWindow::onShowRectangleShape); QAction* actSurfaceShape = imageToolMenu->addAction("面"); connect(actSurfaceShape, &QAction::triggered, this, &CloudViewMainWindow::onShowSurfaceShape); QAction* actTriangleShape = imageToolMenu->addAction("三角形"); connect(actTriangleShape, &QAction::triggered, this, &CloudViewMainWindow::onShowTriangleShape); 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_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* fallbackText; 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); const QIcon icon(preset.iconPath); if (icon.isNull()) { btn->setText(QString::fromUtf8(preset.fallbackText)); } else { btn->setIcon(icon); 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: --"); clearMeasureResultDisplay(); 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); QHBoxLayout* deltaLayout = new QHBoxLayout(); QLabel* lblDeltaTitle = new QLabel("差值:", group); lblDeltaTitle->setStyleSheet("font-size: 10px;"); m_lblDeltaXYZ = new QLabel("--", group); m_lblDeltaXYZ->setStyleSheet("font-weight: bold; color: #1f6feb; font-size: 13px;"); deltaLayout->addWidget(lblDeltaTitle); deltaLayout->addWidget(m_lblDeltaXYZ, 1); layout->addLayout(deltaLayout); 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 *.dat);;PCD 文件 (*.pcd);;PLY 文件 (*.ply);;TXT 文件 (*.txt);;DAT 文件 (*.dat);;所有文件 (*.*)" ); if (fileName.isEmpty()) { return; } if (!loadDisplayFile(fileName)) { 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->clearBasicShapeSegments(); m_glWidget->clearPosePoints(); m_lblPoint1->setText("点1: --"); m_lblPoint2->setText("点2: --"); clearMeasureResultDisplay(); 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::showBasicShape(const QString& shapeName, const QVector& segments, const QVector& points) { if (segments.isEmpty() && points.isEmpty()) { return; } if (!segments.isEmpty()) { m_glWidget->appendBasicShapeSegments(segments); } if (!points.isEmpty()) { m_glWidget->appendBasicShapePoints(points); } m_glWidget->fitToContent(); statusBar()->showMessage(QString("已添加 %1").arg(shapeName)); LOG_INFO("[CloudView] Add basic shape: %s, segments=%d, points=%d\n", shapeName.toStdString().c_str(), segments.size(), points.size()); } void CloudViewMainWindow::showBasicSurface(const QString& shapeName, const QVector& surfaces, const QVector& outlines) { if (surfaces.isEmpty()) { return; } m_glWidget->appendBasicShapeSurfaces(surfaces); m_glWidget->appendBasicShapeSegments(outlines); m_glWidget->fitToContent(); statusBar()->showMessage(QString("已添加 %1").arg(shapeName)); LOG_INFO("[CloudView] Add basic surface: %s, surfaces=%d, outlines=%d\n", shapeName.toStdString().c_str(), surfaces.size(), outlines.size()); } void CloudViewMainWindow::onShowCircleShape() { QVector3D center; QVector3D size; getShapeFrame(m_originalCloud, center, size); const double defaultRadius = std::max(1.0f, std::max(size.x(), size.y()) * 0.25f); QDialog dialog(this); dialog.setWindowTitle("圆"); dialog.setModal(true); QVBoxLayout* layout = new QVBoxLayout(&dialog); QLabel* tip = new QLabel("输入圆心、姿态和半径后点击添加;文件每行一个:{x,y,z}-{r,p,y}-{r}。", &dialog); tip->setWordWrap(true); tip->setStyleSheet("color: gray;"); layout->addWidget(tip); QGroupBox* centerGroup = new QGroupBox("圆心", &dialog); QFormLayout* centerLayout = new QFormLayout(centerGroup); QDoubleSpinBox* centerX = createShapeSpinBox(centerGroup, center.x()); QDoubleSpinBox* centerY = createShapeSpinBox(centerGroup, center.y()); QDoubleSpinBox* centerZ = createShapeSpinBox(centerGroup, center.z()); centerLayout->addRow("X:", centerX); centerLayout->addRow("Y:", centerY); centerLayout->addRow("Z:", centerZ); layout->addWidget(centerGroup); QGroupBox* poseGroup = new QGroupBox("姿态", &dialog); QFormLayout* poseLayout = new QFormLayout(poseGroup); QDoubleSpinBox* poseR = createShapeSpinBox(poseGroup, 0.0); QDoubleSpinBox* poseP = createShapeSpinBox(poseGroup, 0.0); QDoubleSpinBox* poseY = createShapeSpinBox(poseGroup, 0.0); poseLayout->addRow("R/RX:", poseR); poseLayout->addRow("P/RY:", poseP); poseLayout->addRow("Y/RZ:", poseY); layout->addWidget(poseGroup); QGroupBox* radiusGroup = new QGroupBox("尺寸", &dialog); QFormLayout* radiusLayout = new QFormLayout(radiusGroup); QDoubleSpinBox* radius = createShapeSpinBox(radiusGroup, defaultRadius, 0.000001); radiusLayout->addRow("半径:", radius); layout->addWidget(radiusGroup); auto addCurrentCircle = [this, centerX, centerY, centerZ, poseR, poseP, poseY, radius]() { const QVector3D inputCenter(static_cast(centerX->value()), static_cast(centerY->value()), static_cast(centerZ->value())); showBasicShape("圆", makeCircleSegments(inputCenter, poseR->value(), poseP->value(), poseY->value(), static_cast(radius->value()), currentShapeEulerOrder(m_comboEulerOrder)), makeShapeCenterPoint(inputCenter)); }; QPushButton* loadButton = new QPushButton("加载文件", &dialog); connect(loadButton, &QPushButton::clicked, this, [this, centerX, centerY, centerZ, poseR, poseP, poseY, radius]() { const QString fileName = QFileDialog::getOpenFileName( this, "加载圆数据", QString(), "数据文件 (*.txt *.json *.dat);;所有文件 (*.*)"); if (fileName.isEmpty()) { return; } QVector rows; QString errorMessage; if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { QMessageBox::warning(this, "错误", errorMessage); return; } QVector segments; QVector centerPoints; int shapeCount = 0; if (!makeCircleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, centerPoints, shapeCount, errorMessage)) { QMessageBox::warning(this, "错误", errorMessage); return; } if (shapeCount == 1) { const QVector& values = rows.first().values; centerX->setValue(values[0]); centerY->setValue(values[1]); centerZ->setValue(values[2]); poseR->setValue(values[3]); poseP->setValue(values[4]); poseY->setValue(values[5]); radius->setValue(values[6]); } showBasicShape(QString("%1 个圆").arg(shapeCount), segments, centerPoints); }); QPushButton* addButton = new QPushButton("添加", &dialog); connect(addButton, &QPushButton::clicked, this, addCurrentCircle); QHBoxLayout* actionLayout = new QHBoxLayout(); actionLayout->addWidget(loadButton); actionLayout->addWidget(addButton); actionLayout->addStretch(); layout->addLayout(actionLayout); QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, &dialog); connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); layout->addWidget(buttonBox); dialog.exec(); } void CloudViewMainWindow::onShowRectangleShape() { QVector3D center; QVector3D size; getShapeFrame(m_originalCloud, center, size); const double defaultWidth = std::max(1.0f, size.x() * 0.5f); const double defaultHeight = std::max(1.0f, size.y() * 0.5f); QDialog dialog(this); dialog.setWindowTitle("矩形"); dialog.setModal(true); QVBoxLayout* layout = new QVBoxLayout(&dialog); QLabel* tip = new QLabel("输入矩形中心、姿态和宽高后点击添加;文件每行一个:{x,y,z}-{r,p,y}-{w,h}。", &dialog); tip->setWordWrap(true); tip->setStyleSheet("color: gray;"); layout->addWidget(tip); QGroupBox* centerGroup = new QGroupBox("中心", &dialog); QFormLayout* centerLayout = new QFormLayout(centerGroup); QDoubleSpinBox* centerX = createShapeSpinBox(centerGroup, center.x()); QDoubleSpinBox* centerY = createShapeSpinBox(centerGroup, center.y()); QDoubleSpinBox* centerZ = createShapeSpinBox(centerGroup, center.z()); centerLayout->addRow("X:", centerX); centerLayout->addRow("Y:", centerY); centerLayout->addRow("Z:", centerZ); layout->addWidget(centerGroup); QGroupBox* poseGroup = new QGroupBox("姿态", &dialog); QFormLayout* poseLayout = new QFormLayout(poseGroup); QDoubleSpinBox* poseR = createShapeSpinBox(poseGroup, 0.0); QDoubleSpinBox* poseP = createShapeSpinBox(poseGroup, 0.0); QDoubleSpinBox* poseY = createShapeSpinBox(poseGroup, 0.0); poseLayout->addRow("R/RX:", poseR); poseLayout->addRow("P/RY:", poseP); poseLayout->addRow("Y/RZ:", poseY); layout->addWidget(poseGroup); QGroupBox* sizeGroup = new QGroupBox("尺寸", &dialog); QFormLayout* sizeLayout = new QFormLayout(sizeGroup); QDoubleSpinBox* width = createShapeSpinBox(sizeGroup, defaultWidth, 0.000001); QDoubleSpinBox* height = createShapeSpinBox(sizeGroup, defaultHeight, 0.000001); sizeLayout->addRow("宽:", width); sizeLayout->addRow("高:", height); layout->addWidget(sizeGroup); auto addCurrentRectangle = [this, centerX, centerY, centerZ, poseR, poseP, poseY, width, height]() { const QVector3D inputCenter(static_cast(centerX->value()), static_cast(centerY->value()), static_cast(centerZ->value())); showBasicShape("矩形", makeRectangleSegments(inputCenter, poseR->value(), poseP->value(), poseY->value(), static_cast(width->value()), static_cast(height->value()), currentShapeEulerOrder(m_comboEulerOrder)), makeShapeCenterPoint(inputCenter)); }; QPushButton* loadButton = new QPushButton("加载文件", &dialog); connect(loadButton, &QPushButton::clicked, this, [this, centerX, centerY, centerZ, poseR, poseP, poseY, width, height]() { const QString fileName = QFileDialog::getOpenFileName( this, "加载矩形数据", QString(), "数据文件 (*.txt *.json *.dat);;所有文件 (*.*)"); if (fileName.isEmpty()) { return; } QVector rows; QString errorMessage; if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { QMessageBox::warning(this, "错误", errorMessage); return; } QVector segments; QVector centerPoints; int shapeCount = 0; if (!makeRectangleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, centerPoints, shapeCount, errorMessage)) { QMessageBox::warning(this, "错误", errorMessage); return; } if (shapeCount == 1) { const QVector& values = rows.first().values; centerX->setValue(values[0]); centerY->setValue(values[1]); centerZ->setValue(values[2]); poseR->setValue(values[3]); poseP->setValue(values[4]); poseY->setValue(values[5]); width->setValue(values[6]); height->setValue(values[7]); } showBasicShape(QString("%1 个矩形").arg(shapeCount), segments, centerPoints); }); QPushButton* addButton = new QPushButton("添加", &dialog); connect(addButton, &QPushButton::clicked, this, addCurrentRectangle); QHBoxLayout* actionLayout = new QHBoxLayout(); actionLayout->addWidget(loadButton); actionLayout->addWidget(addButton); actionLayout->addStretch(); layout->addLayout(actionLayout); QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, &dialog); connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); layout->addWidget(buttonBox); dialog.exec(); } void CloudViewMainWindow::onShowSurfaceShape() { QVector3D center; QVector3D size; getShapeFrame(m_originalCloud, center, size); const double defaultLength = std::max(1.0f, size.x() * 0.5f); const double defaultWidth = std::max(1.0f, size.y() * 0.5f); QDialog dialog(this); dialog.setWindowTitle("面"); dialog.setModal(true); QVBoxLayout* layout = new QVBoxLayout(&dialog); QLabel* tip = new QLabel("输入中心、姿态和长宽后点击添加;文件每行一个:{x,y,z}-{l,w} 或 {x,y,z}-{r,p,y}-{l,w}。", &dialog); tip->setWordWrap(true); tip->setStyleSheet("color: gray;"); layout->addWidget(tip); QGroupBox* centerGroup = new QGroupBox("中心", &dialog); QFormLayout* centerLayout = new QFormLayout(centerGroup); QDoubleSpinBox* centerX = createShapeSpinBox(centerGroup, center.x()); QDoubleSpinBox* centerY = createShapeSpinBox(centerGroup, center.y()); QDoubleSpinBox* centerZ = createShapeSpinBox(centerGroup, center.z()); centerLayout->addRow("X:", centerX); centerLayout->addRow("Y:", centerY); centerLayout->addRow("Z:", centerZ); layout->addWidget(centerGroup); QGroupBox* poseGroup = new QGroupBox("姿态", &dialog); QFormLayout* poseLayout = new QFormLayout(poseGroup); QDoubleSpinBox* poseR = createShapeSpinBox(poseGroup, 0.0); QDoubleSpinBox* poseP = createShapeSpinBox(poseGroup, 0.0); QDoubleSpinBox* poseY = createShapeSpinBox(poseGroup, 0.0); poseLayout->addRow("R/RX:", poseR); poseLayout->addRow("P/RY:", poseP); poseLayout->addRow("Y/RZ:", poseY); layout->addWidget(poseGroup); QGroupBox* sizeGroup = new QGroupBox("尺寸", &dialog); QFormLayout* sizeLayout = new QFormLayout(sizeGroup); QDoubleSpinBox* length = createShapeSpinBox(sizeGroup, defaultLength, 0.000001); QDoubleSpinBox* width = createShapeSpinBox(sizeGroup, defaultWidth, 0.000001); sizeLayout->addRow("长:", length); sizeLayout->addRow("宽:", width); layout->addWidget(sizeGroup); auto addCurrentSurface = [this, centerX, centerY, centerZ, poseR, poseP, poseY, length, width]() { const QVector3D inputCenter(static_cast(centerX->value()), static_cast(centerY->value()), static_cast(centerZ->value())); const EulerRotationOrder order = currentShapeEulerOrder(m_comboEulerOrder); const QVector surfaces = makeSurfaceQuads(inputCenter, poseR->value(), poseP->value(), poseY->value(), static_cast(length->value()), static_cast(width->value()), order); const QVector outlines = makeSurfaceOutlineSegments(inputCenter, poseR->value(), poseP->value(), poseY->value(), static_cast(length->value()), static_cast(width->value()), order); showBasicSurface("面", surfaces, outlines); }; QPushButton* loadButton = new QPushButton("加载文件", &dialog); connect(loadButton, &QPushButton::clicked, this, [this, centerX, centerY, centerZ, poseR, poseP, poseY, length, width]() { const QString fileName = QFileDialog::getOpenFileName( this, "加载面数据", QString(), "数据文件 (*.txt *.json *.dat);;所有文件 (*.*)"); if (fileName.isEmpty()) { return; } QVector rows; QString errorMessage; if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { QMessageBox::warning(this, "错误", errorMessage); return; } QVector surfaces; QVector outlines; int shapeCount = 0; if (!makeSurfaceDataFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), surfaces, outlines, shapeCount, errorMessage)) { QMessageBox::warning(this, "错误", errorMessage); return; } if (shapeCount == 1) { const QVector& values = rows.first().values; const bool hasPose = values.size() == 8; centerX->setValue(values[0]); centerY->setValue(values[1]); centerZ->setValue(values[2]); poseR->setValue(hasPose ? values[3] : 0.0); poseP->setValue(hasPose ? values[4] : 0.0); poseY->setValue(hasPose ? values[5] : 0.0); length->setValue(hasPose ? values[6] : values[3]); width->setValue(hasPose ? values[7] : values[4]); } showBasicSurface(QString("%1 个面").arg(shapeCount), surfaces, outlines); }); QPushButton* addButton = new QPushButton("添加", &dialog); connect(addButton, &QPushButton::clicked, this, addCurrentSurface); QHBoxLayout* actionLayout = new QHBoxLayout(); actionLayout->addWidget(loadButton); actionLayout->addWidget(addButton); actionLayout->addStretch(); layout->addLayout(actionLayout); QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, &dialog); connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); layout->addWidget(buttonBox); dialog.exec(); } void CloudViewMainWindow::onShowTriangleShape() { QVector3D center; QVector3D size; getShapeFrame(m_originalCloud, center, size); const float radius = std::max(1.0f, std::max(size.x(), size.y()) * 0.3f); const QVector3D defaultP1(center.x(), center.y() - radius, center.z()); const QVector3D defaultP2(center.x() + radius * 0.8660254f, center.y() + radius * 0.5f, center.z()); const QVector3D defaultP3(center.x() - radius * 0.8660254f, center.y() + radius * 0.5f, center.z()); QDialog dialog(this); dialog.setWindowTitle("三角形"); dialog.setModal(true); QVBoxLayout* layout = new QVBoxLayout(&dialog); QLabel* tip = new QLabel("输入三个顶点后点击添加;文件每行一个:{x,y,z}-{x,y,z}-{x,y,z}。", &dialog); tip->setWordWrap(true); tip->setStyleSheet("color: gray;"); layout->addWidget(tip); auto addPointGroup = [&](const QString& title, const QVector3D& value, QDoubleSpinBox*& x, QDoubleSpinBox*& y, QDoubleSpinBox*& z) { QGroupBox* group = new QGroupBox(title, &dialog); QFormLayout* form = new QFormLayout(group); x = createShapeSpinBox(group, value.x()); y = createShapeSpinBox(group, value.y()); z = createShapeSpinBox(group, value.z()); form->addRow("X:", x); form->addRow("Y:", y); form->addRow("Z:", z); layout->addWidget(group); }; QDoubleSpinBox* x1 = nullptr; QDoubleSpinBox* y1 = nullptr; QDoubleSpinBox* z1 = nullptr; QDoubleSpinBox* x2 = nullptr; QDoubleSpinBox* y2 = nullptr; QDoubleSpinBox* z2 = nullptr; QDoubleSpinBox* x3 = nullptr; QDoubleSpinBox* y3 = nullptr; QDoubleSpinBox* z3 = nullptr; addPointGroup("点1", defaultP1, x1, y1, z1); addPointGroup("点2", defaultP2, x2, y2, z2); addPointGroup("点3", defaultP3, x3, y3, z3); auto addCurrentTriangle = [this, x1, y1, z1, x2, y2, z2, x3, y3, z3]() { QVector points; points.reserve(3); points.append(QVector3D(static_cast(x1->value()), static_cast(y1->value()), static_cast(z1->value()))); points.append(QVector3D(static_cast(x2->value()), static_cast(y2->value()), static_cast(z2->value()))); points.append(QVector3D(static_cast(x3->value()), static_cast(y3->value()), static_cast(z3->value()))); showBasicShape("三角形", makeClosedShapeSegments(points, 0.2f, 1.0f, 0.2f)); }; QPushButton* loadButton = new QPushButton("加载文件", &dialog); connect(loadButton, &QPushButton::clicked, this, [this, x1, y1, z1, x2, y2, z2, x3, y3, z3]() { const QString fileName = QFileDialog::getOpenFileName( this, "加载三角形数据", QString(), "数据文件 (*.txt *.json *.dat);;所有文件 (*.*)"); if (fileName.isEmpty()) { return; } QVector rows; QString errorMessage; if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { QMessageBox::warning(this, "错误", errorMessage); return; } QVector segments; int shapeCount = 0; if (!makeTriangleSegmentsFromRows(rows, segments, shapeCount, errorMessage)) { QMessageBox::warning(this, "错误", errorMessage); return; } if (shapeCount == 1) { const QVector& values = rows.first().values; x1->setValue(values[0]); y1->setValue(values[1]); z1->setValue(values[2]); x2->setValue(values[3]); y2->setValue(values[4]); z2->setValue(values[5]); x3->setValue(values[6]); y3->setValue(values[7]); z3->setValue(values[8]); } showBasicShape(QString("%1 个三角形").arg(shapeCount), segments); }); QPushButton* addButton = new QPushButton("添加", &dialog); connect(addButton, &QPushButton::clicked, this, addCurrentTriangle); QHBoxLayout* actionLayout = new QHBoxLayout(); actionLayout->addWidget(loadButton); actionLayout->addWidget(addButton); actionLayout->addStretch(); layout->addLayout(actionLayout); QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, &dialog); connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); layout->addWidget(buttonBox); dialog.exec(); } void CloudViewMainWindow::onClearAll() { m_glWidget->clearPointClouds(); m_glWidget->clearLineSegments(); m_glWidget->clearBasicShapeSegments(); 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: --"); clearMeasureResultDisplay(); 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: --"); clearMeasureResultDisplay(); 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(); updateMeasureResultDisplay(p1, p2, distance, "测量距离"); } void CloudViewMainWindow::clearMeasureResultDisplay() { m_lblDistance->setText("--"); if (m_lblDeltaXYZ) { m_lblDeltaXYZ->setText("--"); } } void CloudViewMainWindow::updateMeasureResultDisplay(const SelectedPointInfo& p1, const SelectedPointInfo& p2, float distance, const QString& statusPrefix) { const float dx = p2.x - p1.x; const float dy = p2.y - p1.y; const float dz = p2.z - p1.z; m_lblDistance->setText(QString("%1 mm").arg(distance, 0, 'f', 3)); m_lblDeltaXYZ->setText(QString("X:%1 mm Y:%2 mm Z:%3 mm") .arg(dx, 0, 'f', 3) .arg(dy, 0, 'f', 3) .arg(dz, 0, 'f', 3)); statusBar()->showMessage(QString("%1: %2 mm | X:%3 Y:%4 Z:%5") .arg(statusPrefix) .arg(distance, 0, 'f', 3) .arg(dx, 0, 'f', 3) .arg(dy, 0, 'f', 3) .arg(dz, 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("--"); } if (selectedPoints.size() < 2 || !selectedPoints[0].valid || !selectedPoints[1].valid) { clearMeasureResultDisplay(); } } 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::onBatchConvertTxtToPly() { // 选择源文件夹 const QString srcDir = QFileDialog::getExistingDirectory( this, "选择源文件夹(包含 txt 点云文件)", QString(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); if (srcDir.isEmpty()) return; // 选择目标文件夹 const QString dstDir = QFileDialog::getExistingDirectory( this, "选择目标文件夹(PLY 文件输出位置)", srcDir, QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); if (dstDir.isEmpty()) return; // 递归收集所有 .txt 文件及其相对路径 struct FileEntry { QString absolutePath; QString relativePath; // 相对于源文件夹的路径,用于构建目标路径 }; QVector fileEntries; { QDirIterator it(srcDir, QStringList("*.txt"), QDir::Files, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks); while (it.hasNext()) { it.next(); FileEntry entry; entry.absolutePath = it.filePath(); // 计算相对路径(文件路径去掉源目录前缀) entry.relativePath = QDir(srcDir).relativeFilePath(it.filePath()); fileEntries.append(entry); } } if (fileEntries.isEmpty()) { QMessageBox::information(this, "提示", QString("源文件夹及其子目录中没有找到 .txt 文件:\n%1").arg(srcDir)); return; } // 按相对路径排序 std::sort(fileEntries.begin(), fileEntries.end(), [](const FileEntry& a, const FileEntry& b) { return a.relativePath < b.relativePath; }); // 进度对话框 QProgressDialog progress("正在批量转换 txt -> ply...", "取消", 0, fileEntries.size(), this); progress.setWindowTitle("批量转换"); progress.setWindowModality(Qt::WindowModal); progress.setMinimumDuration(0); progress.setAutoClose(true); // 使用独立的转换器,避免干扰主窗口状态 PointCloudConverter converter; int successCount = 0; int failCount = 0; int skipCount = 0; QStringList failedFiles; const float kZeroEps = 1e-6f; for (int i = 0; i < fileEntries.size(); ++i) { if (progress.wasCanceled()) { statusBar()->showMessage("批量转换已取消"); break; } const FileEntry& entry = fileEntries[i]; const QString& srcPath = entry.absolutePath; const QString& relPath = entry.relativePath; // 构建目标路径:目标文件夹 + 相对路径,扩展名改为 .ply const QString baseName = QFileInfo(relPath).completeBaseName(); const QString relDir = QFileInfo(relPath).path(); // 相对路径中的目录部分 const QString dstRelPath = (relDir == "." ? baseName + ".ply" : relDir + "/" + baseName + ".ply"); const QString dstPath = dstDir + "/" + dstRelPath; // 确保目标子目录存在 const QString dstParentDir = QFileInfo(dstPath).absolutePath(); if (!QDir().mkpath(dstParentDir)) { failCount++; failedFiles.append(QString("%1 (无法创建目标目录)").arg(relPath)); LOG_WARN("[CloudView] Batch convert: failed to create dir %s\n", dstParentDir.toStdString().c_str()); continue; } // 显示简短的相对路径便于阅读 const QString displayPath = relPath.length() > 60 ? ("..." + relPath.right(57)) : relPath; progress.setLabelText(QString("正在转换: %1 (%2/%3)") .arg(displayPath).arg(i + 1).arg(fileEntries.size())); progress.setValue(i); QCoreApplication::processEvents(); // 加载 txt 点云 PointCloudXYZ cloud; int loadRet = converter.loadFromTxt(srcPath.toStdString(), cloud); if (loadRet != 0) { failCount++; failedFiles.append(QString("%1 (加载失败: %2)") .arg(relPath) .arg(QString::fromStdString(converter.getLastError()))); LOG_WARN("[CloudView] Batch convert: failed to load %s\n", srcPath.toStdString().c_str()); continue; } // 过滤掉零点,构建用于保存的点云 PointCloudXYZ filteredCloud; filteredCloud.reserve(cloud.size()); for (const auto& pt : cloud.points) { if (std::fabs(pt.x) < kZeroEps && std::fabs(pt.y) < kZeroEps && std::fabs(pt.z) < kZeroEps) continue; filteredCloud.push_back(pt); } if (filteredCloud.empty()) { failCount++; failedFiles.append(QString("%1 (无有效点)").arg(relPath)); LOG_WARN("[CloudView] Batch convert: no valid points in %s\n", srcPath.toStdString().c_str()); continue; } // 保存为 PLY int saveRet = converter.saveToPly(dstPath.toStdString(), filteredCloud); if (saveRet != 0) { failCount++; failedFiles.append(QString("%1 (保存失败: %2)") .arg(relPath) .arg(QString::fromStdString(converter.getLastError()))); LOG_WARN("[CloudView] Batch convert: failed to save %s\n", dstPath.toStdString().c_str()); continue; } successCount++; LOG_INFO("[CloudView] Batch convert: %s -> %s (%zu points)\n", srcPath.toStdString().c_str(), dstPath.toStdString().c_str(), filteredCloud.size()); } progress.setValue(fileEntries.size()); // 汇总结果 const int totalCount = fileEntries.size(); QString summary = QString("批量转换完成!\n\n" "总文件数: %1\n" "成功: %2\n" "失败: %3") .arg(totalCount) .arg(successCount) .arg(failCount); if (skipCount > 0) { summary += QString("\n跳过: %1").arg(skipCount); } if (!failedFiles.isEmpty()) { summary += "\n\n失败文件:\n"; // 最多显示前 10 个失败文件 const int maxShow = std::min(failedFiles.size(), 10); for (int i = 0; i < maxShow; ++i) { summary += QString(" • %1\n").arg(failedFiles[i]); } if (failedFiles.size() > maxShow) { summary += QString(" ... 还有 %1 个\n") .arg(failedFiles.size() - maxShow); } } QMessageBox::information(this, "转换结果", summary); statusBar()->showMessage( QString("批量转换完成: %1 成功, %2 失败").arg(successCount).arg(failCount)); } 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) { const float distance = m_glWidget->calculateDistance(updatedPoints[0], updatedPoints[1]); updateMeasureResultDisplay(updatedPoints[0], updatedPoints[1], distance, "点1坐标已更新"); } 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) { const float distance = m_glWidget->calculateDistance(updatedPoints[0], updatedPoints[1]); updateMeasureResultDisplay(updatedPoints[0], updatedPoints[1], distance, "点2坐标已更新"); } 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); }