diff --git a/CloudUtils/Src/LaserDataLoader.cpp b/CloudUtils/Src/LaserDataLoader.cpp index 7512483..99bc738 100644 --- a/CloudUtils/Src/LaserDataLoader.cpp +++ b/CloudUtils/Src/LaserDataLoader.cpp @@ -1,6 +1,7 @@ #include "LaserDataLoader.h" #include #include +#include #include #include #include @@ -128,6 +129,34 @@ bool ReadDatLineRecord(std::ifstream& inputFile, int recordSize, DatLineScan3DDa std::memcpy(&lineData.lineStepping, buffer + 8, sizeof(lineData.lineStepping)); return true; } + +void AppendFormattedLine(std::string& output, const char* format, ...) +{ + char stackBuffer[256]; + + va_list args; + va_start(args, format); + va_list argsCopy; + va_copy(argsCopy, args); + int written = vsnprintf(stackBuffer, sizeof(stackBuffer), format, args); + va_end(args); + + if (written < 0) { + va_end(argsCopy); + return; + } + + if (static_cast(written) < sizeof(stackBuffer)) { + output.append(stackBuffer, static_cast(written)); + va_end(argsCopy); + return; + } + + std::vector dynamicBuffer(static_cast(written) + 1); + vsnprintf(dynamicBuffer.data(), dynamicBuffer.size(), format, argsCopy); + va_end(argsCopy); + output.append(dynamicBuffer.data(), static_cast(written)); +} } LaserDataLoader::LaserDataLoader() @@ -465,11 +494,11 @@ int LaserDataLoader::SaveLaserScanData(const std::string& fileName, } // 写入文件头 - sw << "LineNum:" << lineNum << std::endl; - sw << "DataType: 0" << std::endl; - sw << "ScanSpeed:" << scanSpeed << std::endl; - sw << "PointAdjust: 1" << std::endl; - sw << "MaxTimeStamp:" << maxTimeStamp << "_" << clockPerSecond << std::endl; + sw << "LineNum:" << lineNum << '\n'; + sw << "DataType: 0" << '\n'; + sw << "ScanSpeed:" << scanSpeed << '\n'; + sw << "PointAdjust: 1" << '\n'; + sw << "MaxTimeStamp:" << maxTimeStamp << "_" << clockPerSecond << '\n'; // 写入每条扫描线数据 for (const auto& linePair : laserLines) { @@ -477,21 +506,30 @@ int LaserDataLoader::SaveLaserScanData(const std::string& fileName, EVzResultDataType dataType = linePair.first; const SVzLaserLineData& lineData = linePair.second; - sw << "Line_" << lineData.llFrameIdx << "_" << lineData.llTimeStamp << "_" << lineData.nPointCount << std::endl; + sw << "Line_" << lineData.llFrameIdx << "_" << lineData.llTimeStamp << "_" << lineData.nPointCount << '\n'; // 根据数据类型写入点云数据 if (dataType == keResultDataType_Position && lineData.p3DPoint) { // 写入XYZ格式数据 const SVzNL3DPosition* points = static_cast(lineData.p3DPoint); - const SVzNL2DPosition* points2D = static_cast(lineData.p2DPoint); - for (int i = 0; i < lineData.nPointCount; i++) { - float x = static_cast(points[i].pt3D.x); - float y = static_cast(points[i].pt3D.y); - float z = static_cast(points[i].pt3D.z); - sw << "{ " << std::fixed << std::setprecision(6) << x << "," << y << "," << z << " }-"; - sw << "{ " << points2D[i].ptLeft2D.x << "," << points2D[i].ptLeft2D.y << " }-"; - sw << "{ " << points2D[i].ptRight2D.x << "," << points2D[i].ptRight2D.y << " }" << std::endl; - } + const SVzNL2DPosition* points2D = static_cast(lineData.p2DPoint); + constexpr int BATCH = 256; + constexpr int PER_POINT = 128; + for (int base = 0; base < lineData.nPointCount; base += BATCH) { + int end = (base + BATCH < lineData.nPointCount) ? (base + BATCH) : lineData.nPointCount; + std::string buffer; + buffer.reserve(BATCH * PER_POINT); + for (int i = base; i < end; ++i) { + float x = static_cast(points[i].pt3D.x); + float y = static_cast(points[i].pt3D.y); + float z = static_cast(points[i].pt3D.z); + AppendFormattedLine(buffer, "{ %.6f,%.6f,%.6f }-{ %d,%d }-{ %d,%d }\n", + x, y, z, + points2D[i].ptLeft2D.x, points2D[i].ptLeft2D.y, + points2D[i].ptRight2D.x, points2D[i].ptRight2D.y); + } + sw.write(buffer.data(), static_cast(buffer.size())); + } } else if (dataType == keResultDataType_PointXYZI && lineData.p3DPoint) { // LiDAR 点云:snprintf 批量写入,每 256 点 flush // 坐标 mm 范围可达 ±200000 → "%.6f" 最长 14 字符 → 整行 ≤73 字符 @@ -514,24 +552,29 @@ int LaserDataLoader::SaveLaserScanData(const std::string& fileName, } else if (dataType == keResultDataType_PointXYZRGBA && lineData.p3DPoint) { // 写入RGBD格式数据 const SVzNLPointXYZRGBA* points = static_cast(lineData.p3DPoint); - const SVzNL2DLRPoint* points2D = static_cast(lineData.p2DPoint); - for (int i = 0; i < lineData.nPointCount; i++) { - float x = static_cast(points[i].x); - float y = static_cast(points[i].y); - float z = static_cast(points[i].z); - int r = (points[i].nRGB >> 16) & 0xFF; - int g = (points[i].nRGB >> 8) & 0xFF; - int b = points[i].nRGB & 0xFF; - - sw << "{" << std::fixed << std::setprecision(6) << x << "," - << std::fixed << std::setprecision(6) << y << "," - << std::fixed << std::setprecision(6) << z << "," - << std::fixed << std::setprecision(6) << b * 1.0f / 255 << "," - << std::fixed << std::setprecision(6) << g * 1.0f / 255 << "," - << std::fixed << std::setprecision(6) << r * 1.0f / 255 << "}-"; - sw << "{ " << points2D[i].sLeft.x << "," << points2D[i].sLeft.y << " }-"; - sw << "{ " << points2D[i].sRight.x << "," << points2D[i].sRight.y << " }" << std::endl; - } + const SVzNL2DLRPoint* points2D = static_cast(lineData.p2DPoint); + constexpr int BATCH = 256; + constexpr int PER_POINT = 160; + for (int base = 0; base < lineData.nPointCount; base += BATCH) { + int end = (base + BATCH < lineData.nPointCount) ? (base + BATCH) : lineData.nPointCount; + std::string buffer; + buffer.reserve(BATCH * PER_POINT); + for (int i = base; i < end; ++i) { + float x = static_cast(points[i].x); + float y = static_cast(points[i].y); + float z = static_cast(points[i].z); + int r = (points[i].nRGB >> 16) & 0xFF; + int g = (points[i].nRGB >> 8) & 0xFF; + int b = points[i].nRGB & 0xFF; + + AppendFormattedLine(buffer, "{%.6f,%.6f,%.6f,%.6f,%.6f,%.6f}-{ %d,%d }-{ %d,%d }\n", + x, y, z, + b * 1.0f / 255, g * 1.0f / 255, r * 1.0f / 255, + points2D[i].sLeft.x, points2D[i].sLeft.y, + points2D[i].sRight.x, points2D[i].sRight.y); + } + sw.write(buffer.data(), static_cast(buffer.size())); + } } } @@ -570,11 +613,11 @@ int LaserDataLoader::DebugSaveLaser(std::string fileName, std::vector(BATCH), linePair.size()); + std::string buffer; + buffer.reserve(BATCH * PER_POINT); + for (size_t i = base; i < end; ++i) { + const auto& point = linePair[i]; // 写入XYZ格式数据 - float x = static_cast(point.pt3D.x); - float y = static_cast(point.pt3D.y); - float z = static_cast(point.pt3D.z); - sw << "{ " - << std::fixed << std::setprecision(6) << x << ", " - << std::fixed << std::setprecision(6) << y << ", " - << std::fixed << std::setprecision(6) << z << " } - "; - sw << "{ 0, 0 } - { 0, 0 }" << std::endl; - } + float x = static_cast(point.pt3D.x); + float y = static_cast(point.pt3D.y); + float z = static_cast(point.pt3D.z); + AppendFormattedLine(buffer, "{ %.6f, %.6f, %.6f } - { 0, 0 } - { 0, 0 }\n", + x, y, z); + } + sw.write(buffer.data(), static_cast(buffer.size())); + } ++index; if (callback) { diff --git a/CloudView/Inc/CloudViewMainWindow.h b/CloudView/Inc/CloudViewMainWindow.h index 3309197..e64216a 100644 --- a/CloudView/Inc/CloudViewMainWindow.h +++ b/CloudView/Inc/CloudViewMainWindow.h @@ -17,15 +17,17 @@ #include #include #include -#include -#include -#include +#include +#include +#include +#include #include "PointCloudGLWidget.h" #include "PointCloudConverter.h" -class QTextEdit; -class QTableWidget; +class QTextEdit; +class QTableWidget; +class QCloseEvent; /** * @brief 点云查看器主窗口 @@ -35,14 +37,55 @@ class CloudViewMainWindow : public QMainWindow { Q_OBJECT -public: - explicit CloudViewMainWindow(QWidget* parent = nullptr); - ~CloudViewMainWindow() override; +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(); -protected: - bool eventFilter(QObject* obj, QEvent* event) override; - -private slots: +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 打开文件 */ @@ -168,7 +211,11 @@ private slots: void onSavePointCloud(); // 保存 txt 点云 void onSavePlyPcdCloud(); // 保存 ply/pcd 点云 void onBatchConvertTxtToPly(); // 批量转换 txt -> ply - void onConvertEulerMatrix(); + void onConvertEulerMatrix(); + void onShowCircleShape(); + void onShowRectangleShape(); + void onShowSurfaceShape(); + void onShowTriangleShape(); /** * @brief 姿态补偿计算 @@ -258,8 +305,10 @@ private: * @return 是否成功加载到包围盒数据 */ bool loadBoundingBoxFile(const QString& fileName); - void applyTransformToAllClouds(const QMatrix4x4& matrix); - void addGeneratedCloud(const PointCloudXYZ& cloud, const QString& name); + void applyTransformToAllClouds(const QMatrix4x4& matrix); + void addGeneratedCloud(const PointCloudXYZ& cloud, const QString& name); + void showBasicShape(const QString& shapeName, const QVector& segments); + void showBasicSurface(const QString& shapeName, const QVector& surfaces, const QVector& outlines); // 点云显示控件 PointCloudGLWidget* m_glWidget; @@ -269,7 +318,6 @@ private: // 文件操作控件 QPushButton* m_btnOpenFile; - QPushButton* m_btnOpenPose; QPushButton* m_btnOpenBBox; QPushButton* m_btnClearAll; @@ -351,9 +399,14 @@ private: QTableWidget* m_linePointsTable; QVector m_currentLinePoints; // 当前线的原始点坐标 - // 悬浮缩放按钮 - QToolButton* m_btnZoomIn; - QToolButton* m_btnZoomOut; -}; + // 悬浮缩放按钮 + QToolButton* m_btnZoomIn; + QToolButton* m_btnZoomOut; + + // 启动参数序列显示 + QVector m_startupSequence; + int m_startupSequenceIndex; + bool m_startupSequentialMode; +}; #endif // CLOUD_VIEW_MAIN_WINDOW_H diff --git a/CloudView/Inc/PointCloudGLWidget.h b/CloudView/Inc/PointCloudGLWidget.h index 86af55f..64fe9f6 100644 --- a/CloudView/Inc/PointCloudGLWidget.h +++ b/CloudView/Inc/PointCloudGLWidget.h @@ -84,21 +84,41 @@ 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 姿态点数据 - * + : 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) {} +}; + +/** + * @brief 姿态点数据 + * * 坐标系约定(右手坐标系): * - X轴:红色,指向右 * - Y轴:绿色,指向上 @@ -134,10 +154,11 @@ public: void addPointCloud(const PointCloudXYZ& cloud, const QString& name = ""); void addPointCloud(const PointCloudXYZRGB& cloud, const QString& name = ""); void clearPointClouds(); - void setPointCloudColor(PointCloudColor color); - void setPointSize(float size); - void resetView(); - void zoomIn(); + void setPointCloudColor(PointCloudColor color); + void setPointSize(float size); + void resetView(); + void fitToContent(); + void zoomIn(); void zoomOut(); /** * @brief 设置视角旋转角度(不改变缩放和平移) @@ -269,6 +290,26 @@ public: */ void clearLineSegments(); + /** + * @brief 设置基础图形线段 + */ + void setBasicShapeSegments(const QVector& segments); + + /** + * @brief 追加基础图形线段 + */ + void appendBasicShapeSegments(const QVector& segments); + + /** + * @brief 追加基础图形面片 + */ + void appendBasicShapeSurfaces(const QVector& surfaces); + + /** + * @brief 清除基础图形线段 + */ + void clearBasicShapeSegments(); + /** * @brief 添加姿态点 */ @@ -396,9 +437,12 @@ private: void drawSelectedPoints(); void drawMeasurementLine(); void drawAxis(); - void drawSelectedLine(); // 绘制选中的线 - void drawLineSegments(); // 绘制线段 - void drawPosePoints(); // 绘制姿态点 + void drawSelectedLine(); // 绘制选中的线 + void drawLineSegments(); // 绘制线段 + void drawBasicShapeSurfaces(); // 绘制基础图形面片 + void drawBasicShapeSegments(); // 绘制基础图形 + void drawLineSegmentList(const QVector& segments); + void drawPosePoints(); // 绘制姿态点 void drawAxisLabels(); // 绘制坐标轴 XYZ 标注 void uploadToVBO(PointCloudData& data); // 上传数据到 VBO void releaseVBO(PointCloudData& data); // 释放 VBO 资源 @@ -441,9 +485,11 @@ private: int m_colorIndex; // 颜色轮换索引 static const int COLOR_COUNT = 7; // 可用颜色数量 - // 线段和姿态点数据 - QVector m_lineSegments; - QVector m_posePoints; -}; + // 线段和姿态点数据 + QVector m_lineSegments; + QVector m_basicShapeSegments; + QVector m_basicShapeSurfaces; + QVector m_posePoints; +}; #endif // POINT_CLOUD_GL_WIDGET_H diff --git a/CloudView/Src/CloudViewMainWindow.cpp b/CloudView/Src/CloudViewMainWindow.cpp index d2f92a8..a6cc749 100644 --- a/CloudView/Src/CloudViewMainWindow.cpp +++ b/CloudView/Src/CloudViewMainWindow.cpp @@ -29,7 +29,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -371,6 +373,422 @@ PointCloudXYZ generateBoxPointCloud(const QVector3D& center, const QVector3D& si return cloud; } + +void getShapeFrame(const PointCloudXYZ& cloud, QVector3D& center, QVector3D& size) +{ + center = QVector3D(0.0f, 0.0f, 0.0f); + size = QVector3D(100.0f, 100.0f, 100.0f); + + if (cloud.empty()) { + return; + } + + QVector3D minBound(FLT_MAX, FLT_MAX, FLT_MAX); + QVector3D maxBound(-FLT_MAX, -FLT_MAX, -FLT_MAX); + for (const auto& point : cloud.points) { + minBound.setX(std::min(minBound.x(), point.x)); + minBound.setY(std::min(minBound.y(), point.y)); + minBound.setZ(std::min(minBound.z(), point.z)); + maxBound.setX(std::max(maxBound.x(), point.x)); + maxBound.setY(std::max(maxBound.y(), point.y)); + maxBound.setZ(std::max(maxBound.z(), point.z)); + } + + center = (minBound + maxBound) * 0.5f; + size = maxBound - minBound; +} + +QDoubleSpinBox* createShapeSpinBox(QWidget* parent, double value, double minValue = -1000000000.0, double maxValue = 1000000000.0) +{ + QDoubleSpinBox* spin = new QDoubleSpinBox(parent); + spin->setDecimals(6); + spin->setRange(minValue, maxValue); + spin->setValue(value); + spin->setSingleStep(1.0); + spin->setAlignment(Qt::AlignRight); + spin->setButtonSymbols(QAbstractSpinBox::NoButtons); + return spin; +} + +EulerRotationOrder currentShapeEulerOrder(const QComboBox* combo) +{ + if (combo && combo->currentIndex() >= 0) { + return static_cast(combo->currentData().toInt()); + } + return EulerRotationOrder::ZYX; +} + +QVector3D transformShapePoint(const QVector3D& center, const QMatrix4x4& rotation, const QVector3D& localPoint) +{ + return center + rotation.map(localPoint); +} + +QVector makeClosedShapeSegments(const QVector& points, float r, float g, float b) +{ + QVector segments; + if (points.size() < 2) { + return segments; + } + + segments.reserve(points.size()); + for (int i = 0; i < points.size(); ++i) { + const QVector3D& p1 = points[i]; + const QVector3D& p2 = points[(i + 1) % points.size()]; + segments.append(LineSegment(p1.x(), p1.y(), p1.z(), + p2.x(), p2.y(), p2.z(), + r, g, b, 3.0f)); + } + return segments; +} + +QVector 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(), + 1.0f, 0.85f, 0.0f, 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, 0.0f, 0.8f, 1.0f); +} + +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], 0.0f, 0.8f, 1.0f, 0.5f)); + 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, 0.0f, 0.8f, 1.0f); +} + +void appendLineSegments(QVector& target, const QVector& source) +{ + for (const LineSegment& segment : source) { + target.append(segment); + } +} + +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, + int& shapeCount, + QString& errorMessage) +{ + constexpr int kRecordSize = 7; + segments.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)); + ++shapeCount; + } + + return shapeCount > 0; +} + +bool makeRectangleSegmentsFromRows(const QVector& rows, + EulerRotationOrder order, + QVector& segments, + int& shapeCount, + QString& errorMessage) +{ + constexpr int kRecordSize = 8; + segments.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)); + ++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) @@ -382,6 +800,8 @@ CloudViewMainWindow::CloudViewMainWindow(QWidget* parent) , m_currentLinePtNum(0) , m_linePointsDialog(nullptr) , m_linePointsTable(nullptr) + , m_startupSequenceIndex(-1) + , m_startupSequentialMode(false) { setupUI(); LOG_INFO("CloudViewMainWindow initialized\n"); @@ -391,6 +811,265 @@ 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; + int shapeCount = 0; + if (!makeCircleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, 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); + 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; + int shapeCount = 0; + if (!makeRectangleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, 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); + 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) { @@ -409,6 +1088,20 @@ bool CloudViewMainWindow::eventFilter(QObject* obj, QEvent* event) 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() { // ── 文件菜单 ── @@ -434,12 +1127,25 @@ void CloudViewMainWindow::setupUI() QAction* actRotateByZ = cloudMenu->addAction("点云旋转..."); connect(actRotateByZ, &QAction::triggered, this, &CloudViewMainWindow::onRotateCloudByZ); - QMenu* toolMenu = menuBar()->addMenu("工具"); + QMenu* toolMenu = menuBar()->addMenu("姿态工具"); QAction* actEulerPose = toolMenu->addAction("欧拉角与方向向量矩阵互转..."); connect(actEulerPose, &QAction::triggered, this, &CloudViewMainWindow::onConvertEulerMatrix); QAction* actPoseComp = toolMenu->addAction("姿态补偿计算..."); connect(actPoseComp, &QAction::triggered, this, &CloudViewMainWindow::onPoseCompensation); + QMenu* imageToolMenu = menuBar()->addMenu("图形工具"); + QAction* actOpenPosePoint = imageToolMenu->addAction("点(带姿态)"); + connect(actOpenPosePoint, &QAction::triggered, this, &CloudViewMainWindow::onOpenPoseFile); + imageToolMenu->addSeparator(); + QAction* actCircleShape = imageToolMenu->addAction("圆"); + connect(actCircleShape, &QAction::triggered, this, &CloudViewMainWindow::onShowCircleShape); + QAction* actRectangleShape = imageToolMenu->addAction("矩形"); + connect(actRectangleShape, &QAction::triggered, this, &CloudViewMainWindow::onShowRectangleShape); + QAction* actSurfaceShape = imageToolMenu->addAction("面"); + connect(actSurfaceShape, &QAction::triggered, this, &CloudViewMainWindow::onShowSurfaceShape); + QAction* actTriangleShape = imageToolMenu->addAction("三角形"); + connect(actTriangleShape, &QAction::triggered, this, &CloudViewMainWindow::onShowTriangleShape); + QMenu* transformMenu = menuBar()->addMenu("矩阵变换"); QAction* actEyeToHand = transformMenu->addAction("眼在手外..."); connect(actEyeToHand, &QAction::triggered, this, &CloudViewMainWindow::onEyeToHandTransform); @@ -585,12 +1291,6 @@ QGroupBox* CloudViewMainWindow::createFileGroup() connect(m_btnOpenFile, &QPushButton::clicked, this, &CloudViewMainWindow::onOpenFile); layout->addWidget(m_btnOpenFile); - m_btnOpenPose = new QPushButton("打开姿态点 {x,y,z}-{r,p,y}", group); - m_btnOpenPose->setMinimumHeight(24); - m_btnOpenPose->setMaximumHeight(24); - connect(m_btnOpenPose, &QPushButton::clicked, this, &CloudViewMainWindow::onOpenPoseFile); - layout->addWidget(m_btnOpenPose); - m_btnOpenBBox = new QPushButton("加载包围盒 (grasp.json)", group); m_btnOpenBBox->setMinimumHeight(24); m_btnOpenBBox->setMaximumHeight(24); @@ -1148,11 +1848,7 @@ void CloudViewMainWindow::onOpenFile() return; } - // 同一文件中可能同时包含点云和线段,两者都尝试加载 - bool cloudOk = loadPointCloudFile(fileName); - bool segmentOk = loadSegmentFile(fileName); - - if (!cloudOk && !segmentOk) { + if (!loadDisplayFile(fileName)) { QMessageBox::critical(this, "错误", "无法从文件中加载点云或线段数据"); statusBar()->showMessage("加载失败"); } @@ -1577,6 +2273,7 @@ void CloudViewMainWindow::applyTransformToAllClouds(const QMatrix4x4& matrix) m_glWidget->clearSelectedLine(); m_glWidget->clearListHighlightPoint(); m_glWidget->clearLineSegments(); + m_glWidget->clearBasicShapeSegments(); m_glWidget->clearPosePoints(); m_lblPoint1->setText("点1: --"); @@ -1616,10 +2313,495 @@ void CloudViewMainWindow::addGeneratedCloud(const PointCloudXYZ& cloud, const QS cloudName.toStdString().c_str(), cloud.size()); } +void CloudViewMainWindow::showBasicShape(const QString& shapeName, const QVector& segments) +{ + if (segments.isEmpty()) { + return; + } + + m_glWidget->appendBasicShapeSegments(segments); + statusBar()->showMessage(QString("已添加 %1").arg(shapeName)); + LOG_INFO("[CloudView] Add basic shape: %s, segments=%d\n", + shapeName.toStdString().c_str(), segments.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); + statusBar()->showMessage(QString("已添加 %1").arg(shapeName)); + LOG_INFO("[CloudView] Add basic surface: %s, surfaces=%d, outlines=%d\n", + shapeName.toStdString().c_str(), surfaces.size(), outlines.size()); +} + +void CloudViewMainWindow::onShowCircleShape() +{ + QVector3D center; + QVector3D size; + getShapeFrame(m_originalCloud, center, size); + const double defaultRadius = std::max(1.0f, std::max(size.x(), size.y()) * 0.25f); + + QDialog dialog(this); + dialog.setWindowTitle("圆"); + dialog.setModal(true); + + QVBoxLayout* layout = new QVBoxLayout(&dialog); + + QLabel* tip = new QLabel("输入圆心、姿态和半径后点击添加;文件每行一个:{x,y,z}-{r,p,y}-{r}。", &dialog); + tip->setWordWrap(true); + tip->setStyleSheet("color: gray;"); + layout->addWidget(tip); + + QGroupBox* centerGroup = new QGroupBox("圆心", &dialog); + QFormLayout* centerLayout = new QFormLayout(centerGroup); + QDoubleSpinBox* centerX = createShapeSpinBox(centerGroup, center.x()); + QDoubleSpinBox* centerY = createShapeSpinBox(centerGroup, center.y()); + QDoubleSpinBox* centerZ = createShapeSpinBox(centerGroup, center.z()); + centerLayout->addRow("X:", centerX); + centerLayout->addRow("Y:", centerY); + centerLayout->addRow("Z:", centerZ); + layout->addWidget(centerGroup); + + QGroupBox* poseGroup = new QGroupBox("姿态", &dialog); + QFormLayout* poseLayout = new QFormLayout(poseGroup); + QDoubleSpinBox* poseR = createShapeSpinBox(poseGroup, 0.0); + QDoubleSpinBox* poseP = createShapeSpinBox(poseGroup, 0.0); + QDoubleSpinBox* poseY = createShapeSpinBox(poseGroup, 0.0); + poseLayout->addRow("R/RX:", poseR); + poseLayout->addRow("P/RY:", poseP); + poseLayout->addRow("Y/RZ:", poseY); + layout->addWidget(poseGroup); + + QGroupBox* radiusGroup = new QGroupBox("尺寸", &dialog); + QFormLayout* radiusLayout = new QFormLayout(radiusGroup); + QDoubleSpinBox* radius = createShapeSpinBox(radiusGroup, defaultRadius, 0.000001); + radiusLayout->addRow("半径:", radius); + layout->addWidget(radiusGroup); + + auto addCurrentCircle = [this, centerX, centerY, centerZ, poseR, poseP, poseY, radius]() { + const QVector3D inputCenter(static_cast(centerX->value()), + static_cast(centerY->value()), + static_cast(centerZ->value())); + showBasicShape("圆", makeCircleSegments(inputCenter, + poseR->value(), + poseP->value(), + poseY->value(), + static_cast(radius->value()), + currentShapeEulerOrder(m_comboEulerOrder))); + }; + + 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; + int shapeCount = 0; + if (!makeCircleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, 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); + }); + + QPushButton* addButton = new QPushButton("添加", &dialog); + connect(addButton, &QPushButton::clicked, this, addCurrentCircle); + + QHBoxLayout* actionLayout = new QHBoxLayout(); + actionLayout->addWidget(loadButton); + actionLayout->addWidget(addButton); + actionLayout->addStretch(); + layout->addLayout(actionLayout); + + QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, &dialog); + connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + layout->addWidget(buttonBox); + + dialog.exec(); +} + +void CloudViewMainWindow::onShowRectangleShape() +{ + QVector3D center; + QVector3D size; + getShapeFrame(m_originalCloud, center, size); + const double defaultWidth = std::max(1.0f, size.x() * 0.5f); + const double defaultHeight = std::max(1.0f, size.y() * 0.5f); + + QDialog dialog(this); + dialog.setWindowTitle("矩形"); + dialog.setModal(true); + + QVBoxLayout* layout = new QVBoxLayout(&dialog); + + QLabel* tip = new QLabel("输入矩形中心、姿态和宽高后点击添加;文件每行一个:{x,y,z}-{r,p,y}-{w,h}。", &dialog); + tip->setWordWrap(true); + tip->setStyleSheet("color: gray;"); + layout->addWidget(tip); + + QGroupBox* centerGroup = new QGroupBox("中心", &dialog); + QFormLayout* centerLayout = new QFormLayout(centerGroup); + QDoubleSpinBox* centerX = createShapeSpinBox(centerGroup, center.x()); + QDoubleSpinBox* centerY = createShapeSpinBox(centerGroup, center.y()); + QDoubleSpinBox* centerZ = createShapeSpinBox(centerGroup, center.z()); + centerLayout->addRow("X:", centerX); + centerLayout->addRow("Y:", centerY); + centerLayout->addRow("Z:", centerZ); + layout->addWidget(centerGroup); + + QGroupBox* poseGroup = new QGroupBox("姿态", &dialog); + QFormLayout* poseLayout = new QFormLayout(poseGroup); + QDoubleSpinBox* poseR = createShapeSpinBox(poseGroup, 0.0); + QDoubleSpinBox* poseP = createShapeSpinBox(poseGroup, 0.0); + QDoubleSpinBox* poseY = createShapeSpinBox(poseGroup, 0.0); + poseLayout->addRow("R/RX:", poseR); + poseLayout->addRow("P/RY:", poseP); + poseLayout->addRow("Y/RZ:", poseY); + layout->addWidget(poseGroup); + + QGroupBox* sizeGroup = new QGroupBox("尺寸", &dialog); + QFormLayout* sizeLayout = new QFormLayout(sizeGroup); + QDoubleSpinBox* width = createShapeSpinBox(sizeGroup, defaultWidth, 0.000001); + QDoubleSpinBox* height = createShapeSpinBox(sizeGroup, defaultHeight, 0.000001); + sizeLayout->addRow("宽:", width); + sizeLayout->addRow("高:", height); + layout->addWidget(sizeGroup); + + auto addCurrentRectangle = [this, centerX, centerY, centerZ, poseR, poseP, poseY, width, height]() { + const QVector3D inputCenter(static_cast(centerX->value()), + static_cast(centerY->value()), + static_cast(centerZ->value())); + showBasicShape("矩形", makeRectangleSegments(inputCenter, + poseR->value(), + poseP->value(), + poseY->value(), + static_cast(width->value()), + static_cast(height->value()), + currentShapeEulerOrder(m_comboEulerOrder))); + }; + + 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; + int shapeCount = 0; + if (!makeRectangleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, 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); + }); + + QPushButton* addButton = new QPushButton("添加", &dialog); + connect(addButton, &QPushButton::clicked, this, addCurrentRectangle); + + QHBoxLayout* actionLayout = new QHBoxLayout(); + actionLayout->addWidget(loadButton); + actionLayout->addWidget(addButton); + actionLayout->addStretch(); + layout->addLayout(actionLayout); + + QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, &dialog); + connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + layout->addWidget(buttonBox); + + dialog.exec(); +} + +void CloudViewMainWindow::onShowSurfaceShape() +{ + QVector3D center; + QVector3D size; + getShapeFrame(m_originalCloud, center, size); + const double defaultLength = std::max(1.0f, size.x() * 0.5f); + const double defaultWidth = std::max(1.0f, size.y() * 0.5f); + + QDialog dialog(this); + dialog.setWindowTitle("面"); + dialog.setModal(true); + + QVBoxLayout* layout = new QVBoxLayout(&dialog); + + QLabel* tip = new QLabel("输入中心、姿态和长宽后点击添加;文件每行一个:{x,y,z}-{l,w} 或 {x,y,z}-{r,p,y}-{l,w}。", &dialog); + tip->setWordWrap(true); + tip->setStyleSheet("color: gray;"); + layout->addWidget(tip); + + QGroupBox* centerGroup = new QGroupBox("中心", &dialog); + QFormLayout* centerLayout = new QFormLayout(centerGroup); + QDoubleSpinBox* centerX = createShapeSpinBox(centerGroup, center.x()); + QDoubleSpinBox* centerY = createShapeSpinBox(centerGroup, center.y()); + QDoubleSpinBox* centerZ = createShapeSpinBox(centerGroup, center.z()); + centerLayout->addRow("X:", centerX); + centerLayout->addRow("Y:", centerY); + centerLayout->addRow("Z:", centerZ); + layout->addWidget(centerGroup); + + QGroupBox* poseGroup = new QGroupBox("姿态", &dialog); + QFormLayout* poseLayout = new QFormLayout(poseGroup); + QDoubleSpinBox* poseR = createShapeSpinBox(poseGroup, 0.0); + QDoubleSpinBox* poseP = createShapeSpinBox(poseGroup, 0.0); + QDoubleSpinBox* poseY = createShapeSpinBox(poseGroup, 0.0); + poseLayout->addRow("R/RX:", poseR); + poseLayout->addRow("P/RY:", poseP); + poseLayout->addRow("Y/RZ:", poseY); + layout->addWidget(poseGroup); + + QGroupBox* sizeGroup = new QGroupBox("尺寸", &dialog); + QFormLayout* sizeLayout = new QFormLayout(sizeGroup); + QDoubleSpinBox* length = createShapeSpinBox(sizeGroup, defaultLength, 0.000001); + QDoubleSpinBox* width = createShapeSpinBox(sizeGroup, defaultWidth, 0.000001); + sizeLayout->addRow("长:", length); + sizeLayout->addRow("宽:", width); + layout->addWidget(sizeGroup); + + auto addCurrentSurface = [this, centerX, centerY, centerZ, poseR, poseP, poseY, length, width]() { + const QVector3D inputCenter(static_cast(centerX->value()), + static_cast(centerY->value()), + static_cast(centerZ->value())); + const EulerRotationOrder order = currentShapeEulerOrder(m_comboEulerOrder); + const QVector surfaces = makeSurfaceQuads(inputCenter, + poseR->value(), + poseP->value(), + poseY->value(), + static_cast(length->value()), + static_cast(width->value()), + order); + const QVector outlines = makeSurfaceOutlineSegments(inputCenter, + poseR->value(), + poseP->value(), + poseY->value(), + static_cast(length->value()), + static_cast(width->value()), + order); + showBasicSurface("面", surfaces, outlines); + }; + + QPushButton* loadButton = new QPushButton("加载文件", &dialog); + connect(loadButton, &QPushButton::clicked, this, [this, centerX, centerY, centerZ, poseR, poseP, poseY, length, width]() { + const QString fileName = QFileDialog::getOpenFileName( + this, "加载面数据", QString(), + "数据文件 (*.txt *.json *.dat);;所有文件 (*.*)"); + if (fileName.isEmpty()) { + return; + } + + QVector rows; + QString errorMessage; + if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { + QMessageBox::warning(this, "错误", errorMessage); + return; + } + + QVector surfaces; + QVector outlines; + int shapeCount = 0; + if (!makeSurfaceDataFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), surfaces, outlines, shapeCount, errorMessage)) { + QMessageBox::warning(this, "错误", errorMessage); + return; + } + + if (shapeCount == 1) { + const QVector& values = rows.first().values; + const bool hasPose = values.size() == 8; + centerX->setValue(values[0]); + centerY->setValue(values[1]); + centerZ->setValue(values[2]); + poseR->setValue(hasPose ? values[3] : 0.0); + poseP->setValue(hasPose ? values[4] : 0.0); + poseY->setValue(hasPose ? values[5] : 0.0); + length->setValue(hasPose ? values[6] : values[3]); + width->setValue(hasPose ? values[7] : values[4]); + } + + showBasicSurface(QString("%1 个面").arg(shapeCount), surfaces, outlines); + }); + + QPushButton* addButton = new QPushButton("添加", &dialog); + connect(addButton, &QPushButton::clicked, this, addCurrentSurface); + + QHBoxLayout* actionLayout = new QHBoxLayout(); + actionLayout->addWidget(loadButton); + actionLayout->addWidget(addButton); + actionLayout->addStretch(); + layout->addLayout(actionLayout); + + QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, &dialog); + connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + layout->addWidget(buttonBox); + + dialog.exec(); +} + +void CloudViewMainWindow::onShowTriangleShape() +{ + QVector3D center; + QVector3D size; + getShapeFrame(m_originalCloud, center, size); + const float radius = std::max(1.0f, std::max(size.x(), size.y()) * 0.3f); + + const QVector3D defaultP1(center.x(), center.y() - radius, center.z()); + const QVector3D defaultP2(center.x() + radius * 0.8660254f, center.y() + radius * 0.5f, center.z()); + const QVector3D defaultP3(center.x() - radius * 0.8660254f, center.y() + radius * 0.5f, center.z()); + + QDialog dialog(this); + dialog.setWindowTitle("三角形"); + dialog.setModal(true); + + QVBoxLayout* layout = new QVBoxLayout(&dialog); + + QLabel* tip = new QLabel("输入三个顶点后点击添加;文件每行一个:{x,y,z}-{x,y,z}-{x,y,z}。", &dialog); + tip->setWordWrap(true); + tip->setStyleSheet("color: gray;"); + layout->addWidget(tip); + + auto addPointGroup = [&](const QString& title, const QVector3D& value, QDoubleSpinBox*& x, QDoubleSpinBox*& y, QDoubleSpinBox*& z) { + QGroupBox* group = new QGroupBox(title, &dialog); + QFormLayout* form = new QFormLayout(group); + x = createShapeSpinBox(group, value.x()); + y = createShapeSpinBox(group, value.y()); + z = createShapeSpinBox(group, value.z()); + form->addRow("X:", x); + form->addRow("Y:", y); + form->addRow("Z:", z); + layout->addWidget(group); + }; + + QDoubleSpinBox* x1 = nullptr; + QDoubleSpinBox* y1 = nullptr; + QDoubleSpinBox* z1 = nullptr; + QDoubleSpinBox* x2 = nullptr; + QDoubleSpinBox* y2 = nullptr; + QDoubleSpinBox* z2 = nullptr; + QDoubleSpinBox* x3 = nullptr; + QDoubleSpinBox* y3 = nullptr; + QDoubleSpinBox* z3 = nullptr; + addPointGroup("点1", defaultP1, x1, y1, z1); + addPointGroup("点2", defaultP2, x2, y2, z2); + addPointGroup("点3", defaultP3, x3, y3, z3); + + auto addCurrentTriangle = [this, x1, y1, z1, x2, y2, z2, x3, y3, z3]() { + QVector points; + points.reserve(3); + points.append(QVector3D(static_cast(x1->value()), static_cast(y1->value()), static_cast(z1->value()))); + points.append(QVector3D(static_cast(x2->value()), static_cast(y2->value()), static_cast(z2->value()))); + points.append(QVector3D(static_cast(x3->value()), static_cast(y3->value()), static_cast(z3->value()))); + showBasicShape("三角形", makeClosedShapeSegments(points, 0.2f, 1.0f, 0.2f)); + }; + + QPushButton* loadButton = new QPushButton("加载文件", &dialog); + connect(loadButton, &QPushButton::clicked, this, [this, x1, y1, z1, x2, y2, z2, x3, y3, z3]() { + const QString fileName = QFileDialog::getOpenFileName( + this, "加载三角形数据", QString(), + "数据文件 (*.txt *.json *.dat);;所有文件 (*.*)"); + if (fileName.isEmpty()) { + return; + } + + QVector rows; + QString errorMessage; + if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { + QMessageBox::warning(this, "错误", errorMessage); + return; + } + + QVector segments; + int shapeCount = 0; + if (!makeTriangleSegmentsFromRows(rows, segments, shapeCount, errorMessage)) { + QMessageBox::warning(this, "错误", errorMessage); + return; + } + + if (shapeCount == 1) { + const QVector& values = rows.first().values; + x1->setValue(values[0]); + y1->setValue(values[1]); + z1->setValue(values[2]); + x2->setValue(values[3]); + y2->setValue(values[4]); + z2->setValue(values[5]); + x3->setValue(values[6]); + y3->setValue(values[7]); + z3->setValue(values[8]); + } + + showBasicShape(QString("%1 个三角形").arg(shapeCount), segments); + }); + + QPushButton* addButton = new QPushButton("添加", &dialog); + connect(addButton, &QPushButton::clicked, this, addCurrentTriangle); + + QHBoxLayout* actionLayout = new QHBoxLayout(); + actionLayout->addWidget(loadButton); + actionLayout->addWidget(addButton); + actionLayout->addStretch(); + layout->addLayout(actionLayout); + + QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, &dialog); + connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + layout->addWidget(buttonBox); + + dialog.exec(); +} + void CloudViewMainWindow::onClearAll() { m_glWidget->clearPointClouds(); m_glWidget->clearLineSegments(); + m_glWidget->clearBasicShapeSegments(); m_glWidget->clearPosePoints(); m_cloudList->clear(); m_cloudCount = 0; diff --git a/CloudView/Src/PointCloudGLWidget.cpp b/CloudView/Src/PointCloudGLWidget.cpp index 13b604f..735efb4 100644 --- a/CloudView/Src/PointCloudGLWidget.cpp +++ b/CloudView/Src/PointCloudGLWidget.cpp @@ -225,10 +225,12 @@ void PointCloudGLWidget::paintGL() } drawSelectedPoints(); - drawMeasurementLine(); - drawSelectedLine(); - drawLineSegments(); - drawPosePoints(); + drawMeasurementLine(); + drawSelectedLine(); + drawLineSegments(); + drawBasicShapeSurfaces(); + drawBasicShapeSegments(); + drawPosePoints(); // 最后绘制坐标系指示器(覆盖在所有内容之上) drawAxis(); @@ -447,10 +449,12 @@ void PointCloudGLWidget::clearPointClouds() doneCurrent(); m_pointClouds.clear(); - m_selectedPoints.clear(); - m_selectedLine = SelectedLineInfo(); - m_lineSegments.clear(); - m_posePoints.clear(); + m_selectedPoints.clear(); + m_selectedLine = SelectedLineInfo(); + m_lineSegments.clear(); + m_basicShapeSegments.clear(); + m_basicShapeSurfaces.clear(); + m_posePoints.clear(); m_minBound = QVector3D(-50, -50, -50); m_maxBound = QVector3D(50, 50, 50); m_center = QVector3D(0, 0, 0); @@ -471,10 +475,10 @@ 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,适配激光雷达大范围点云 @@ -490,14 +494,20 @@ void PointCloudGLWidget::resetView() 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::zoomIn() -{ - m_distance *= 0.9f; - m_distance = qBound(0.1f, m_distance, 1.0e7f); + emit viewAnglesChanged(m_rotationX, m_rotationY, m_rotationZ); + update(); +} + +void PointCloudGLWidget::fitToContent() +{ + computeBoundingBox(); + resetView(); +} + +void PointCloudGLWidget::zoomIn() +{ + m_distance *= 0.9f; + m_distance = qBound(0.1f, m_distance, 1.0e7f); update(); } @@ -845,37 +855,65 @@ size_t PointCloudGLWidget::getAllCloudsByLines(std::vector& 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::clearBasicShapeSegments() +{ + m_basicShapeSegments.clear(); + m_basicShapeSurfaces.clear(); + update(); +} + void PointCloudGLWidget::addPosePoints(const QVector& poses) { m_posePoints.append(poses); @@ -1551,17 +1626,51 @@ void PointCloudGLWidget::clearPosePoints() update(); } -void PointCloudGLWidget::drawLineSegments() +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::drawLineSegmentList(const QVector& segments) { - if (m_lineSegments.isEmpty()) { + if (segments.isEmpty()) { return; } // 按线宽分组绘制 // 收集所有不同的线宽值(0 表示默认 2.0f) std::map> widthGroups; - for (int i = 0; i < m_lineSegments.size(); ++i) { - float w = m_lineSegments[i].lineWidth > 0 ? m_lineSegments[i].lineWidth : 2.0f; + for (int i = 0; i < segments.size(); ++i) { + float w = segments[i].lineWidth > 0 ? segments[i].lineWidth : 2.0f; widthGroups[w].append(i); } @@ -1569,7 +1678,7 @@ void PointCloudGLWidget::drawLineSegments() glLineWidth(group.first); glBegin(GL_LINES); for (int idx : group.second) { - const auto& seg = m_lineSegments[idx]; + 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); diff --git a/CloudView/Version.md b/CloudView/Version.md index 0a16e60..48697db 100644 --- a/CloudView/Version.md +++ b/CloudView/Version.md @@ -1,3 +1,17 @@ +# 1.2.0 +- 增加基础图像的显示 + +# 1.1.6 + - 增加批量点云txt->ply的保存 + +# 1.1.5 +- 增加了ply点云保存 + +# 1.1.4 +- 增加了矩阵转换 +- 增加了生成点云 +- 增加了视角范围(为了显示雷达点云) + # 1.1.3 - 修复崩溃 - 修复选点序号不对 diff --git a/CloudView/main.cpp b/CloudView/main.cpp index 6455439..2229ffc 100644 --- a/CloudView/main.cpp +++ b/CloudView/main.cpp @@ -1,9 +1,11 @@ -#include -#include -#include -#include "CloudViewMainWindow.h" +#include +#include +#include +#include +#include +#include "CloudViewMainWindow.h" -#define APP_VERSION "1.1.6" +#define APP_VERSION "1.2.0" int main(int argc, char* argv[]) { @@ -19,17 +21,74 @@ int main(int argc, char* argv[]) // 设置应用信息 app.setApplicationName("CloudView"); - app.setOrganizationName("CloudView"); - app.setApplicationVersion(APP_VERSION); - - // 设置应用程序图标 - app.setWindowIcon(QIcon(":/logo.png")); + app.setOrganizationName("CloudView"); + app.setApplicationVersion(APP_VERSION); + + QCommandLineParser parser; + parser.setApplicationDescription("CloudView point cloud and shape viewer"); + parser.addHelpOption(); + parser.addVersionOption(); + + QCommandLineOption cloudOption( + QStringList() << "cloud" << "point-cloud", + "Load a point cloud or segment file. Positional files are treated the same way.", + "file"); + QCommandLineOption bboxOption( + QStringList() << "bbox", + "Load a bounding-box JSON file.", + "file"); + QCommandLineOption circleOption( + QStringList() << "circle", + "Load a circle data file. Row format: {x,y,z}-{r,p,y}-{radius}.", + "file"); + QCommandLineOption rectangleOption( + QStringList() << "rectangle", + "Load a rectangle data file. Row format: {x,y,z}-{r,p,y}-{width,height}.", + "file"); + QCommandLineOption surfaceOption( + QStringList() << "surface", + "Load a surface data file. Row format: {x,y,z}-{length,width} or {x,y,z}-{r,p,y}-{length,width}.", + "file"); + QCommandLineOption triangleOption( + QStringList() << "triangle", + "Load a triangle data file. Row format: {x,y,z}-{x,y,z}-{x,y,z}.", + "file"); + QCommandLineOption sequenceOption( + QStringList() << "sequence" << "sequential", + "Display startup files one at a time. Closing the window loads the next file."); + + parser.addOption(cloudOption); + parser.addOption(bboxOption); + parser.addOption(circleOption); + parser.addOption(rectangleOption); + parser.addOption(surfaceOption); + parser.addOption(triangleOption); + parser.addOption(sequenceOption); + parser.addPositionalArgument("files", "Point cloud or segment files to load."); + parser.process(app); + + CloudViewMainWindow::StartupFiles startupFiles; + startupFiles.displayFiles = parser.values(cloudOption); + startupFiles.displayFiles.append(parser.positionalArguments()); + startupFiles.bboxFiles = parser.values(bboxOption); + startupFiles.circleFiles = parser.values(circleOption); + startupFiles.rectangleFiles = parser.values(rectangleOption); + startupFiles.surfaceFiles = parser.values(surfaceOption); + startupFiles.triangleFiles = parser.values(triangleOption); + startupFiles.sequential = parser.isSet(sequenceOption); + + // 设置应用程序图标 + app.setWindowIcon(QIcon(":/logo.png")); // 创建并显示主窗口 CloudViewMainWindow mainWindow; - mainWindow.setWindowTitle(QString("点云查看器 v%1").arg(APP_VERSION)); - mainWindow.resize(1280, 720); - mainWindow.show(); - - return app.exec(); -} + mainWindow.setWindowTitle(QString("点云查看器 v%1").arg(APP_VERSION)); + mainWindow.resize(1280, 720); + mainWindow.show(); + + QTimer::singleShot(0, &mainWindow, [&mainWindow, startupFiles]() { + mainWindow.loadStartupFiles(startupFiles); + }); + + return app.exec(); +}