diff --git a/CloudView/Inc/CloudViewMainWindow.h b/CloudView/Inc/CloudViewMainWindow.h index eabe549..73cda7e 100644 --- a/CloudView/Inc/CloudViewMainWindow.h +++ b/CloudView/Inc/CloudViewMainWindow.h @@ -17,17 +17,17 @@ #include #include #include -#include -#include -#include -#include +#include +#include +#include +#include -#include "CloudView3D.h" -#include "PointCloudConverter.h" +#include "CloudView3D.h" +#include "PointCloudConverter.h" -class QTextEdit; -class QTableWidget; -class QCloseEvent; +class QTextEdit; +class QTableWidget; +class QCloseEvent; /** * @brief 点云查看器主窗口 @@ -37,55 +37,55 @@ class CloudViewMainWindow : public QMainWindow { Q_OBJECT -public: - struct StartupFiles - { - QStringList displayFiles; - QStringList bboxFiles; - QStringList circleFiles; - QStringList rectangleFiles; - QStringList surfaceFiles; - QStringList triangleFiles; - bool sequential = false; - }; - - explicit CloudViewMainWindow(QWidget* parent = nullptr); - ~CloudViewMainWindow() override; - - bool loadStartupFiles(const StartupFiles& files); - bool loadDisplayFile(const QString& fileName); - bool loadCircleShapeFile(const QString& fileName); - bool loadRectangleShapeFile(const QString& fileName); - bool loadSurfaceShapeFile(const QString& fileName); - bool loadTriangleShapeFile(const QString& fileName); - void fitViewToContent(); +public: + struct StartupFiles + { + QStringList displayFiles; + QStringList bboxFiles; + QStringList circleFiles; + QStringList rectangleFiles; + QStringList surfaceFiles; + QStringList triangleFiles; + bool sequential = false; + }; -protected: - bool eventFilter(QObject* obj, QEvent* event) override; - void closeEvent(QCloseEvent* event) override; - -private: - enum class StartupFileType - { - Display, - BBox, - Circle, - Rectangle, - Surface, - Triangle - }; - - struct StartupItem - { - StartupFileType type; - QString fileName; - }; - - void buildStartupSequence(const StartupFiles& files); - bool loadStartupItem(const StartupItem& item); - bool loadCurrentStartupSequenceItem(); - -private slots: + explicit CloudViewMainWindow(QWidget* parent = nullptr); + ~CloudViewMainWindow() override; + + bool loadStartupFiles(const StartupFiles& files); + bool loadDisplayFile(const QString& fileName); + bool loadCircleShapeFile(const QString& fileName); + bool loadRectangleShapeFile(const QString& fileName); + bool loadSurfaceShapeFile(const QString& fileName); + bool loadTriangleShapeFile(const QString& fileName); + void fitViewToContent(); + +protected: + bool eventFilter(QObject* obj, QEvent* event) override; + void closeEvent(QCloseEvent* event) override; + +private: + enum class StartupFileType + { + Display, + BBox, + Circle, + Rectangle, + Surface, + Triangle + }; + + struct StartupItem + { + StartupFileType type; + QString fileName; + }; + + void buildStartupSequence(const StartupFiles& files); + bool loadStartupItem(const StartupItem& item); + bool loadCurrentStartupSequenceItem(); + +private slots: /** * @brief 打开文件 */ @@ -146,11 +146,6 @@ private slots: */ void onPoint2CoordChanged(); - /** - * @brief 处理视角旋转角度改变事件 - */ - void onViewAnglesChanged(float rotX, float rotY, float rotZ); - /** * @brief 通过输入线号选择线 */ @@ -211,11 +206,11 @@ private slots: void onSavePointCloud(); // 保存 txt 点云 void onSavePlyPcdCloud(); // 保存 ply/pcd 点云 void onBatchConvertTxtToPly(); // 批量转换 txt -> ply - void onConvertEulerMatrix(); - void onShowCircleShape(); - void onShowRectangleShape(); - void onShowSurfaceShape(); - void onShowTriangleShape(); + void onConvertEulerMatrix(); + void onShowCircleShape(); + void onShowRectangleShape(); + void onShowSurfaceShape(); + void onShowTriangleShape(); /** * @brief 姿态补偿计算 @@ -243,11 +238,6 @@ private: */ QGroupBox* createFileGroup(); - /** - * @brief 创建视图方向工具栏(纵向排列,位于3D视图左侧) - */ - QWidget* createViewToolbar(); - /** * @brief 创建选点测距组 */ @@ -276,14 +266,14 @@ private: /** * @brief 更新选点信息显示 */ - void updateSelectedPointsDisplay(); - void clearMeasureResultDisplay(); - void updateMeasureResultDisplay(const SelectedPointInfo& p1, - const SelectedPointInfo& p2, - float distance, - const QString& statusPrefix = QString()); - - /** + void updateSelectedPointsDisplay(); + void clearMeasureResultDisplay(); + void updateMeasureResultDisplay(const SelectedPointInfo& p1, + const SelectedPointInfo& p2, + float distance, + const QString& statusPrefix = QString()); + + /** * @brief 从原始数据获取线上点(包含0,0,0) */ QVector getOriginalLinePoints(const SelectedLineInfo& lineInfo); @@ -310,12 +300,12 @@ private: * @return 是否成功加载到包围盒数据 */ bool loadBoundingBoxFile(const QString& fileName); - void applyTransformToAllClouds(const QMatrix4x4& matrix); - void addGeneratedCloud(const PointCloudXYZ& cloud, const QString& name); - void showBasicShape(const QString& shapeName, - const QVector& segments, - const QVector& points = QVector()); - void showBasicSurface(const QString& shapeName, const QVector& surfaces, const QVector& outlines); + void applyTransformToAllClouds(const QMatrix4x4& matrix); + void addGeneratedCloud(const PointCloudXYZ& cloud, const QString& name); + void showBasicShape(const QString& shapeName, + const QVector& segments, + const QVector& points = QVector()); + void showBasicSurface(const QString& shapeName, const QVector& surfaces, const QVector& outlines); // 点云显示控件 PointCloudGLWidget* m_glWidget; @@ -328,18 +318,13 @@ private: QPushButton* m_btnOpenBBox; QPushButton* m_btnClearAll; - // 视图角度输入控件 - QLineEdit* m_editRotX; - QLineEdit* m_editRotY; - QLineEdit* m_editRotZ; - // 选点测距控件 QCheckBox* m_cbMeasureDistance; QPushButton* m_btnClearPoints; - QLabel* m_lblPoint1; - QLabel* m_lblPoint2; - QLabel* m_lblDistance; - QLabel* m_lblDeltaXYZ; + QLabel* m_lblPoint1; + QLabel* m_lblPoint2; + QLabel* m_lblDistance; + QLabel* m_lblDeltaXYZ; // 点1坐标编辑控件 QLineEdit* m_editPoint1X; @@ -407,14 +392,10 @@ private: QTableWidget* m_linePointsTable; QVector m_currentLinePoints; // 当前线的原始点坐标 - // 悬浮缩放按钮 - QToolButton* m_btnZoomIn; - QToolButton* m_btnZoomOut; - - // 启动参数序列显示 - QVector m_startupSequence; - int m_startupSequenceIndex; - bool m_startupSequentialMode; -}; + // 启动参数序列显示 + QVector m_startupSequence; + int m_startupSequenceIndex; + bool m_startupSequentialMode; +}; #endif // CLOUD_VIEW_MAIN_WINDOW_H diff --git a/CloudView/Src/CloudViewMainWindow.cpp b/CloudView/Src/CloudViewMainWindow.cpp index 4b0ccfa..39d02ab 100644 --- a/CloudView/Src/CloudViewMainWindow.cpp +++ b/CloudView/Src/CloudViewMainWindow.cpp @@ -25,12 +25,12 @@ #include #include #include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include #include #include #include @@ -40,22 +40,22 @@ 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; +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) { @@ -412,36 +412,36 @@ void getShapeFrame(const PointCloudXYZ& cloud, QVector3D& center, QVector3D& siz 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); +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; + 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()); @@ -451,702 +451,690 @@ QVector makeClosedShapeSegments(const QVector& points, f 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 + } + 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) + : 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) -{ + , 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() -{ +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) +{ + // 缩放按钮已移至 CloudView3D,事件过滤器不再需要处理缩放按钮定位 + 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"); @@ -1176,18 +1164,18 @@ void CloudViewMainWindow::setupUI() QAction* actPoseComp = toolMenu->addAction("姿态补偿计算..."); connect(actPoseComp, &QAction::triggered, this, &CloudViewMainWindow::onPoseCompensation); - QMenu* imageToolMenu = menuBar()->addMenu("图形工具"); + 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); + 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("眼在手外..."); @@ -1206,16 +1194,12 @@ void CloudViewMainWindow::setupUI() // 创建分割器 QSplitter* splitter = new QSplitter(Qt::Horizontal, centralWidget); - // 左侧:视图工具栏 + 点云显示区域 + // 左侧:点云显示区域(视图工具栏已移至 CloudView3D) 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); @@ -1246,50 +1230,12 @@ QWidget* CloudViewMainWindow::createViewerArea() 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); + // 缩放按钮 / 旋转角度显示已移至 CloudView3D,此处不再创建 // 连接信号 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; } @@ -1349,111 +1295,6 @@ QGroupBox* CloudViewMainWindow::createFileGroup() 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); @@ -1571,7 +1412,7 @@ QGroupBox* CloudViewMainWindow::createMeasureGroup() m_glWidget->clearSelectedPoints(); m_lblPoint1->setText("点1: --"); m_lblPoint2->setText("点2: --"); - clearMeasureResultDisplay(); + clearMeasureResultDisplay(); m_editPoint1X->setText("--"); m_editPoint1Y->setText("--"); m_editPoint1Z->setText("--"); @@ -1773,21 +1614,21 @@ QGroupBox* CloudViewMainWindow::createMeasureGroup() 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; -} + 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() { @@ -1906,11 +1747,11 @@ void CloudViewMainWindow::onOpenFile() return; } - if (!loadDisplayFile(fileName)) { - QMessageBox::critical(this, "错误", "无法从文件中加载点云或线段数据"); - statusBar()->showMessage("加载失败"); - } -} + if (!loadDisplayFile(fileName)) { + QMessageBox::critical(this, "错误", "无法从文件中加载点云或线段数据"); + statusBar()->showMessage("加载失败"); + } +} bool CloudViewMainWindow::loadPointCloudFile(const QString& fileName) { @@ -2336,7 +2177,7 @@ void CloudViewMainWindow::applyTransformToAllClouds(const QMatrix4x4& matrix) m_lblPoint1->setText("点1: --"); m_lblPoint2->setText("点2: --"); - clearMeasureResultDisplay(); + clearMeasureResultDisplay(); m_editPoint1X->setText("--"); m_editPoint1Y->setText("--"); m_editPoint1Z->setText("--"); @@ -2371,57 +2212,57 @@ void CloudViewMainWindow::addGeneratedCloud(const PointCloudXYZ& cloud, const QS 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); +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); + + 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); @@ -2430,97 +2271,97 @@ void CloudViewMainWindow::onShowCircleShape() 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(); -} + centerLayout->addRow("Z:", centerZ); + layout->addWidget(centerGroup); -void CloudViewMainWindow::onShowRectangleShape() -{ - QVector3D center; - QVector3D size; + 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); @@ -2531,10 +2372,10 @@ void CloudViewMainWindow::onShowRectangleShape() 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); + 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); @@ -2543,225 +2384,225 @@ void CloudViewMainWindow::onShowRectangleShape() 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);;所有文件 (*.*)"); + 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; + 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); @@ -2776,7 +2617,7 @@ void CloudViewMainWindow::onShowTriangleShape() QVBoxLayout* layout = new QVBoxLayout(&dialog); - QLabel* tip = new QLabel("输入三个顶点后点击添加;文件每行一个:{x,y,z}-{x,y,z}-{x,y,z}。", &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); @@ -2802,73 +2643,73 @@ void CloudViewMainWindow::onShowTriangleShape() 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);;所有文件 (*.*)"); + 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(); -} + 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() { @@ -2885,7 +2726,7 @@ void CloudViewMainWindow::onClearAll() // 清除选点信息 m_lblPoint1->setText("点1: --"); m_lblPoint2->setText("点2: --"); - clearMeasureResultDisplay(); + clearMeasureResultDisplay(); m_editPoint1X->setText("--"); m_editPoint1Y->setText("--"); m_editPoint1Z->setText("--"); @@ -2912,7 +2753,7 @@ void CloudViewMainWindow::onClearSelectedPoints() m_glWidget->clearPosePoints(); // 清除选点时也清除姿态 m_lblPoint1->setText("点1: --"); m_lblPoint2->setText("点2: --"); - clearMeasureResultDisplay(); + clearMeasureResultDisplay(); m_editPoint1X->setText("--"); m_editPoint1Y->setText("--"); m_editPoint1Z->setText("--"); @@ -2947,46 +2788,46 @@ void CloudViewMainWindow::onPointSelected(const SelectedPointInfo& point) 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(); +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; @@ -3025,17 +2866,17 @@ void CloudViewMainWindow::updateSelectedPointsDisplay() 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(); - } -} + } 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) { @@ -4599,14 +4440,6 @@ void CloudViewMainWindow::onEulerOrderChanged(int index) 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() { // 读取编辑框中的坐标 @@ -4636,8 +4469,8 @@ void CloudViewMainWindow::onPoint1CoordChanged() // 如果启用了测距且有两个点,重新计算距离 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坐标已更新"); + 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)); } @@ -4674,8 +4507,8 @@ void CloudViewMainWindow::onPoint2CoordChanged() // 如果启用了测距,重新计算距离 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坐标已更新"); + 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)); } diff --git a/CloudView3D/Inc/PointCloudGLWidget.h b/CloudView3D/Inc/PointCloudGLWidget.h index 1aaf7ca..7af1aa0 100644 --- a/CloudView3D/Inc/PointCloudGLWidget.h +++ b/CloudView3D/Inc/PointCloudGLWidget.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -11,8 +12,13 @@ #include #include -#include "CloudView3DTypes.h" -#include "VZNL_Types.h" +#include "CloudView3DTypes.h" +#include "VZNL_Types.h" + +class QToolButton; +class QPushButton; +class QPinchGesture; +class QResizeEvent; /** * @brief 点云显示颜色枚举 @@ -84,58 +90,58 @@ struct SelectedLineInfo /** * @brief 线段数据 */ -struct LineSegment -{ - float x1, y1, z1; // 起点 - float x2, y2, z2; // 终点 +struct LineSegment +{ + float x1, y1, z1; // 起点 + float x2, y2, z2; // 终点 float r, g, b; // 颜色 (0-1) float lineWidth; // 线宽,0 表示使用默认值(来自 RGBA 中 A > 1 的值) LineSegment() : x1(0), y1(0), z1(0), x2(0), y2(0), z2(0), r(1), g(1), b(1), lineWidth(0) {} LineSegment(float _x1, float _y1, float _z1, float _x2, float _y2, float _z2, float _r = 1.0f, float _g = 1.0f, float _b = 1.0f, float _lw = 0) - : x1(_x1), y1(_y1), z1(_z1), x2(_x2), y2(_y2), z2(_z2), r(_r), g(_g), b(_b), lineWidth(_lw) {} -}; - -/** - * @brief 面片数据 - */ -struct SurfaceQuad -{ - QVector3D p1, p2, p3, p4; - float r, g, b, a; - - SurfaceQuad() : p1(), p2(), p3(), p4(), r(0.0f), g(0.8f), b(1.0f), a(0.5f) {} - SurfaceQuad(const QVector3D& _p1, - const QVector3D& _p2, - const QVector3D& _p3, - const QVector3D& _p4, - float _r = 0.0f, - float _g = 0.8f, - float _b = 1.0f, - float _a = 0.5f) - : p1(_p1), p2(_p2), p3(_p3), p4(_p4), r(_r), g(_g), b(_b), a(_a) {} -}; - -struct BasicShapePoint -{ - float x, y, z; - float r, g, b; - float pointSize; - - BasicShapePoint() : x(0), y(0), z(0), r(1), g(1), b(1), pointSize(8.0f) {} - BasicShapePoint(float _x, - float _y, - float _z, - float _r = 1.0f, - float _g = 1.0f, - float _b = 1.0f, - float _size = 8.0f) - : x(_x), y(_y), z(_z), r(_r), g(_g), b(_b), pointSize(_size) {} -}; - -/** - * @brief 姿态点数据 - * + : x1(_x1), y1(_y1), z1(_z1), x2(_x2), y2(_y2), z2(_z2), r(_r), g(_g), b(_b), lineWidth(_lw) {} +}; + +/** + * @brief 面片数据 + */ +struct SurfaceQuad +{ + QVector3D p1, p2, p3, p4; + float r, g, b, a; + + SurfaceQuad() : p1(), p2(), p3(), p4(), r(0.0f), g(0.8f), b(1.0f), a(0.5f) {} + SurfaceQuad(const QVector3D& _p1, + const QVector3D& _p2, + const QVector3D& _p3, + const QVector3D& _p4, + float _r = 0.0f, + float _g = 0.8f, + float _b = 1.0f, + float _a = 0.5f) + : p1(_p1), p2(_p2), p3(_p3), p4(_p4), r(_r), g(_g), b(_b), a(_a) {} +}; + +struct BasicShapePoint +{ + float x, y, z; + float r, g, b; + float pointSize; + + BasicShapePoint() : x(0), y(0), z(0), r(1), g(1), b(1), pointSize(8.0f) {} + BasicShapePoint(float _x, + float _y, + float _z, + float _r = 1.0f, + float _g = 1.0f, + float _b = 1.0f, + float _size = 8.0f) + : x(_x), y(_y), z(_z), r(_r), g(_g), b(_b), pointSize(_size) {} +}; + +/** + * @brief 姿态点数据 + * * 坐标系约定(右手坐标系): * - X轴:红色,指向右 * - Y轴:绿色,指向上 @@ -168,15 +174,15 @@ public: explicit PointCloudGLWidget(QWidget* parent = nullptr); ~PointCloudGLWidget() override; - void addPointCloud(const PointCloudXYZ& cloud, const QString& name = ""); - void addPointCloud(const PointCloudXYZRGB& cloud, const QString& name = ""); - void appendPointCloud(const PointCloudXYZRGB& cloud, const QString& name = ""); - void clearPointClouds(); - void setPointCloudColor(PointCloudColor color); - void setPointSize(float size); - void resetView(); - void fitToContent(); - void zoomIn(); + void addPointCloud(const PointCloudXYZ& cloud, const QString& name = ""); + void addPointCloud(const PointCloudXYZRGB& cloud, const QString& name = ""); + void appendPointCloud(const PointCloudXYZRGB& cloud, const QString& name = ""); + void clearPointClouds(); + void setPointCloudColor(PointCloudColor color); + void setPointSize(float size); + void resetView(); + void fitToContent(); + void zoomIn(); void zoomOut(); /** * @brief 设置视角旋转角度(不改变缩放和平移) @@ -308,26 +314,26 @@ public: */ void clearLineSegments(); - /** - * @brief 设置基础图形线段 - */ - void setBasicShapeSegments(const QVector& segments); - - /** - * @brief 追加基础图形线段 - */ - void appendBasicShapeSegments(const QVector& segments); - - /** - * @brief 追加基础图形面片 - */ - void appendBasicShapeSurfaces(const QVector& surfaces); - void appendBasicShapePoints(const QVector& points); - - /** - * @brief 清除基础图形线段 - */ - void clearBasicShapeSegments(); + /** + * @brief 设置基础图形线段 + */ + void setBasicShapeSegments(const QVector& segments); + + /** + * @brief 追加基础图形线段 + */ + void appendBasicShapeSegments(const QVector& segments); + + /** + * @brief 追加基础图形面片 + */ + void appendBasicShapeSurfaces(const QVector& surfaces); + void appendBasicShapePoints(const QVector& points); + + /** + * @brief 清除基础图形线段 + */ + void clearBasicShapeSegments(); /** * @brief 添加姿态点 @@ -339,20 +345,21 @@ public: */ void clearPosePoints(); -signals: - void pointSelected(const SelectedPointInfo& point); - void twoPointsSelected(const SelectedPointInfo& p1, const SelectedPointInfo& p2, float distance); - void measurementFinished(const SelectedPointInfo& p1, - const SelectedPointInfo& p2, - float distance, - float dx, - float dy, - float dz); - void lineSelected(const SelectedLineInfo& line); - void viewAnglesChanged(float rotX, float rotY, float rotZ); +signals: + void pointSelected(const SelectedPointInfo& point); + void twoPointsSelected(const SelectedPointInfo& p1, const SelectedPointInfo& p2, float distance); + void measurementFinished(const SelectedPointInfo& p1, + const SelectedPointInfo& p2, + float distance, + float dx, + float dy, + float dz); + void lineSelected(const SelectedLineInfo& line); + void viewAnglesChanged(float rotX, float rotY, float rotZ); -protected: - void initializeGL() override; +protected: + bool event(QEvent* event) override; + void initializeGL() override; void resizeGL(int w, int h) override; void paintGL() override; void mousePressEvent(QMouseEvent* event) override; @@ -361,6 +368,7 @@ protected: void wheelEvent(QWheelEvent* event) override; void mouseDoubleClickEvent(QMouseEvent* event) override; void keyPressEvent(QKeyEvent* event) override; + void resizeEvent(QResizeEvent* event) override; private: struct PointCloudData @@ -455,34 +463,69 @@ private: PointCloudData& operator=(const PointCloudData&) = delete; }; - void computeBoundingBox(); - void updateProjection(const QMatrix4x4& modelView); - float minimumCameraDistance() const; - void setCurrentColor(PointCloudColor color); - void setColorByIndex(int colorIndex); // 根据索引设置颜色 + void computeBoundingBox(); + void updateProjection(const QMatrix4x4& modelView); + float fittedCameraDistance(float padding = 1.05f) const; + float minimumCameraDistance() const; + QVector3D colorForIndex(int colorIndex) const; SelectedPointInfo pickPoint(int screenX, int screenY); void drawSelectedPoints(); void drawMeasurementLine(); void drawAxis(); - void drawSelectedLine(); // 绘制选中的线 - void drawLineSegments(); // 绘制线段 - void drawBasicShapeSurfaces(); // 绘制基础图形面片 - void drawBasicShapeSegments(); // 绘制基础图形 - void drawBasicShapePoints(); - void drawLineSegmentList(const QVector& segments); - void drawPosePoints(); // 绘制姿态点 + void drawSelectedLine(); // 绘制选中的线 + void drawLineSegments(); // 绘制线段 + void drawBasicShapeSurfaces(); // 绘制基础图形面片 + void drawBasicShapeSegments(); // 绘制基础图形 + void drawBasicShapePoints(); + void drawLineSegmentList(const QVector& segments); + void drawPosePoints(); // 绘制姿态点 void drawAxisLabels(); // 绘制坐标轴 XYZ 标注 void uploadToVBO(PointCloudData& data); // 上传数据到 VBO void releaseVBO(PointCloudData& data); // 释放 VBO 资源 + // GLES2 shader 渲染 + bool ensureShader(); + void drawCloud(PointCloudData& data, float pointSize); + void drawPrimitives(const QMatrix4x4& mvp, + const float* positions, + int vertexCount, + GLenum mode, + const float* colors, + const QVector3D& uniformColor, + float pointSize, + float alpha, + bool depthTest); + void resetRenderState(); + + // 覆盖控件:左侧竖向视图按钮 / 左下角旋转角度 / 右侧缩放 + void createOverlayControls(); + void repositionOverlayControls(); + void showViewAngleDialog(); + void updateViewAngleDisplay(); + bool handlePinchGesture(QPinchGesture* gesture); + std::vector m_pointClouds; QMatrix4x4 m_projection; QMatrix4x4 m_view; QMatrix4x4 m_model; + QMatrix4x4 m_mvp; // 当前帧的模型视图投影矩阵(点云主渲染用) - float m_distance; - float m_rotationX; + // GLES2 shader 渲染资源 + QOpenGLShaderProgram* m_shader; + QOpenGLBuffer m_dynamicVbo; // 动态叠加图元(选中点/线/轴/姿态等)的临时 VBO + bool m_shaderReady; + GLint m_mvpLocation; + GLint m_positionLocation; + GLint m_colorLocation; + GLint m_pointSizeLocation; + GLint m_alphaLocation; + GLint m_useVertexColorLocation; + GLint m_uniformColorLocation; + + float m_distance; + float m_pinchStartDistance; + float m_rotationX; float m_rotationY; float m_rotationZ; QQuaternion m_rotation; // 使用四元数存储旋转状态 @@ -513,12 +556,19 @@ private: int m_colorIndex; // 颜色轮换索引 static const int COLOR_COUNT = 7; // 可用颜色数量 - // 线段和姿态点数据 - QVector m_lineSegments; - QVector m_basicShapeSegments; - QVector m_basicShapeSurfaces; - QVector m_basicShapePoints; - QVector m_posePoints; -}; + // 线段和姿态点数据 + QVector m_lineSegments; + QVector m_basicShapeSegments; + QVector m_basicShapeSurfaces; + QVector m_basicShapePoints; + QVector m_posePoints; + + // 覆盖控件 + QWidget* m_viewToolbar; // 左侧竖向视图按钮容器 + QVector m_viewButtons; + QPushButton* m_viewAngleButton; // 左下角旋转角度显示按钮 + QToolButton* m_zoomInButton; + QToolButton* m_zoomOutButton; +}; #endif // POINT_CLOUD_GL_WIDGET_H diff --git a/CloudView3D/Src/PointCloudGLWidget.cpp b/CloudView3D/Src/PointCloudGLWidget.cpp index cf1e503..e724303 100644 --- a/CloudView3D/Src/PointCloudGLWidget.cpp +++ b/CloudView3D/Src/PointCloudGLWidget.cpp @@ -1,9 +1,22 @@ #include "PointCloudGLWidget.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 "VrLog.h" @@ -17,9 +30,34 @@ #include #endif +namespace +{ +// 将角度归一化到 [-180, 180],避免往同一方向旋转时无限累加 +float NormalizeAngleDegrees(float angle) +{ + float wrapped = std::fmod(angle, 360.0f); + if (wrapped > 180.0f) { + wrapped -= 360.0f; + } else if (wrapped < -180.0f) { + wrapped += 360.0f; + } + return wrapped; +} +} + PointCloudGLWidget::PointCloudGLWidget(QWidget* parent) : QOpenGLWidget(parent) + , m_shader(nullptr) + , m_shaderReady(false) + , m_mvpLocation(0) + , m_positionLocation(0) + , m_colorLocation(0) + , m_pointSizeLocation(0) + , m_alphaLocation(0) + , m_useVertexColorLocation(0) + , m_uniformColorLocation(0) , m_distance(100.0f) + , m_pinchStartDistance(100.0f) , m_rotationX(0.0f) , m_rotationY(0.0f) , m_rotationZ(0.0f) @@ -38,12 +76,60 @@ PointCloudGLWidget::PointCloudGLWidget(QWidget* parent) , m_measureDistanceEnabled(false) , m_hasListHighlightPoint(false) , m_colorIndex(0) + , m_viewToolbar(nullptr) + , m_viewAngleButton(nullptr) + , m_zoomInButton(nullptr) + , m_zoomOutButton(nullptr) { setFocusPolicy(Qt::StrongFocus); setMouseTracking(true); + setAttribute(Qt::WA_AcceptTouchEvents, true); + grabGesture(Qt::PinchGesture); // 确保 m_model 是单位矩阵 m_model.setToIdentity(); + + createOverlayControls(); + connect(this, &PointCloudGLWidget::viewAnglesChanged, + this, [this](float, float, float) { updateViewAngleDisplay(); }); +} + +bool PointCloudGLWidget::event(QEvent* event) +{ + if (event->type() == QEvent::Gesture) { + auto* gestureEvent = static_cast(event); + if (auto* pinch = static_cast( + gestureEvent->gesture(Qt::PinchGesture))) { + const bool handled = handlePinchGesture(pinch); + if (handled) { + gestureEvent->accept(pinch); + return true; + } + } + } + return QOpenGLWidget::event(event); +} + +bool PointCloudGLWidget::handlePinchGesture(QPinchGesture* gesture) +{ + if (!gesture) { + return false; + } + + if (gesture->state() == Qt::GestureStarted) { + m_pinchStartDistance = m_distance; + } + + const qreal scale = gesture->totalScaleFactor(); + if (scale <= 0.0 || !std::isfinite(static_cast(scale))) { + return false; + } + + m_distance = qBound(minimumCameraDistance(), + m_pinchStartDistance / static_cast(scale), + 1.0e7f); + update(); + return true; } PointCloudGLWidget::~PointCloudGLWidget() @@ -53,6 +139,10 @@ PointCloudGLWidget::~PointCloudGLWidget() for (auto& cloudData : m_pointClouds) { releaseVBO(cloudData); } + if (m_shader) { + delete m_shader; + m_shader = nullptr; + } doneCurrent(); } @@ -102,6 +192,216 @@ void PointCloudGLWidget::releaseVBO(PointCloudData& data) data.vboCreated = false; } +void PointCloudGLWidget::createOverlayControls() +{ + const int buttonSize = 32; + const QString toolButtonStyle = + "QToolButton {" + " background-color: rgba(20, 36, 58, 200);" + " color: #d8e8f4;" + " border: 1px solid #2b4f77;" + " border-radius: 4px;" + " font-size: 13px;" + "}" + "QToolButton:hover { background-color: #204a72; }" + "QToolButton:pressed { background-color: #10243a; }"; + + // ── 左侧视图方向工具栏(纵向排布) ── + struct ViewPreset { + const char* label; + const char* tooltip; + float rx, ry, rz; + }; + // 坐标系定义:X向右,Y向下,Z朝后(与 CloudView 一致) + const ViewPreset presets[] = { + {"前", "正视 (XY面)", 180.0f, 0.0f, 0.0f}, + {"后", "后视", 180.0f, 180.0f, 0.0f}, + {"左", "左视 (YZ面)", 180.0f, 90.0f, 0.0f}, + {"右", "右视", 180.0f, -90.0f, 0.0f}, + {"上", "俯视 (XZ面)", 90.0f, 0.0f, 0.0f}, + {"下", "仰视", -90.0f, 0.0f, 0.0f}, + {"机", "机械臂", -90.0f, 90.0f, 0.0f}, + }; + + m_viewToolbar = new QWidget(this); + m_viewToolbar->setStyleSheet("background: transparent;"); + auto* toolbarLayout = new QVBoxLayout(m_viewToolbar); + toolbarLayout->setContentsMargins(4, 4, 4, 4); + toolbarLayout->setSpacing(4); + // 工具栏尺寸跟随内容,便于垂直居中定位 + toolbarLayout->setSizeConstraint(QLayout::SetFixedSize); + + for (const ViewPreset& preset : presets) { + auto* button = new QToolButton(m_viewToolbar); + button->setText(QString::fromUtf8(preset.label)); + button->setToolTip(QString::fromUtf8(preset.tooltip)); + button->setFixedSize(buttonSize, buttonSize); + button->setStyleSheet(toolButtonStyle); + const float rx = preset.rx; + const float ry = preset.ry; + const float rz = preset.rz; + connect(button, &QToolButton::clicked, this, [this, rx, ry, rz]() { + setViewAngles(rx, ry, rz); + }); + m_viewButtons.append(button); + toolbarLayout->addWidget(button); + } + toolbarLayout->addStretch(1); + m_viewToolbar->setFixedWidth(buttonSize + 12); + m_viewToolbar->show(); + + // ── 左下角旋转角度显示按钮(点击弹窗输入角度) ── + m_viewAngleButton = new QPushButton(this); + m_viewAngleButton->setStyleSheet( + "QPushButton {" + " background-color: rgba(20, 36, 58, 200);" + " color: #d8e8f4;" + " border: 1px solid #2b4f77;" + " border-radius: 4px;" + " padding: 4px 8px;" + " text-align: left;" + "}" + "QPushButton:hover { background-color: #204a72; }"); + connect(m_viewAngleButton, &QPushButton::clicked, + this, &PointCloudGLWidget::showViewAngleDialog); + // 显式指定字体,保证 fontMetrics 度量与实际渲染一致 + QFont angleFont = m_viewAngleButton->font(); + angleFont.setPixelSize(12); + m_viewAngleButton->setFont(angleFont); + // 竖向三行显示 RX/RY/RZ:宽度按最长一行算,高度按三行算 + const QString longestAngleLine = QStringLiteral("RX:-360.0"); + const int angleTextWidth = + m_viewAngleButton->fontMetrics().horizontalAdvance(longestAngleLine); + const int angleLineHeight = m_viewAngleButton->fontMetrics().lineSpacing(); + m_viewAngleButton->setFixedSize(angleTextWidth + 24, angleLineHeight * 3 + 16); + m_viewAngleButton->show(); + + // ── 右侧悬浮缩放按钮(垂直居中,与 CloudView 位置一致) ── + const QString zoomButtonStyle = + "QToolButton {" + " background-color: rgba(20, 36, 58, 200);" + " color: #e8f2fb;" + " border: 1px solid #2b4f77;" + " border-radius: 4px;" + " font-size: 26px;" + " font-weight: bold;" + "}" + "QToolButton:hover { background-color: #204a72; }" + "QToolButton:pressed { background-color: #10243a; }"; + const int zoomButtonSize = 28; + m_zoomInButton = new QToolButton(this); + m_zoomInButton->setText("+"); + m_zoomInButton->setFixedSize(zoomButtonSize, zoomButtonSize); + m_zoomInButton->setStyleSheet(zoomButtonStyle); + m_zoomInButton->setAutoRepeat(true); + m_zoomInButton->setAutoRepeatDelay(300); + m_zoomInButton->setAutoRepeatInterval(50); + connect(m_zoomInButton, &QToolButton::clicked, this, &PointCloudGLWidget::zoomIn); + m_zoomInButton->show(); + + m_zoomOutButton = new QToolButton(this); + m_zoomOutButton->setText("-"); + m_zoomOutButton->setFixedSize(zoomButtonSize, zoomButtonSize); + m_zoomOutButton->setStyleSheet(zoomButtonStyle); + m_zoomOutButton->setAutoRepeat(true); + m_zoomOutButton->setAutoRepeatDelay(300); + m_zoomOutButton->setAutoRepeatInterval(50); + connect(m_zoomOutButton, &QToolButton::clicked, this, &PointCloudGLWidget::zoomOut); + m_zoomOutButton->show(); + + updateViewAngleDisplay(); + repositionOverlayControls(); +} + +void PointCloudGLWidget::repositionOverlayControls() +{ + const int widgetWidth = width(); + const int widgetHeight = height(); + if (widgetWidth <= 0 || widgetHeight <= 0) { + return; + } + + // 左侧视图工具栏:垂直居中 + if (m_viewToolbar) { + m_viewToolbar->move(8, qMax(0, (widgetHeight - m_viewToolbar->height()) / 2)); + } + + // 左下角旋转角度按钮 + if (m_viewAngleButton) { + m_viewAngleButton->adjustSize(); + const int margin = 8; + m_viewAngleButton->move(margin, + widgetHeight - m_viewAngleButton->height() - margin); + } + + // 右侧缩放按钮:垂直居中 + if (m_zoomInButton && m_zoomOutButton) { + const int buttonWidth = m_zoomInButton->width(); + const int buttonHeight = m_zoomInButton->height(); + const int margin = 8; + const int gap = 4; + const int x = widgetWidth - buttonWidth - margin; + const int y = qMax(0, (widgetHeight - buttonHeight * 2 - gap) / 2); + m_zoomInButton->move(x, y); + m_zoomOutButton->move(x, y + buttonHeight + gap); + } +} + +void PointCloudGLWidget::resizeEvent(QResizeEvent* event) +{ + QOpenGLWidget::resizeEvent(event); + repositionOverlayControls(); +} + +void PointCloudGLWidget::updateViewAngleDisplay() +{ + if (!m_viewAngleButton) { + return; + } + m_viewAngleButton->setText(QStringLiteral("RX:%1\nRY:%2\nRZ:%3") + .arg(m_rotationX, 0, 'f', 1) + .arg(m_rotationY, 0, 'f', 1) + .arg(m_rotationZ, 0, 'f', 1)); +} + +void PointCloudGLWidget::showViewAngleDialog() +{ + QDialog dialog(this); + dialog.setWindowTitle(QStringLiteral("设置视图角度")); + dialog.setMinimumWidth(240); + + auto* form = new QFormLayout(&dialog); + auto* spinX = new QDoubleSpinBox(&dialog); + auto* spinY = new QDoubleSpinBox(&dialog); + auto* spinZ = new QDoubleSpinBox(&dialog); + for (QDoubleSpinBox* spin : {spinX, spinY, spinZ}) { + spin->setRange(-180.0, 180.0); + spin->setDecimals(1); + spin->setSingleStep(1.0); + spin->setSuffix(QStringLiteral(" °")); + } + spinX->setValue(m_rotationX); + spinY->setValue(m_rotationY); + spinZ->setValue(m_rotationZ); + form->addRow(QStringLiteral("RX"), spinX); + form->addRow(QStringLiteral("RY"), spinY); + form->addRow(QStringLiteral("RZ"), spinZ); + + auto* buttons = new QDialogButtonBox( + QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); + buttons->button(QDialogButtonBox::Ok)->setText(QStringLiteral("确认")); + buttons->button(QDialogButtonBox::Cancel)->setText(QStringLiteral("取消")); + connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + form->addRow(buttons); + + if (dialog.exec() == QDialog::Accepted) { + setViewAngles(static_cast(spinX->value()), + static_cast(spinY->value()), + static_cast(spinZ->value())); + } +} + void PointCloudGLWidget::initializeGL() { initializeOpenGLFunctions(); @@ -110,18 +410,229 @@ void PointCloudGLWidget::initializeGL() glClearColor(0.15f, 0.15f, 0.15f, 1.0f); glEnable(GL_DEPTH_TEST); - glEnable(GL_POINT_SMOOTH); - glHint(GL_POINT_SMOOTH_HINT, GL_NICEST); + // 桌面 GL(GL 3.2+)下,顶点着色器输出的 gl_PointSize 需显式开启才生效; + // GLES2 总是使用 shader 点大小,无需此开关。 + if (QOpenGLContext::currentContext() && + !QOpenGLContext::currentContext()->isOpenGLES()) { +#ifdef GL_PROGRAM_POINT_SIZE + glEnable(GL_PROGRAM_POINT_SIZE); +#endif + } + if (!m_dynamicVbo.isCreated()) { + m_dynamicVbo.create(); + } + m_shaderReady = false; + ensureShader(); } -void PointCloudGLWidget::resizeGL(int w, int h) -{ - glViewport(0, 0, w, h); -} +bool PointCloudGLWidget::ensureShader() +{ + if (m_shaderReady) { + return m_shader != nullptr; + } + m_shaderReady = true; + + if (!m_shader) { + auto* program = new QOpenGLShaderProgram(); + program->bindAttributeLocation("aPosition", 0); + program->bindAttributeLocation("aColor", 1); + + if (!program->addShaderFromSourceCode( + QOpenGLShader::Vertex, + // GLSL ES 1.00 的 vertex shader 有默认 float 精度(highp), + // 但显式声明更稳妥,兼容性更好。 + "precision highp float;\n" + "attribute vec3 aPosition;\n" + "attribute vec3 aColor;\n" + "uniform mat4 uMvp;\n" + "uniform float uPointSize;\n" + "uniform float uAlpha;\n" + "uniform float uUseVertexColor;\n" + "uniform vec3 uColor;\n" + "varying vec3 vColor;\n" + "varying float vAlpha;\n" + "void main() {\n" + " gl_Position = uMvp * vec4(aPosition, 1.0);\n" + " gl_PointSize = uPointSize;\n" + " vColor = (uUseVertexColor > 0.5) ? aColor : uColor;\n" + " vAlpha = uAlpha;\n" + "}\n") || + !program->addShaderFromSourceCode( + QOpenGLShader::Fragment, + // GLSL ES 1.00 的 fragment shader 没有默认 float 精度, + // 必须显式声明,否则 Mali 等 GLES 驱动编译失败(S0032)。 + "precision highp float;\n" + "varying vec3 vColor;\n" + "varying float vAlpha;\n" + "void main() {\n" + " gl_FragColor = vec4(vColor, vAlpha);\n" + "}\n")) { + LOG_ERROR("[CloudView] Shader compile failed: %s\n", + program->log().isEmpty() + ? "unknown error" + : qPrintable(program->log())); + delete program; + return false; + } + + if (!program->link()) { + LOG_ERROR("[CloudView] Shader link failed: %s\n", + program->log().isEmpty() + ? "unknown error" + : qPrintable(program->log())); + delete program; + return false; + } + + m_shader = program; + m_positionLocation = m_shader->attributeLocation("aPosition"); + m_colorLocation = m_shader->attributeLocation("aColor"); + m_mvpLocation = m_shader->uniformLocation("uMvp"); + m_pointSizeLocation = m_shader->uniformLocation("uPointSize"); + m_alphaLocation = m_shader->uniformLocation("uAlpha"); + m_useVertexColorLocation = m_shader->uniformLocation("uUseVertexColor"); + m_uniformColorLocation = m_shader->uniformLocation("uColor"); + } + return m_shader != nullptr; +} + +QVector3D PointCloudGLWidget::colorForIndex(int colorIndex) const +{ + static const QVector3D colorTable[COLOR_COUNT] = { + QVector3D(0.75f, 0.75f, 0.75f), // 浅灰 + QVector3D(0.3f, 1.0f, 0.3f), // 浅绿 + QVector3D(0.3f, 0.3f, 1.0f), // 浅蓝 + QVector3D(1.0f, 1.0f, 0.3f), // 黄 + QVector3D(0.3f, 1.0f, 1.0f), // 青 + QVector3D(1.0f, 0.3f, 1.0f), // 品红 + QVector3D(1.0f, 1.0f, 1.0f) // 白 + }; + return colorTable[colorIndex % COLOR_COUNT]; +} + +void PointCloudGLWidget::drawCloud(PointCloudData& data, float pointSize) +{ + if (data.vertices.empty() || !m_shader) { + return; + } + m_shader->bind(); + glUniformMatrix4fv(m_mvpLocation, 1, GL_FALSE, m_mvp.constData()); + glUniform1f(m_pointSizeLocation, pointSize); + glUniform1f(m_alphaLocation, 1.0f); + // 兜底:桌面 GL 未开启 GL_PROGRAM_POINT_SIZE 时,点大小取自 GL_POINT_SIZE 状态 + glPointSize(pointSize); + + data.vertexBuffer.bind(); + glEnableVertexAttribArray(m_positionLocation); + glVertexAttribPointer(m_positionLocation, 3, GL_FLOAT, GL_FALSE, 0, nullptr); + + const bool useVertexColor = data.colorBuffer.isCreated() && !data.colors.empty(); + if (useVertexColor) { + data.colorBuffer.bind(); + glEnableVertexAttribArray(m_colorLocation); + glVertexAttribPointer(m_colorLocation, 3, GL_FLOAT, GL_FALSE, 0, nullptr); + glUniform1f(m_useVertexColorLocation, 1.0f); + } else { + glDisableVertexAttribArray(m_colorLocation); + const QVector3D color = colorForIndex(data.colorIndex); + glUniform3f(m_uniformColorLocation, color.x(), color.y(), color.z()); + glUniform1f(m_useVertexColorLocation, 0.0f); + } + + glDrawArrays(GL_POINTS, 0, static_cast(data.vertices.size() / 3)); + + glDisableVertexAttribArray(m_positionLocation); + if (useVertexColor) { + glDisableVertexAttribArray(m_colorLocation); + } + data.vertexBuffer.release(); + data.colorBuffer.release(); +} + +void PointCloudGLWidget::drawPrimitives(const QMatrix4x4& mvp, + const float* positions, + int vertexCount, + GLenum mode, + const float* colors, + const QVector3D& uniformColor, + float pointSize, + float alpha, + bool depthTest) +{ + if (!positions || vertexCount <= 0 || !m_shader) { + return; + } + m_shader->bind(); + glUniformMatrix4fv(m_mvpLocation, 1, GL_FALSE, mvp.constData()); + glUniform1f(m_pointSizeLocation, pointSize); + glUniform1f(m_alphaLocation, alpha); + // 兜底:桌面 GL 未开启 GL_PROGRAM_POINT_SIZE 时,点大小取自 GL_POINT_SIZE 状态 + glPointSize(pointSize); + if (depthTest) { + glEnable(GL_DEPTH_TEST); + } else { + glDisable(GL_DEPTH_TEST); + } + + const GLsizeiptr positionBytes = + static_cast(vertexCount) * 3 * sizeof(float); + const GLsizeiptr colorBytes = + colors ? static_cast(vertexCount) * 3 * sizeof(float) : 0; + + m_dynamicVbo.bind(); + m_dynamicVbo.allocate(positionBytes + colorBytes); + m_dynamicVbo.write(0, positions, positionBytes); + if (colors) { + m_dynamicVbo.write(positionBytes, colors, colorBytes); + } + + glEnableVertexAttribArray(m_positionLocation); + glVertexAttribPointer(m_positionLocation, 3, GL_FLOAT, GL_FALSE, 0, nullptr); + + if (colors) { + glEnableVertexAttribArray(m_colorLocation); + glVertexAttribPointer(m_colorLocation, 3, GL_FLOAT, GL_FALSE, 0, + reinterpret_cast(positionBytes)); + glUniform1f(m_useVertexColorLocation, 1.0f); + } else { + glDisableVertexAttribArray(m_colorLocation); + glUniform3f(m_uniformColorLocation, + uniformColor.x(), uniformColor.y(), uniformColor.z()); + glUniform1f(m_useVertexColorLocation, 0.0f); + } + + glDrawArrays(mode, 0, vertexCount); + + m_dynamicVbo.release(); + glDisableVertexAttribArray(m_positionLocation); + if (colors) { + glDisableVertexAttribArray(m_colorLocation); + } + glEnable(GL_DEPTH_TEST); +} + +void PointCloudGLWidget::resetRenderState() +{ + glDisableVertexAttribArray(m_positionLocation); + glDisableVertexAttribArray(m_colorLocation); + m_dynamicVbo.release(); + if (m_shader) { + m_shader->release(); + } + glEnable(GL_DEPTH_TEST); +} + +void PointCloudGLWidget::resizeGL(int w, int h) +{ + glViewport(0, 0, w, h); +} void PointCloudGLWidget::paintGL() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + if (!ensureShader()) { + return; + } m_view.setToIdentity(); // 平移在相机空间中进行,使鼠标拖动方向与屏幕方向一致 @@ -141,19 +652,9 @@ void PointCloudGLWidget::paintGL() euler.x(), euler.y(), euler.z()); } - QMatrix4x4 modelView = m_view * m_model; - - glMatrixMode(GL_PROJECTION); - updateProjection(modelView); - glLoadMatrixf(m_projection.constData()); - - glMatrixMode(GL_MODELVIEW); - glLoadMatrixf(modelView.constData()); - - // 确保颜色数组禁用 - glDisableClientState(GL_COLOR_ARRAY); - - glPointSize(m_pointSize); + QMatrix4x4 modelView = m_view * m_model; + updateProjection(modelView); + m_mvp = m_projection * modelView; for (size_t cloudIdx = 0; cloudIdx < m_pointClouds.size(); ++cloudIdx) { auto& cloudData = m_pointClouds[cloudIdx]; @@ -166,73 +667,64 @@ void PointCloudGLWidget::paintGL() uploadToVBO(cloudData); } - glEnableClientState(GL_VERTEX_ARRAY); - - if (cloudData.vboCreated && cloudData.vertexBuffer.isCreated()) { - // 使用 VBO 渲染:数据已在 GPU 显存中,无需每帧 CPU→GPU 拷贝 - cloudData.vertexBuffer.bind(); - glVertexPointer(3, GL_FLOAT, 0, nullptr); - cloudData.vertexBuffer.release(); - } else { - // 回退到 CPU 指针(VBO 创建失败时) - glVertexPointer(3, GL_FLOAT, 0, cloudData.vertices.data()); - } - - // 如果有原始颜色且选择显示原始颜色 - if (cloudData.hasColor && !cloudData.colors.empty()) { - glEnableClientState(GL_COLOR_ARRAY); - if (cloudData.vboCreated && cloudData.colorBuffer.isCreated()) { - cloudData.colorBuffer.bind(); - glColorPointer(3, GL_FLOAT, 0, nullptr); - cloudData.colorBuffer.release(); - } else { - glColorPointer(3, GL_FLOAT, 0, cloudData.colors.data()); - } - } else { - // 使用点云自己的颜色索引 - glDisableClientState(GL_COLOR_ARRAY); - setColorByIndex(cloudData.colorIndex); - } - - glDrawArrays(GL_POINTS, 0, static_cast(cloudData.vertices.size() / 3)); - - glDisableClientState(GL_VERTEX_ARRAY); - glDisableClientState(GL_COLOR_ARRAY); + drawCloud(cloudData, m_pointSize); // 绘制自定义大小的点(RGBA 中 A > 1 的点) if (cloudData.hasCustomPointSizes) { glDepthFunc(GL_LEQUAL); // 允许在相同深度覆盖绘制 + const bool cloudHasColor = cloudData.colorBuffer.isCreated() && + !cloudData.colors.empty(); for (const auto& group : cloudData.customPointSizeGroups) { - glPointSize(group.pointSize); - glBegin(GL_POINTS); + QVector positions; + positions.reserve(static_cast(group.indices.size()) * 3); + QVector colors; + if (cloudHasColor) { + colors.reserve(static_cast(group.indices.size()) * 3); + } for (size_t idx : group.indices) { - size_t vi = idx * 3; + const size_t vi = idx * 3; if (vi + 2 < cloudData.vertices.size()) { - if (cloudData.hasColor && vi + 2 < cloudData.colors.size()) { - glColor3f(cloudData.colors[vi], cloudData.colors[vi + 1], cloudData.colors[vi + 2]); + positions.append(cloudData.vertices[vi]); + positions.append(cloudData.vertices[vi + 1]); + positions.append(cloudData.vertices[vi + 2]); + if (cloudHasColor && vi + 2 < cloudData.colors.size()) { + colors.append(cloudData.colors[vi]); + colors.append(cloudData.colors[vi + 1]); + colors.append(cloudData.colors[vi + 2]); } - glVertex3f(cloudData.vertices[vi], cloudData.vertices[vi + 1], cloudData.vertices[vi + 2]); } } - glEnd(); + if (!positions.isEmpty()) { + drawPrimitives(m_mvp, + positions.constData(), + positions.size() / 3, + GL_POINTS, + cloudHasColor ? colors.constData() : nullptr, + colorForIndex(cloudData.colorIndex), + group.pointSize, + 1.0f, + true); + } } - glDepthFunc(GL_LESS); // 恢复默认深度测试 - glPointSize(m_pointSize); // 恢复默认点大小 + glDepthFunc(GL_LESS); // 恢复默认深度测试 } } drawSelectedPoints(); - drawMeasurementLine(); - drawSelectedLine(); - drawLineSegments(); - drawBasicShapeSurfaces(); - drawBasicShapeSegments(); - drawBasicShapePoints(); - drawPosePoints(); + drawMeasurementLine(); + drawSelectedLine(); + drawLineSegments(); + drawBasicShapeSurfaces(); + drawBasicShapeSegments(); + drawBasicShapePoints(); + drawPosePoints(); // 最后绘制坐标系指示器(覆盖在所有内容之上) drawAxis(); + // 清理 GL 状态,避免影响 QPainter 标注 + resetRenderState(); + // 使用 QPainter 绘制坐标轴 XYZ 标注 drawAxisLabels(); } @@ -327,8 +819,8 @@ void PointCloudGLWidget::addPointCloud(const PointCloudXYZ& cloud, const QString update(); } -void PointCloudGLWidget::addPointCloud(const PointCloudXYZRGB& cloud, const QString& name) -{ +void PointCloudGLWidget::addPointCloud(const PointCloudXYZRGB& cloud, const QString& name) +{ if (cloud.empty()) { return; } @@ -434,105 +926,102 @@ void PointCloudGLWidget::addPointCloud(const PointCloudXYZRGB& cloud, const QStr computeBoundingBox(); resetView(); - update(); -} - -void PointCloudGLWidget::appendPointCloud(const PointCloudXYZRGB& cloud, - const QString& name) -{ - if (cloud.empty()) { - return; - } - if (m_pointClouds.empty() || !m_pointClouds.back().hasColor) { - addPointCloud(cloud, name); - return; - } - - PointCloudData& data = m_pointClouds.back(); - if (!name.isEmpty()) { - data.name = name; - } - - const bool incomingHasLineInfo = !cloud.lineIndices.empty(); - data.hasLineInfo = data.hasLineInfo || incomingHasLineInfo; - const size_t originalIndexOffset = data.vertices.size() / 3; - const float epsilon = 1e-6f; - int previousLineIndex = -1; - int pointInLineIndex = 0; - - for (size_t index = 0; index < cloud.points.size(); ++index) { - const Point3DRGB& point = cloud.points[index]; - int lineIndex = -1; - if (incomingHasLineInfo && index < cloud.lineIndices.size()) { - lineIndex = cloud.lineIndices[index]; - } - if (lineIndex != previousLineIndex) { - pointInLineIndex = 0; - previousLineIndex = lineIndex; - } - - if (!std::isfinite(point.x) || !std::isfinite(point.y) || - !std::isfinite(point.z) || - (std::fabs(point.x) < epsilon && - std::fabs(point.y) < epsilon && - std::fabs(point.z) < epsilon)) { - ++pointInLineIndex; - continue; - } - - const size_t pointIndex = data.vertices.size() / 3; - data.vertices.push_back(point.x); - data.vertices.push_back(point.y); - data.vertices.push_back(point.z); - data.colors.push_back(point.r / 255.0f); - data.colors.push_back(point.g / 255.0f); - data.colors.push_back(point.b / 255.0f); - data.originalIndices.push_back( - static_cast(originalIndexOffset + index)); - data.pointInLineIndices.push_back(pointInLineIndex); - - if (lineIndex >= 0) { - data.lineIndices.push_back(lineIndex); - data.totalLines = qMax(data.totalLines, lineIndex + 1); - } - - if (point.pointSize > 1.0f) { - auto sizeGroup = std::find_if( - data.customPointSizeGroups.begin(), - data.customPointSizeGroups.end(), - [&point](const PointCloudData::PointSizeGroup& group) { - return group.pointSize == point.pointSize; - }); - if (sizeGroup == data.customPointSizeGroups.end()) { - PointCloudData::PointSizeGroup group; - group.pointSize = point.pointSize; - data.customPointSizeGroups.push_back(std::move(group)); - sizeGroup = data.customPointSizeGroups.end() - 1; - } - sizeGroup->indices.push_back(pointIndex); - data.hasCustomPointSizes = true; - } - ++pointInLineIndex; - } - - if (data.totalLines > 0) { - data.pointsPerLine = - static_cast(data.vertices.size() / 3) / data.totalLines; - } - - makeCurrent(); - uploadToVBO(data); - doneCurrent(); - - computeBoundingBox(); - const QVector3D size = m_maxBound - m_minBound; - const float maximumSize = qMax(qMax(size.x(), size.y()), size.z()); - m_distance = qMax(m_distance, - qBound(10.0f, maximumSize * 2.5f, 1.0e7f)); - update(); -} - -void PointCloudGLWidget::clearPointClouds() + update(); +} + +void PointCloudGLWidget::appendPointCloud(const PointCloudXYZRGB& cloud, + const QString& name) +{ + if (cloud.empty()) { + return; + } + if (m_pointClouds.empty() || !m_pointClouds.back().hasColor) { + addPointCloud(cloud, name); + return; + } + + PointCloudData& data = m_pointClouds.back(); + if (!name.isEmpty()) { + data.name = name; + } + + const bool incomingHasLineInfo = !cloud.lineIndices.empty(); + data.hasLineInfo = data.hasLineInfo || incomingHasLineInfo; + const size_t originalIndexOffset = data.vertices.size() / 3; + const float epsilon = 1e-6f; + int previousLineIndex = -1; + int pointInLineIndex = 0; + + for (size_t index = 0; index < cloud.points.size(); ++index) { + const Point3DRGB& point = cloud.points[index]; + int lineIndex = -1; + if (incomingHasLineInfo && index < cloud.lineIndices.size()) { + lineIndex = cloud.lineIndices[index]; + } + if (lineIndex != previousLineIndex) { + pointInLineIndex = 0; + previousLineIndex = lineIndex; + } + + if (!std::isfinite(point.x) || !std::isfinite(point.y) || + !std::isfinite(point.z) || + (std::fabs(point.x) < epsilon && + std::fabs(point.y) < epsilon && + std::fabs(point.z) < epsilon)) { + ++pointInLineIndex; + continue; + } + + const size_t pointIndex = data.vertices.size() / 3; + data.vertices.push_back(point.x); + data.vertices.push_back(point.y); + data.vertices.push_back(point.z); + data.colors.push_back(point.r / 255.0f); + data.colors.push_back(point.g / 255.0f); + data.colors.push_back(point.b / 255.0f); + data.originalIndices.push_back( + static_cast(originalIndexOffset + index)); + data.pointInLineIndices.push_back(pointInLineIndex); + + if (lineIndex >= 0) { + data.lineIndices.push_back(lineIndex); + data.totalLines = qMax(data.totalLines, lineIndex + 1); + } + + if (point.pointSize > 1.0f) { + auto sizeGroup = std::find_if( + data.customPointSizeGroups.begin(), + data.customPointSizeGroups.end(), + [&point](const PointCloudData::PointSizeGroup& group) { + return group.pointSize == point.pointSize; + }); + if (sizeGroup == data.customPointSizeGroups.end()) { + PointCloudData::PointSizeGroup group; + group.pointSize = point.pointSize; + data.customPointSizeGroups.push_back(std::move(group)); + sizeGroup = data.customPointSizeGroups.end() - 1; + } + sizeGroup->indices.push_back(pointIndex); + data.hasCustomPointSizes = true; + } + ++pointInLineIndex; + } + + if (data.totalLines > 0) { + data.pointsPerLine = + static_cast(data.vertices.size() / 3) / data.totalLines; + } + + makeCurrent(); + uploadToVBO(data); + doneCurrent(); + + computeBoundingBox(); + m_distance = qMax(m_distance, fittedCameraDistance()); + update(); +} + +void PointCloudGLWidget::clearPointClouds() { // 释放所有 VBO makeCurrent(); @@ -542,13 +1031,13 @@ void PointCloudGLWidget::clearPointClouds() doneCurrent(); m_pointClouds.clear(); - m_selectedPoints.clear(); - m_selectedLine = SelectedLineInfo(); - m_lineSegments.clear(); - m_basicShapeSegments.clear(); - m_basicShapeSurfaces.clear(); - m_basicShapePoints.clear(); - m_posePoints.clear(); + m_selectedPoints.clear(); + m_selectedLine = SelectedLineInfo(); + m_lineSegments.clear(); + m_basicShapeSegments.clear(); + m_basicShapeSurfaces.clear(); + m_basicShapePoints.clear(); + m_posePoints.clear(); m_minBound = QVector3D(-50, -50, -50); m_maxBound = QVector3D(50, 50, 50); m_center = QVector3D(0, 0, 0); @@ -569,14 +1058,11 @@ void PointCloudGLWidget::setPointSize(float size) update(); } -void PointCloudGLWidget::resetView() -{ - QVector3D size = m_maxBound - m_minBound; - float maxSize = qMax(qMax(size.x(), size.y()), size.z()); +void PointCloudGLWidget::resetView() +{ + QVector3D size = m_maxBound - m_minBound; + float maxSize = qMax(qMax(size.x(), size.y()), size.z()); - // 视距至少为 maxSize 的 2.5 倍,并确保不小于 10 - // 上限放宽至 1e7,适配激光雷达大范围点云 - m_distance = qBound(10.0f, maxSize * 2.5f, 1.0e7f); // 默认视角:面向 XY 平面,X 向右,Y 向下 // 绕 X 轴旋转 180°:Y 轴从向上翻转为向下 m_rotationX = 180.0f; @@ -584,108 +1070,140 @@ void PointCloudGLWidget::resetView() m_rotationZ = 0.0f; m_rotation = QQuaternion::fromEulerAngles(m_rotationX, m_rotationY, m_rotationZ); m_pan = QVector3D(0, 0, 0); + m_distance = fittedCameraDistance(); LOG_INFO("[CloudView] resetView: size=(%.3f, %.3f, %.3f) maxSize=%.3f distance=%.3f\n", size.x(), size.y(), size.z(), maxSize, m_distance); - emit viewAnglesChanged(m_rotationX, m_rotationY, m_rotationZ); - update(); -} - -void PointCloudGLWidget::fitToContent() -{ - computeBoundingBox(); - resetView(); -} - -void PointCloudGLWidget::updateProjection(const QMatrix4x4& modelView) -{ - const QVector3D corners[] = { - {m_minBound.x(), m_minBound.y(), m_minBound.z()}, - {m_minBound.x(), m_minBound.y(), m_maxBound.z()}, - {m_minBound.x(), m_maxBound.y(), m_minBound.z()}, - {m_minBound.x(), m_maxBound.y(), m_maxBound.z()}, - {m_maxBound.x(), m_minBound.y(), m_minBound.z()}, - {m_maxBound.x(), m_minBound.y(), m_maxBound.z()}, - {m_maxBound.x(), m_maxBound.y(), m_minBound.z()}, - {m_maxBound.x(), m_maxBound.y(), m_maxBound.z()} - }; - - float minimumDepth = FLT_MAX; - float maximumDepth = -FLT_MAX; - for (const QVector3D& corner : corners) { - const float depth = -modelView.map(corner).z(); - minimumDepth = qMin(minimumDepth, depth); - maximumDepth = qMax(maximumDepth, depth); - } - - constexpr float kMinimumNearPlane = 0.01f; - float nearPlane = kMinimumNearPlane; - float farPlane = 1.0f; - if (maximumDepth > kMinimumNearPlane) { - // 点云贴近或跨过相机时保持很小的近裁剪距离,避免前侧点被裁掉。 - if (minimumDepth > kMinimumNearPlane) { - nearPlane = qMax(kMinimumNearPlane, minimumDepth * 0.5f); - } - // 远裁剪面仅覆盖当前内容,并限制 near/far 比值以保证深度缓冲精度。 - farPlane = qMax(nearPlane + 1.0f, maximumDepth * 1.5f); - nearPlane = qMax(nearPlane, farPlane / 100000.0f); - } - - const float aspect = static_cast(width()) / - static_cast(height() > 0 ? height() : 1); - m_projection.setToIdentity(); - m_projection.perspective(45.0f, aspect, nearPlane, farPlane); -} - -float PointCloudGLWidget::minimumCameraDistance() const -{ - const QVector3D corners[] = { - {m_minBound.x(), m_minBound.y(), m_minBound.z()}, - {m_minBound.x(), m_minBound.y(), m_maxBound.z()}, - {m_minBound.x(), m_maxBound.y(), m_minBound.z()}, - {m_minBound.x(), m_maxBound.y(), m_maxBound.z()}, - {m_maxBound.x(), m_minBound.y(), m_minBound.z()}, - {m_maxBound.x(), m_minBound.y(), m_maxBound.z()}, - {m_maxBound.x(), m_maxBound.y(), m_minBound.z()}, - {m_maxBound.x(), m_maxBound.y(), m_maxBound.z()} - }; - - float frontExtent = 0.0f; - for (const QVector3D& corner : corners) { - const QVector3D relativePosition = corner - m_center; - frontExtent = qMax(frontExtent, - m_rotation.rotatedVector(relativePosition).z()); - } - - const float margin = qMax(0.1f, (m_maxBound - m_minBound).length() * 0.002f); - return qBound(0.1f, frontExtent + margin, 1.0e7f); -} - -void PointCloudGLWidget::zoomIn() -{ - m_distance *= 0.9f; - m_distance = qBound(minimumCameraDistance(), m_distance, 1.0e7f); - update(); -} + emit viewAnglesChanged(m_rotationX, m_rotationY, m_rotationZ); + update(); +} + +void PointCloudGLWidget::fitToContent() +{ + computeBoundingBox(); + resetView(); +} + +void PointCloudGLWidget::updateProjection(const QMatrix4x4& modelView) +{ + const QVector3D corners[] = { + {m_minBound.x(), m_minBound.y(), m_minBound.z()}, + {m_minBound.x(), m_minBound.y(), m_maxBound.z()}, + {m_minBound.x(), m_maxBound.y(), m_minBound.z()}, + {m_minBound.x(), m_maxBound.y(), m_maxBound.z()}, + {m_maxBound.x(), m_minBound.y(), m_minBound.z()}, + {m_maxBound.x(), m_minBound.y(), m_maxBound.z()}, + {m_maxBound.x(), m_maxBound.y(), m_minBound.z()}, + {m_maxBound.x(), m_maxBound.y(), m_maxBound.z()} + }; + + float minimumDepth = FLT_MAX; + float maximumDepth = -FLT_MAX; + for (const QVector3D& corner : corners) { + const float depth = -modelView.map(corner).z(); + minimumDepth = qMin(minimumDepth, depth); + maximumDepth = qMax(maximumDepth, depth); + } + + constexpr float kMinimumNearPlane = 0.01f; + float nearPlane = kMinimumNearPlane; + float farPlane = 1.0f; + if (maximumDepth > kMinimumNearPlane) { + // 点云贴近或跨过相机时保持很小的近裁剪距离,避免前侧点被裁掉。 + if (minimumDepth > kMinimumNearPlane) { + nearPlane = qMax(kMinimumNearPlane, minimumDepth * 0.5f); + } + // 远裁剪面仅覆盖当前内容,并限制 near/far 比值以保证深度缓冲精度。 + farPlane = qMax(nearPlane + 1.0f, maximumDepth * 1.5f); + nearPlane = qMax(nearPlane, farPlane / 100000.0f); + } + + const float aspect = static_cast(width()) / + static_cast(height() > 0 ? height() : 1); + m_projection.setToIdentity(); + m_projection.perspective(45.0f, aspect, nearPlane, farPlane); +} + +float PointCloudGLWidget::fittedCameraDistance(float padding) const +{ + const QVector3D corners[] = { + {m_minBound.x(), m_minBound.y(), m_minBound.z()}, + {m_minBound.x(), m_minBound.y(), m_maxBound.z()}, + {m_minBound.x(), m_maxBound.y(), m_minBound.z()}, + {m_minBound.x(), m_maxBound.y(), m_maxBound.z()}, + {m_maxBound.x(), m_minBound.y(), m_minBound.z()}, + {m_maxBound.x(), m_minBound.y(), m_maxBound.z()}, + {m_maxBound.x(), m_maxBound.y(), m_minBound.z()}, + {m_maxBound.x(), m_maxBound.y(), m_maxBound.z()}}; + + const float aspect = qMax(0.01f, + static_cast(width()) / + static_cast(height() > 0 ? height() : 1)); + const float tangent = std::tan(qDegreesToRadians(45.0f * 0.5f)); + const float safePadding = qMax(1.0f, padding); + float distance = minimumCameraDistance(); + for (const QVector3D& corner : corners) { + const QVector3D relative = + m_rotation.rotatedVector(corner - m_center); + const float horizontalDistance = + relative.z() + std::fabs(relative.x()) * safePadding / (tangent * aspect); + const float verticalDistance = + relative.z() + std::fabs(relative.y()) * safePadding / tangent; + distance = qMax(distance, qMax(horizontalDistance, verticalDistance)); + } + return qBound(10.0f, distance, 1.0e7f); +} + +float PointCloudGLWidget::minimumCameraDistance() const +{ + const QVector3D corners[] = { + {m_minBound.x(), m_minBound.y(), m_minBound.z()}, + {m_minBound.x(), m_minBound.y(), m_maxBound.z()}, + {m_minBound.x(), m_maxBound.y(), m_minBound.z()}, + {m_minBound.x(), m_maxBound.y(), m_maxBound.z()}, + {m_maxBound.x(), m_minBound.y(), m_minBound.z()}, + {m_maxBound.x(), m_minBound.y(), m_maxBound.z()}, + {m_maxBound.x(), m_maxBound.y(), m_minBound.z()}, + {m_maxBound.x(), m_maxBound.y(), m_maxBound.z()} + }; + + float frontExtent = 0.0f; + for (const QVector3D& corner : corners) { + const QVector3D relativePosition = corner - m_center; + frontExtent = qMax(frontExtent, + m_rotation.rotatedVector(relativePosition).z()); + } + + const float margin = qMax(0.1f, (m_maxBound - m_minBound).length() * 0.002f); + return qBound(0.1f, frontExtent + margin, 1.0e7f); +} + +void PointCloudGLWidget::zoomIn() +{ + m_distance *= 0.9f; + m_distance = qBound(minimumCameraDistance(), m_distance, 1.0e7f); + update(); +} void PointCloudGLWidget::zoomOut() -{ - m_distance *= 1.1f; - m_distance = qBound(minimumCameraDistance(), m_distance, 1.0e7f); - update(); -} +{ + m_distance *= 1.1f; + m_distance = qBound(minimumCameraDistance(), m_distance, 1.0e7f); + update(); +} void PointCloudGLWidget::setViewAngles(float rotX, float rotY, float rotZ) { - m_rotationX = rotX; - m_rotationY = rotY; - m_rotationZ = rotZ; + // 归一化到 ±180°,避免往同一方向旋转时角度无限累加 + m_rotationX = NormalizeAngleDegrees(rotX); + m_rotationY = NormalizeAngleDegrees(rotY); + m_rotationZ = NormalizeAngleDegrees(rotZ); m_pan = QVector3D(0, 0, 0); - // 从欧拉角转换为四元数(ZYX顺序) - m_rotation = QQuaternion::fromEulerAngles(rotX, rotY, rotZ); - m_distance = qMax(m_distance, minimumCameraDistance()); + // 从欧拉角转换为四元数(ZYX顺序) + m_rotation = QQuaternion::fromEulerAngles(m_rotationX, m_rotationY, m_rotationZ); + m_distance = qMax(m_distance, minimumCameraDistance()); LOG_INFO("[CloudView] setViewAngles: rotX=%.1f, rotY=%.1f, rotZ=%.1f\n", rotX, rotY, rotZ); @@ -1014,99 +1532,68 @@ size_t PointCloudGLWidget::getAllCloudsByLines(std::vector(width()), static_cast(height())}; + const QMatrix4x4 modelView = m_view * m_model; + const QMatrix4x4 mvp = m_projection * modelView; // 转换屏幕 Y 坐标(OpenGL Y 轴向上) - int glScreenY = viewport[3] - screenY; + const int glScreenY = viewport[3] - screenY; float minScreenDist = FLT_MAX; size_t bestIndex = 0; @@ -1138,18 +1622,24 @@ SelectedPointInfo PointCloudGLWidget::pickPoint(int screenX, int screenY) for (size_t cloudIdx = 0; cloudIdx < m_pointClouds.size(); ++cloudIdx) { const auto& cloudData = m_pointClouds[cloudIdx]; for (size_t i = 0; i < cloudData.vertices.size(); i += 3) { - float wx = cloudData.vertices[i]; - float wy = cloudData.vertices[i + 1]; - float wz = cloudData.vertices[i + 2]; + const float wx = cloudData.vertices[i]; + const float wy = cloudData.vertices[i + 1]; + const float wz = cloudData.vertices[i + 2]; - // 将世界坐标投影到屏幕 - GLdouble sx, sy, sz; - gluProject(wx, wy, wz, modelMatrix, projMatrix, viewport, &sx, &sy, &sz); + // 将世界坐标投影到屏幕(等价于 gluProject) + const QVector4D clip = mvp * QVector4D(wx, wy, wz, 1.0f); + if (std::fabs(clip.w()) < 1e-9f) { + continue; + } + const float ndcX = clip.x() / clip.w(); + const float ndcY = clip.y() / clip.w(); + const float sx = viewport[0] + (ndcX + 1.0f) * 0.5f * viewport[2]; + const float sy = viewport[1] + (ndcY + 1.0f) * 0.5f * viewport[3]; // 计算屏幕距离 - float dx = static_cast(sx) - screenX; - float dy = static_cast(sy) - glScreenY; - float screenDist = dx * dx + dy * dy; + const float dx = sx - screenX; + const float dy = sy - glScreenY; + const float screenDist = dx * dx + dy * dy; if (screenDist < minScreenDist) { minScreenDist = screenDist; @@ -1200,29 +1690,43 @@ SelectedPointInfo PointCloudGLWidget::pickPoint(int screenX, int screenY) void PointCloudGLWidget::drawSelectedPoints() { - glPointSize(10.0f); - glDisable(GL_DEPTH_TEST); - - glBegin(GL_POINTS); + QVector positions; + QVector colors; // 绘制选中的点(橙色) for (const auto& pt : m_selectedPoints) { if (pt.valid) { - glColor3f(1.0f, 0.5f, 0.0f); // 橙色 - glVertex3f(pt.x, pt.y, pt.z); + positions.append(pt.x); + positions.append(pt.y); + positions.append(pt.z); + colors.append(1.0f); + colors.append(0.5f); + colors.append(0.0f); } } // 绘制列表高亮点(蓝色,与选点区分) if (m_hasListHighlightPoint) { - glColor3f(0.0f, 0.5f, 1.0f); // 蓝色 - glVertex3f(m_listHighlightPoint.x(), m_listHighlightPoint.y(), m_listHighlightPoint.z()); + positions.append(m_listHighlightPoint.x()); + positions.append(m_listHighlightPoint.y()); + positions.append(m_listHighlightPoint.z()); + colors.append(0.0f); + colors.append(0.5f); + colors.append(1.0f); } - glEnd(); - - glEnable(GL_DEPTH_TEST); - glPointSize(m_pointSize); + if (positions.isEmpty()) { + return; + } + drawPrimitives(m_mvp, + positions.constData(), + positions.size() / 3, + GL_POINTS, + colors.constData(), + QVector3D(1.0f, 1.0f, 1.0f), + 10.0f, + 1.0f, + false); } void PointCloudGLWidget::drawMeasurementLine() @@ -1238,17 +1742,18 @@ void PointCloudGLWidget::drawMeasurementLine() return; } - glDisable(GL_DEPTH_TEST); - glLineWidth(2.0f); - - glBegin(GL_LINES); - glColor3f(0.0f, 1.0f, 0.0f); - glVertex3f(p1.x, p1.y, p1.z); - glVertex3f(p2.x, p2.y, p2.z); - glEnd(); - - glLineWidth(1.0f); - glEnable(GL_DEPTH_TEST); + const float positions[] = { + p1.x, p1.y, p1.z, + p2.x, p2.y, p2.z}; + drawPrimitives(m_mvp, + positions, + 2, + GL_LINES, + nullptr, + QVector3D(0.0f, 1.0f, 0.0f), + 1.0f, + 1.0f, + false); } void PointCloudGLWidget::drawSelectedLine() @@ -1266,22 +1771,18 @@ void PointCloudGLWidget::drawSelectedLine() return; } - // 高亮显示选中线上的所有点 - glPointSize(3.0f); - glDisable(GL_DEPTH_TEST); - - glBegin(GL_POINTS); - glColor3f(1.0f, 1.0f, 0.0f); // 黄色高亮 + // 高亮显示选中线上的所有点(黄色) + QVector positions; if (m_selectedLine.mode == LineSelectMode::Vertical) { // 纵向选线:显示同一条扫描线上的所有点 for (size_t i = 0; i < cloudData.lineIndices.size(); ++i) { if (cloudData.lineIndices[i] == m_selectedLine.lineIndex) { - size_t vertIdx = i * 3; + const size_t vertIdx = i * 3; if (vertIdx + 2 < cloudData.vertices.size()) { - glVertex3f(cloudData.vertices[vertIdx], - cloudData.vertices[vertIdx + 1], - cloudData.vertices[vertIdx + 2]); + positions.append(cloudData.vertices[vertIdx]); + positions.append(cloudData.vertices[vertIdx + 1]); + positions.append(cloudData.vertices[vertIdx + 2]); } } } @@ -1289,23 +1790,32 @@ void PointCloudGLWidget::drawSelectedLine() // 横向选线:显示所有线的相同原始索引点 if (cloudData.pointsPerLine > 0 && m_selectedLine.pointIndex >= 0) { for (size_t i = 0; i < cloudData.originalIndices.size(); ++i) { - int originalIdx = cloudData.originalIndices[i]; + const int originalIdx = cloudData.originalIndices[i]; // 原始索引 % 每线点数 == 选中的点索引 if (originalIdx % cloudData.pointsPerLine == m_selectedLine.pointIndex) { - size_t vertIdx = i * 3; + const size_t vertIdx = i * 3; if (vertIdx + 2 < cloudData.vertices.size()) { - glVertex3f(cloudData.vertices[vertIdx], - cloudData.vertices[vertIdx + 1], - cloudData.vertices[vertIdx + 2]); + positions.append(cloudData.vertices[vertIdx]); + positions.append(cloudData.vertices[vertIdx + 1]); + positions.append(cloudData.vertices[vertIdx + 2]); } } } } } - glEnd(); - glEnable(GL_DEPTH_TEST); - glPointSize(m_pointSize); + if (positions.isEmpty()) { + return; + } + drawPrimitives(m_mvp, + positions.constData(), + positions.size() / 3, + GL_POINTS, + nullptr, + QVector3D(1.0f, 1.0f, 0.0f), + 3.0f, + 1.0f, + false); } void PointCloudGLWidget::drawAxis() @@ -1323,58 +1833,35 @@ void PointCloudGLWidget::drawAxis() int axisX = viewport[2] - axisSize - margin; // 右下角 X int axisY = margin; // 右下角 Y(OpenGL Y 轴向上) - // 保存当前矩阵状态 - glMatrixMode(GL_PROJECTION); - glPushMatrix(); - glLoadIdentity(); - // 设置正交投影,覆盖整个视口 - glOrtho(0, viewport[2], 0, viewport[3], -100, 100); + // 正交投影 + 视图变换(等价于原 glOrtho + glTranslatef + glMultMatrixf) + QMatrix4x4 projection; + projection.setToIdentity(); + projection.ortho(0, viewport[2], 0, viewport[3], -100, 100); - glMatrixMode(GL_MODELVIEW); - glPushMatrix(); - glLoadIdentity(); + QMatrix4x4 modelView; + modelView.setToIdentity(); + modelView.translate(axisX + axisSize / 2.0f, axisY + axisSize / 2.0f, 0.0f); + QMatrix4x4 rotation; + rotation.rotate(m_rotation); + modelView *= rotation; - // 移动到右下角中心位置 - glTranslatef(axisX + axisSize / 2.0f, axisY + axisSize / 2.0f, 0.0f); + const QMatrix4x4 mvp = projection * modelView; - // 应用当前视图的旋转(使用四元数转换为矩阵) - QMatrix4x4 rotMatrix; - rotMatrix.rotate(m_rotation); - glMultMatrixf(rotMatrix.constData()); + const float axisLength = axisSize * 0.4f; // 坐标轴长度 + const float positions[] = { + 0.0f, 0.0f, 0.0f, axisLength, 0.0f, 0.0f, // X 轴 - 红色(向右) + 0.0f, 0.0f, 0.0f, 0.0f, axisLength, 0.0f, // Y 轴 - 绿色(向下) + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, axisLength // Z 轴 - 蓝色(向后) + }; + const float colors[] = { + 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f + }; // 关闭深度测试,确保坐标系始终可见 - glDisable(GL_DEPTH_TEST); - - float axisLength = axisSize * 0.4f; // 坐标轴长度 - glLineWidth(2.0f); - glBegin(GL_LINES); - - // X 轴 - 红色(向右) - glColor3f(1.0f, 0.0f, 0.0f); - glVertex3f(0.0f, 0.0f, 0.0f); - glVertex3f(axisLength, 0.0f, 0.0f); - - // Y 轴 - 绿色(向下,在屏幕坐标中是向上显示) - glColor3f(0.0f, 1.0f, 0.0f); - glVertex3f(0.0f, 0.0f, 0.0f); - glVertex3f(0.0f, axisLength, 0.0f); - - // Z 轴 - 蓝色(向后,在屏幕坐标中是向观察者) - glColor3f(0.0f, 0.0f, 1.0f); - glVertex3f(0.0f, 0.0f, 0.0f); - glVertex3f(0.0f, 0.0f, axisLength); - - glEnd(); - glLineWidth(1.0f); - - // 恢复深度测试 - glEnable(GL_DEPTH_TEST); - - // 恢复矩阵状态 - glPopMatrix(); - glMatrixMode(GL_PROJECTION); - glPopMatrix(); - glMatrixMode(GL_MODELVIEW); + drawPrimitives(mvp, positions, 6, GL_LINES, colors, + QVector3D(1.0f, 1.0f, 1.0f), 1.0f, 1.0f, false); } void PointCloudGLWidget::drawAxisLabels() @@ -1453,14 +1940,14 @@ void PointCloudGLWidget::mousePressEvent(QMouseEvent* event) emit pointSelected(point); - if (m_selectedPoints.size() == 2) { - const float distance = calculateDistance(m_selectedPoints[0], m_selectedPoints[1]); - const float dx = m_selectedPoints[1].x - m_selectedPoints[0].x; - const float dy = m_selectedPoints[1].y - m_selectedPoints[0].y; - const float dz = m_selectedPoints[1].z - m_selectedPoints[0].z; - emit twoPointsSelected(m_selectedPoints[0], m_selectedPoints[1], distance); - emit measurementFinished(m_selectedPoints[0], m_selectedPoints[1], distance, dx, dy, dz); - } + if (m_selectedPoints.size() == 2) { + const float distance = calculateDistance(m_selectedPoints[0], m_selectedPoints[1]); + const float dx = m_selectedPoints[1].x - m_selectedPoints[0].x; + const float dy = m_selectedPoints[1].y - m_selectedPoints[0].y; + const float dz = m_selectedPoints[1].z - m_selectedPoints[0].z; + emit twoPointsSelected(m_selectedPoints[0], m_selectedPoints[1], distance); + emit measurementFinished(m_selectedPoints[0], m_selectedPoints[1], distance, dx, dy, dz); + } } else { // 未启用测距:只保留一个点 m_selectedPoints.clear(); @@ -1541,14 +2028,14 @@ void PointCloudGLWidget::mouseDoubleClickEvent(QMouseEvent* event) emit pointSelected(point); - if (m_selectedPoints.size() == 2) { - const float distance = calculateDistance(m_selectedPoints[0], m_selectedPoints[1]); - const float dx = m_selectedPoints[1].x - m_selectedPoints[0].x; - const float dy = m_selectedPoints[1].y - m_selectedPoints[0].y; - const float dz = m_selectedPoints[1].z - m_selectedPoints[0].z; - emit twoPointsSelected(m_selectedPoints[0], m_selectedPoints[1], distance); - emit measurementFinished(m_selectedPoints[0], m_selectedPoints[1], distance, dx, dy, dz); - } + if (m_selectedPoints.size() == 2) { + const float distance = calculateDistance(m_selectedPoints[0], m_selectedPoints[1]); + const float dx = m_selectedPoints[1].x - m_selectedPoints[0].x; + const float dy = m_selectedPoints[1].y - m_selectedPoints[0].y; + const float dz = m_selectedPoints[1].z - m_selectedPoints[0].z; + emit twoPointsSelected(m_selectedPoints[0], m_selectedPoints[1], distance); + emit measurementFinished(m_selectedPoints[0], m_selectedPoints[1], distance, dx, dy, dz); + } } else { // 未启用测距:只保留一个点 m_selectedPoints.clear(); @@ -1573,7 +2060,7 @@ void PointCloudGLWidget::mouseMoveEvent(QMouseEvent* event) float angle = -delta.x() * 0.5f; // 取反修正方向 QQuaternion deltaRotation = QQuaternion::fromAxisAndAngle(QVector3D(0, 0, 1), angle); m_rotation = deltaRotation * m_rotation; - m_rotationZ += angle; + m_rotationZ = NormalizeAngleDegrees(m_rotationZ + angle); } else { // 普通左键拖动:基于当前物体坐标系的旋转 // 鼠标水平移动 -> 绕当前Y轴旋转 @@ -1592,14 +2079,14 @@ void PointCloudGLWidget::mouseMoveEvent(QMouseEvent* event) // 应用增量旋转(左乘,相机空间旋转) m_rotation = deltaRotation * m_rotation; - // 更新欧拉角(用于显示) - m_rotationX += angleX; - m_rotationY += angleY; + // 更新欧拉角(用于显示),归一化到 ±180° + m_rotationX = NormalizeAngleDegrees(m_rotationX + angleX); + m_rotationY = NormalizeAngleDegrees(m_rotationY + angleY); } - // 发送角度变化信号 - m_distance = qMax(m_distance, minimumCameraDistance()); - emit viewAnglesChanged(m_rotationX, m_rotationY, m_rotationZ); + // 发送角度变化信号 + m_distance = qMax(m_distance, minimumCameraDistance()); + emit viewAnglesChanged(m_rotationX, m_rotationY, m_rotationZ); update(); } else if (m_middleButtonPressed) { float factor = m_distance * 0.002f; @@ -1610,10 +2097,10 @@ void PointCloudGLWidget::mouseMoveEvent(QMouseEvent* event) // 右键拖动:绕视线方向旋转(滚转)(修正方向) float angle = -delta.x() * 0.5f; // 取反修正方向 QQuaternion deltaRotation = QQuaternion::fromAxisAndAngle(QVector3D(0, 0, 1), angle); - m_rotation = deltaRotation * m_rotation; - m_rotationZ += angle; - m_distance = qMax(m_distance, minimumCameraDistance()); - emit viewAnglesChanged(m_rotationX, m_rotationY, m_rotationZ); + m_rotation = deltaRotation * m_rotation; + m_rotationZ = NormalizeAngleDegrees(m_rotationZ + angle); + m_distance = qMax(m_distance, minimumCameraDistance()); + emit viewAnglesChanged(m_rotationX, m_rotationY, m_rotationZ); update(); } } @@ -1631,11 +2118,11 @@ void PointCloudGLWidget::mouseReleaseEvent(QMouseEvent* event) void PointCloudGLWidget::wheelEvent(QWheelEvent* event) { - float delta = event->angleDelta().y() / 120.0f; - m_distance *= (1.0f - delta * 0.1f); - m_distance = qBound(minimumCameraDistance(), m_distance, 1.0e7f); - update(); -} + float delta = event->angleDelta().y() / 120.0f; + m_distance *= (1.0f - delta * 0.1f); + m_distance = qBound(minimumCameraDistance(), m_distance, 1.0e7f); + update(); +} void PointCloudGLWidget::keyPressEvent(QKeyEvent* event) { @@ -1750,55 +2237,55 @@ void PointCloudGLWidget::clearLineSegments() update(); } -void PointCloudGLWidget::setBasicShapeSegments(const QVector& segments) -{ - m_basicShapeSegments = segments; - update(); -} - -void PointCloudGLWidget::appendBasicShapeSegments(const QVector& segments) -{ - if (segments.isEmpty()) { - return; - } - - for (const LineSegment& segment : segments) { - m_basicShapeSegments.append(segment); - } - update(); -} - -void PointCloudGLWidget::appendBasicShapeSurfaces(const QVector& surfaces) -{ - if (surfaces.isEmpty()) { - return; - } - - for (const SurfaceQuad& surface : surfaces) { - m_basicShapeSurfaces.append(surface); - } - update(); -} - -void PointCloudGLWidget::appendBasicShapePoints(const QVector& points) -{ - if (points.isEmpty()) { - return; - } - - for (const BasicShapePoint& point : points) { - m_basicShapePoints.append(point); - } - update(); -} - -void PointCloudGLWidget::clearBasicShapeSegments() -{ - m_basicShapeSegments.clear(); - m_basicShapeSurfaces.clear(); - m_basicShapePoints.clear(); - update(); -} +void PointCloudGLWidget::setBasicShapeSegments(const QVector& segments) +{ + m_basicShapeSegments = segments; + update(); +} + +void PointCloudGLWidget::appendBasicShapeSegments(const QVector& segments) +{ + if (segments.isEmpty()) { + return; + } + + for (const LineSegment& segment : segments) { + m_basicShapeSegments.append(segment); + } + update(); +} + +void PointCloudGLWidget::appendBasicShapeSurfaces(const QVector& surfaces) +{ + if (surfaces.isEmpty()) { + return; + } + + for (const SurfaceQuad& surface : surfaces) { + m_basicShapeSurfaces.append(surface); + } + update(); +} + +void PointCloudGLWidget::appendBasicShapePoints(const QVector& points) +{ + if (points.isEmpty()) { + return; + } + + for (const BasicShapePoint& point : points) { + m_basicShapePoints.append(point); + } + update(); +} + +void PointCloudGLWidget::clearBasicShapeSegments() +{ + m_basicShapeSegments.clear(); + m_basicShapeSurfaces.clear(); + m_basicShapePoints.clear(); + update(); +} void PointCloudGLWidget::addPosePoints(const QVector& poses) { @@ -1812,82 +2299,94 @@ void PointCloudGLWidget::clearPosePoints() update(); } -void PointCloudGLWidget::drawLineSegments() -{ - drawLineSegmentList(m_lineSegments); -} - -void PointCloudGLWidget::drawBasicShapeSurfaces() -{ - if (m_basicShapeSurfaces.isEmpty()) { - return; - } - - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glDepthMask(GL_FALSE); - - glBegin(GL_QUADS); - for (const SurfaceQuad& surface : m_basicShapeSurfaces) { - glColor4f(surface.r, surface.g, surface.b, surface.a); - glVertex3f(surface.p1.x(), surface.p1.y(), surface.p1.z()); - glVertex3f(surface.p2.x(), surface.p2.y(), surface.p2.z()); - glVertex3f(surface.p3.x(), surface.p3.y(), surface.p3.z()); - glVertex3f(surface.p4.x(), surface.p4.y(), surface.p4.z()); - } - glEnd(); - - glDepthMask(GL_TRUE); - glDisable(GL_BLEND); -} - -void PointCloudGLWidget::drawBasicShapeSegments() -{ - drawLineSegmentList(m_basicShapeSegments); -} - -void PointCloudGLWidget::drawBasicShapePoints() -{ - if (m_basicShapePoints.isEmpty()) { - return; - } - - glDisable(GL_DEPTH_TEST); - for (const BasicShapePoint& point : m_basicShapePoints) { - glPointSize(point.pointSize); - glBegin(GL_POINTS); - glColor3f(point.r, point.g, point.b); - glVertex3f(point.x, point.y, point.z); - glEnd(); - } - glEnable(GL_DEPTH_TEST); - glPointSize(m_pointSize); -} - -void PointCloudGLWidget::drawLineSegmentList(const QVector& segments) -{ - if (segments.isEmpty()) { - return; +void PointCloudGLWidget::drawLineSegments() +{ + drawLineSegmentList(m_lineSegments); +} + +void PointCloudGLWidget::drawBasicShapeSurfaces() +{ + if (m_basicShapeSurfaces.isEmpty()) { + return; + } + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDepthMask(GL_FALSE); + + for (const SurfaceQuad& surface : m_basicShapeSurfaces) { + // GLES2 无 GL_QUADS,四边形拆成两个三角形 (p1,p2,p3) 和 (p1,p3,p4) + const float positions[] = { + surface.p1.x(), surface.p1.y(), surface.p1.z(), + surface.p2.x(), surface.p2.y(), surface.p2.z(), + surface.p3.x(), surface.p3.y(), surface.p3.z(), + surface.p1.x(), surface.p1.y(), surface.p1.z(), + surface.p3.x(), surface.p3.y(), surface.p3.z(), + surface.p4.x(), surface.p4.y(), surface.p4.z(), + }; + drawPrimitives(m_mvp, positions, 6, GL_TRIANGLES, nullptr, + QVector3D(surface.r, surface.g, surface.b), + 1.0f, surface.a, true); + } + + glDepthMask(GL_TRUE); + glDisable(GL_BLEND); +} + +void PointCloudGLWidget::drawBasicShapeSegments() +{ + drawLineSegmentList(m_basicShapeSegments); +} + +void PointCloudGLWidget::drawBasicShapePoints() +{ + if (m_basicShapePoints.isEmpty()) { + return; + } + + for (const BasicShapePoint& point : m_basicShapePoints) { + const float positions[] = {point.x, point.y, point.z}; + drawPrimitives(m_mvp, positions, 1, GL_POINTS, nullptr, + QVector3D(point.r, point.g, point.b), + point.pointSize, 1.0f, false); + } +} + +void PointCloudGLWidget::drawLineSegmentList(const QVector& segments) +{ + if (segments.isEmpty()) { + return; } // 按线宽分组绘制 // 收集所有不同的线宽值(0 表示默认 2.0f) std::map> widthGroups; for (int i = 0; i < segments.size(); ++i) { - float w = segments[i].lineWidth > 0 ? segments[i].lineWidth : 2.0f; + const float w = segments[i].lineWidth > 0 ? segments[i].lineWidth : 2.0f; widthGroups[w].append(i); } for (const auto& group : widthGroups) { - glLineWidth(group.first); - glBegin(GL_LINES); + QVector positions; + QVector colors; + positions.reserve(group.second.size() * 6); + colors.reserve(group.second.size() * 6); for (int idx : group.second) { - const auto& seg = segments[idx]; - glColor3f(seg.r, seg.g, seg.b); - glVertex3f(seg.x1, seg.y1, seg.z1); - glVertex3f(seg.x2, seg.y2, seg.z2); + const LineSegment& seg = segments[idx]; + positions.append(seg.x1); positions.append(seg.y1); positions.append(seg.z1); + positions.append(seg.x2); positions.append(seg.y2); positions.append(seg.z2); + colors.append(seg.r); colors.append(seg.g); colors.append(seg.b); + colors.append(seg.r); colors.append(seg.g); colors.append(seg.b); } - glEnd(); + if (positions.isEmpty()) { + continue; + } + // GLES2 下 glLineWidth 有效宽度常受限(部分 Mali 仅支持 1.0), + // 仍按原线宽设置,驱动不支持时自动退化为最接近的可用宽度。 + glLineWidth(group.first); + drawPrimitives(m_mvp, positions.constData(), positions.size() / 3, + GL_LINES, colors.constData(), + QVector3D(1.0f, 1.0f, 1.0f), 1.0f, 1.0f, true); } glLineWidth(1.0f); @@ -1902,67 +2401,58 @@ void PointCloudGLWidget::drawPosePoints() glLineWidth(2.0f); for (const auto& pose : m_posePoints) { - glPushMatrix(); - - // 移动到姿态点位置 - glTranslatef(pose.x, pose.y, pose.z); - - // 根据选择的欧拉角旋转顺序应用旋转 - // OpenGL的glRotatef是右乘,所以需要反向写 + // 移动到姿态点位置,并按欧拉角顺序旋转(QMatrix4x4::rotate 与 glRotatef 同为右乘) + QMatrix4x4 modelView; + modelView.setToIdentity(); + modelView.translate(pose.x, pose.y, pose.z); switch (m_eulerRotationOrder) { case EulerRotationOrder::ZYX: // Yaw-Pitch-Roll(最常用) - glRotatef(pose.rx, 1, 0, 0); // X轴旋转(最后应用) - glRotatef(pose.ry, 0, 1, 0); // Y轴旋转(第二个应用) - glRotatef(pose.rz, 0, 0, 1); // Z轴旋转(第一个应用) + modelView.rotate(pose.rx, 1, 0, 0); + modelView.rotate(pose.ry, 0, 1, 0); + modelView.rotate(pose.rz, 0, 0, 1); break; case EulerRotationOrder::XYZ: // Roll-Pitch-Yaw - glRotatef(pose.rz, 0, 0, 1); - glRotatef(pose.ry, 0, 1, 0); - glRotatef(pose.rx, 1, 0, 0); + modelView.rotate(pose.rz, 0, 0, 1); + modelView.rotate(pose.ry, 0, 1, 0); + modelView.rotate(pose.rx, 1, 0, 0); break; case EulerRotationOrder::ZXY: // Yaw-Roll-Pitch - glRotatef(pose.ry, 0, 1, 0); - glRotatef(pose.rx, 1, 0, 0); - glRotatef(pose.rz, 0, 0, 1); + modelView.rotate(pose.ry, 0, 1, 0); + modelView.rotate(pose.rx, 1, 0, 0); + modelView.rotate(pose.rz, 0, 0, 1); break; case EulerRotationOrder::YXZ: // Pitch-Roll-Yaw - glRotatef(pose.rz, 0, 0, 1); - glRotatef(pose.rx, 1, 0, 0); - glRotatef(pose.ry, 0, 1, 0); + modelView.rotate(pose.rz, 0, 0, 1); + modelView.rotate(pose.rx, 1, 0, 0); + modelView.rotate(pose.ry, 0, 1, 0); break; case EulerRotationOrder::XZY: // Roll-Yaw-Pitch - glRotatef(pose.ry, 0, 1, 0); - glRotatef(pose.rz, 0, 0, 1); - glRotatef(pose.rx, 1, 0, 0); + modelView.rotate(pose.ry, 0, 1, 0); + modelView.rotate(pose.rz, 0, 0, 1); + modelView.rotate(pose.rx, 1, 0, 0); break; case EulerRotationOrder::YZX: // Pitch-Yaw-Roll - glRotatef(pose.rx, 1, 0, 0); - glRotatef(pose.rz, 0, 0, 1); - glRotatef(pose.ry, 0, 1, 0); + modelView.rotate(pose.rx, 1, 0, 0); + modelView.rotate(pose.rz, 0, 0, 1); + modelView.rotate(pose.ry, 0, 1, 0); break; } + const QMatrix4x4 mvp = m_projection * modelView; + // 绘制坐标系(右手坐标系:X红右,Y绿上,Z蓝前) - glBegin(GL_LINES); - - // X轴 - 红色 - glColor3f(1.0f, 0.0f, 0.0f); - glVertex3f(0.0f, 0.0f, 0.0f); - glVertex3f(pose.scale, 0.0f, 0.0f); - - // Y轴 - 绿色 - glColor3f(0.0f, 1.0f, 0.0f); - glVertex3f(0.0f, 0.0f, 0.0f); - glVertex3f(0.0f, pose.scale, 0.0f); - - // Z轴 - 蓝色 - glColor3f(0.0f, 0.0f, 1.0f); - glVertex3f(0.0f, 0.0f, 0.0f); - glVertex3f(0.0f, 0.0f, pose.scale); - - glEnd(); - - glPopMatrix(); + const float positions[] = { + 0.0f, 0.0f, 0.0f, pose.scale, 0.0f, 0.0f, // X轴 - 红色 + 0.0f, 0.0f, 0.0f, 0.0f, pose.scale, 0.0f, // Y轴 - 绿色 + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, pose.scale // Z轴 - 蓝色 + }; + const float colors[] = { + 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f + }; + drawPrimitives(mvp, positions, 6, GL_LINES, colors, + QVector3D(1.0f, 1.0f, 1.0f), 1.0f, 1.0f, true); } glLineWidth(1.0f);