From 9271a37a8167d06806df62b09b5ce5288ce8e4e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=B0=E4=BB=94?= Date: Tue, 18 Aug 2026 12:26:28 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E6=9E=84=E5=BB=BA=E7=B3=BB=E7=BB=9F=E4=B8=8E=E7=82=B9?= =?UTF-8?q?=E4=BA=91=E6=9F=A5=E7=9C=8B=E5=99=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增CloudUtils静态库与相关CMake配置 2. 重构VrUtils构建脚本,修正编译选项与依赖 3. 为Windows平台添加utf-8编译选项 4. 扩展PointCloudGLWidget功能:新增点云移除、选择信息叠加、增量相机适配 5. 优化点云添加接口,支持不重置相机视图 6. 修正激光数据保存时的DataType输出 7. 为Point3D结构体添加单位注释 8. 调整鼠标交互逻辑,统一按键操作行为 --- CMakeLists.txt | 17 +- CloudUtils/CMakeLists.txt | 46 ++++ CloudUtils/Src/LaserDataLoader.cpp | 2 +- CloudView3D/CMakeLists.txt | 1 + CloudView3D/Inc/CloudView3DTypes.h | 4 +- CloudView3D/Inc/PointCloudGLWidget.h | 36 +-- CloudView3D/Src/PointCloudGLWidget.cpp | 360 ++++++++++++++++--------- VrUtils/CMakeLists.txt | 146 +++++++--- 8 files changed, 427 insertions(+), 185 deletions(-) create mode 100644 CloudUtils/CMakeLists.txt diff --git a/CMakeLists.txt b/CMakeLists.txt index 558c96d..efab55f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,16 +1,23 @@ cmake_minimum_required(VERSION 3.14) -project(WoodCamUtils LANGUAGES CXX) +project(WoodCamUtils LANGUAGES C CXX) option(WOODCAM_BUILD_CLOUDVIEW3D "Build the Qt/OpenGL CloudView3D utility" ON) +option(WOODCAM_BUILD_CLOUDUTILS "Build the CloudUtils static library" ON) # VrCommon is a header-oriented compatibility module. Keep it as an interface # target so consumers get the canonical VZNL_Types.h include path. -add_library(VrCommon INTERFACE) -target_include_directories(VrCommon INTERFACE - ${CMAKE_CURRENT_SOURCE_DIR}/VrCommon/Inc -) +if(NOT TARGET VrCommon) + add_library(VrCommon INTERFACE) + target_include_directories(VrCommon INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/VrCommon/Inc + ) +endif() if(WOODCAM_BUILD_CLOUDVIEW3D) add_subdirectory(CloudView3D) endif() + +if(WOODCAM_BUILD_CLOUDUTILS) + add_subdirectory(CloudUtils) +endif() diff --git a/CloudUtils/CMakeLists.txt b/CloudUtils/CMakeLists.txt new file mode 100644 index 0000000..4f36108 --- /dev/null +++ b/CloudUtils/CMakeLists.txt @@ -0,0 +1,46 @@ +cmake_minimum_required(VERSION 3.14) + +find_package(Qt5 REQUIRED COMPONENTS Core Gui) + +if(NOT TARGET VrCommon) + add_library(VrCommon INTERFACE) + target_include_directories(VrCommon INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/../VrCommon/Inc + ) +endif() + +if(NOT TARGET VrUtils) + add_subdirectory( + "${CMAKE_CURRENT_SOURCE_DIR}/../VrUtils" + "${CMAKE_CURRENT_BINARY_DIR}/VrUtils" + ) +endif() + +add_library(CloudUtils STATIC + Src/LaserDataLoader.cpp + Src/PointCloudImageUtils.cpp + Inc/LaserDataLoader.h + Inc/PointCloudImageUtils.h +) + +set_target_properties(CloudUtils PROPERTIES + CXX_STANDARD 11 + CXX_STANDARD_REQUIRED ON +) + +target_include_directories(CloudUtils PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/Inc +) + +target_link_libraries(CloudUtils + PUBLIC + VrCommon + Qt5::Core + Qt5::Gui + PRIVATE + VrUtils +) + +if(MSVC) + target_compile_options(CloudUtils PRIVATE /utf-8) +endif() diff --git a/CloudUtils/Src/LaserDataLoader.cpp b/CloudUtils/Src/LaserDataLoader.cpp index 052be43..70ae04e 100644 --- a/CloudUtils/Src/LaserDataLoader.cpp +++ b/CloudUtils/Src/LaserDataLoader.cpp @@ -546,7 +546,7 @@ int LaserDataLoader::SaveLaserScanData(const std::string& fileName, // 写入文件头 sw << "LineNum:" << lineNum << '\n'; - sw << "DataType:" << static_cast(fileDataType) << '\n'; + sw << "DataType:" << 0 << '\n'; sw << "ScanSpeed:" << scanSpeed << '\n'; sw << "PointAdjust: 1" << '\n'; sw << "MaxTimeStamp:" << maxTimeStamp << "_" << clockPerSecond << '\n'; diff --git a/CloudView3D/CMakeLists.txt b/CloudView3D/CMakeLists.txt index e4a4178..38715cb 100644 --- a/CloudView3D/CMakeLists.txt +++ b/CloudView3D/CMakeLists.txt @@ -36,6 +36,7 @@ target_link_libraries(CloudView3D PUBLIC ) if(WIN32) + target_compile_options(CloudView3D PRIVATE /utf-8) target_link_libraries(CloudView3D PUBLIC opengl32 glu32) elseif(UNIX AND NOT APPLE) target_link_libraries(CloudView3D PUBLIC GL GLU) diff --git a/CloudView3D/Inc/CloudView3DTypes.h b/CloudView3D/Inc/CloudView3DTypes.h index 0cdecb6..9e74271 100644 --- a/CloudView3D/Inc/CloudView3DTypes.h +++ b/CloudView3D/Inc/CloudView3DTypes.h @@ -7,14 +7,14 @@ struct Point3D { - float x, y, z; + float x, y, z; // millimetres Point3D() : x(0), y(0), z(0) {} Point3D(float _x, float _y, float _z) : x(_x), y(_y), z(_z) {} }; struct Point3DRGB { - float x, y, z; + float x, y, z; // millimetres uint8_t r, g, b; float pointSize; diff --git a/CloudView3D/Inc/PointCloudGLWidget.h b/CloudView3D/Inc/PointCloudGLWidget.h index 7af1aa0..7392e60 100644 --- a/CloudView3D/Inc/PointCloudGLWidget.h +++ b/CloudView3D/Inc/PointCloudGLWidget.h @@ -9,8 +9,9 @@ #include #include #include -#include -#include +#include +#include +#include #include "CloudView3DTypes.h" #include "VZNL_Types.h" @@ -174,10 +175,12 @@ 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 addPointCloud(const PointCloudXYZ& cloud, const QString& name = ""); + void addPointCloud(const PointCloudXYZRGB& cloud, const QString& name = "", + bool resetCamera = true); + void appendPointCloud(const PointCloudXYZRGB& cloud, const QString& name = ""); + void removeOldestPointCloud(); + void clearPointClouds(); void setPointCloudColor(PointCloudColor color); void setPointSize(float size); void resetView(); @@ -370,8 +373,8 @@ protected: void keyPressEvent(QKeyEvent* event) override; void resizeEvent(QResizeEvent* event) override; -private: - struct PointCloudData +private: + struct PointCloudData { std::vector vertices; std::vector colors; @@ -460,10 +463,10 @@ private: // 禁止拷贝 PointCloudData(const PointCloudData&) = delete; - PointCloudData& operator=(const PointCloudData&) = delete; - }; - - void computeBoundingBox(); + PointCloudData& operator=(const PointCloudData&) = delete; + }; + + void computeBoundingBox(); void updateProjection(const QMatrix4x4& modelView); float fittedCameraDistance(float padding = 1.05f) const; float minimumCameraDistance() const; @@ -478,8 +481,9 @@ private: void drawBasicShapeSegments(); // 绘制基础图形 void drawBasicShapePoints(); void drawLineSegmentList(const QVector& segments); - void drawPosePoints(); // 绘制姿态点 - void drawAxisLabels(); // 绘制坐标轴 XYZ 标注 + void drawPosePoints(); // 绘制姿态点 + void drawAxisLabels(); // 绘制坐标轴 XYZ 标注 + void drawSelectionInfoOverlay(); // 右上角显示选点和测量结果 void uploadToVBO(PointCloudData& data); // 上传数据到 VBO void releaseVBO(PointCloudData& data); // 释放 VBO 资源 @@ -504,7 +508,9 @@ private: void updateViewAngleDisplay(); bool handlePinchGesture(QPinchGesture* gesture); - std::vector m_pointClouds; + // Each entry owns QOpenGLBuffer objects. deque avoids relocating existing + // entries when new scan lines are appended or the oldest one is removed. + std::deque m_pointClouds; QMatrix4x4 m_projection; QMatrix4x4 m_view; diff --git a/CloudView3D/Src/PointCloudGLWidget.cpp b/CloudView3D/Src/PointCloudGLWidget.cpp index 8937c52..da7ceb9 100644 --- a/CloudView3D/Src/PointCloudGLWidget.cpp +++ b/CloudView3D/Src/PointCloudGLWidget.cpp @@ -1,5 +1,5 @@ #include "PointCloudGLWidget.h" -#include +#include #include #include #include @@ -7,22 +7,24 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include #include #include #include - -#define CLOUD_LOG_INFO(...) \ - qInfo().noquote() << QString::asprintf(__VA_ARGS__).trimmed() -#define CLOUD_LOG_WARN(...) \ - qWarning().noquote() << QString::asprintf(__VA_ARGS__).trimmed() + +#define CLOUD_LOG_INFO(...) \ + qInfo().noquote() << QString::asprintf(__VA_ARGS__).trimmed() +#define CLOUD_LOG_WARN(...) \ + qWarning().noquote() << QString::asprintf(__VA_ARGS__).trimmed() // OpenGL/GLU 头文件 #ifdef _WIN32 @@ -410,7 +412,7 @@ void PointCloudGLWidget::initializeGL() { initializeOpenGLFunctions(); - CLOUD_LOG_INFO("[CloudView] OpenGL initialized, version: %s\n", reinterpret_cast(glGetString(GL_VERSION))); + CLOUD_LOG_INFO("[CloudView] OpenGL initialized, version: %s\n", reinterpret_cast(glGetString(GL_VERSION))); glClearColor(0.15f, 0.15f, 0.15f, 1.0f); glEnable(GL_DEPTH_TEST); @@ -471,19 +473,11 @@ bool PointCloudGLWidget::ensureShader() "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; } @@ -652,7 +646,7 @@ void PointCloudGLWidget::paintGL() if (++frameCount % 100 == 0) { // 从四元数转换为欧拉角用于显示 QVector3D euler = m_rotation.toEulerAngles(); - CLOUD_LOG_INFO("[CloudView] Current rotation: pitch=%.1f, yaw=%.1f, roll=%.1f\n", + CLOUD_LOG_INFO("[CloudView] Current rotation: pitch=%.1f, yaw=%.1f, roll=%.1f\n", euler.x(), euler.y(), euler.z()); } @@ -731,14 +725,15 @@ void PointCloudGLWidget::paintGL() // 使用 QPainter 绘制坐标轴 XYZ 标注 drawAxisLabels(); + drawSelectionInfoOverlay(); } void PointCloudGLWidget::addPointCloud(const PointCloudXYZ& cloud, const QString& name) { - CLOUD_LOG_INFO("[CloudView] addPointCloud called, cloud size: %zu\n", cloud.size()); + CLOUD_LOG_INFO("[CloudView] addPointCloud called, cloud size: %zu\n", cloud.size()); if (cloud.empty()) { - CLOUD_LOG_WARN("[CloudView] Cloud is empty, returning\n"); + CLOUD_LOG_WARN("[CloudView] Cloud is empty, returning\n"); return; } @@ -802,10 +797,10 @@ void PointCloudGLWidget::addPointCloud(const PointCloudXYZ& cloud, const QString data.pointsPerLine = static_cast(cloud.size()) / data.totalLines; } - CLOUD_LOG_INFO("[CloudView] Valid vertices count: %zu, colorIndex: %d, totalLines: %d, pointsPerLine: %d\n", + CLOUD_LOG_INFO("[CloudView] Valid vertices count: %zu, colorIndex: %d, totalLines: %d, pointsPerLine: %d\n", data.vertices.size() / 3, data.colorIndex, data.totalLines, data.pointsPerLine); - m_pointClouds.push_back(std::move(data)); + m_pointClouds.push_back(std::move(data)); // 上传 VBO(如果当前在 GL 上下文中) makeCurrent(); @@ -813,17 +808,18 @@ void PointCloudGLWidget::addPointCloud(const PointCloudXYZ& cloud, const QString doneCurrent(); computeBoundingBox(); - CLOUD_LOG_INFO("[CloudView] BoundingBox min: (%.3f, %.3f, %.3f) max: (%.3f, %.3f, %.3f)\n", + CLOUD_LOG_INFO("[CloudView] BoundingBox min: (%.3f, %.3f, %.3f) max: (%.3f, %.3f, %.3f)\n", m_minBound.x(), m_minBound.y(), m_minBound.z(), m_maxBound.x(), m_maxBound.y(), m_maxBound.z()); - CLOUD_LOG_INFO("[CloudView] Center: (%.3f, %.3f, %.3f) Distance: %.3f\n", + CLOUD_LOG_INFO("[CloudView] Center: (%.3f, %.3f, %.3f) Distance: %.3f\n", m_center.x(), m_center.y(), m_center.z(), m_distance); resetView(); update(); } -void PointCloudGLWidget::addPointCloud(const PointCloudXYZRGB& cloud, const QString& name) +void PointCloudGLWidget::addPointCloud(const PointCloudXYZRGB& cloud, const QString& name, + bool resetCamera) { if (cloud.empty()) { return; @@ -913,7 +909,7 @@ void PointCloudGLWidget::addPointCloud(const PointCloudXYZRGB& cloud, const QStr group.indices = std::move(pair.second); data.customPointSizeGroups.push_back(std::move(group)); } - CLOUD_LOG_INFO("[CloudView] Found %zu custom point size groups\n", data.customPointSizeGroups.size()); + CLOUD_LOG_INFO("[CloudView] Found %zu custom point size groups\n", data.customPointSizeGroups.size()); } // 计算每线点数 @@ -921,17 +917,27 @@ void PointCloudGLWidget::addPointCloud(const PointCloudXYZRGB& cloud, const QStr data.pointsPerLine = static_cast(cloud.size()) / data.totalLines; } - m_pointClouds.push_back(std::move(data)); + m_pointClouds.push_back(std::move(data)); // 上传 VBO makeCurrent(); uploadToVBO(m_pointClouds.back()); - doneCurrent(); - - computeBoundingBox(); - resetView(); - update(); -} + doneCurrent(); + + computeBoundingBox(); + if (resetCamera) { + resetView(); + } else { + // A line-scan cloud grows along X over time. Keep the user's view + // orientation/pan, but expand the camera distance when the retained + // cloud no longer fits in the frustum. + const float fittedDistance = fittedCameraDistance(); + if (std::isfinite(fittedDistance)) { + m_distance = qMax(m_distance, fittedDistance); + } + } + update(); +} void PointCloudGLWidget::appendPointCloud(const PointCloudXYZRGB& cloud, const QString& name) @@ -1016,16 +1022,19 @@ void PointCloudGLWidget::appendPointCloud(const PointCloudXYZRGB& cloud, static_cast(data.vertices.size() / 3) / data.totalLines; } - makeCurrent(); - uploadToVBO(data); - doneCurrent(); + makeCurrent(); + uploadToVBO(data); + doneCurrent(); + + computeBoundingBox(); + const float fittedDistance = fittedCameraDistance(); + if (std::isfinite(fittedDistance)) { + m_distance = qMax(m_distance, fittedDistance); + } + update(); +} - computeBoundingBox(); - m_distance = qMax(m_distance, fittedCameraDistance()); - update(); -} - -void PointCloudGLWidget::clearPointClouds() +void PointCloudGLWidget::clearPointClouds() { // 释放所有 VBO makeCurrent(); @@ -1047,8 +1056,25 @@ void PointCloudGLWidget::clearPointClouds() m_center = QVector3D(0, 0, 0); m_colorIndex = 0; // 重置颜色索引 resetView(); - update(); -} + update(); +} + +void PointCloudGLWidget::removeOldestPointCloud() +{ + if (m_pointClouds.empty()) { + return; + } + + // deque::pop_front does not relocate the remaining PointCloudData objects. + makeCurrent(); + releaseVBO(m_pointClouds.front()); + doneCurrent(); + m_pointClouds.pop_front(); + + m_selectedLine = SelectedLineInfo(); + computeBoundingBox(); + update(); +} void PointCloudGLWidget::setPointCloudColor(PointCloudColor color) { @@ -1076,7 +1102,7 @@ void PointCloudGLWidget::resetView() m_pan = QVector3D(0, 0, 0); m_distance = fittedCameraDistance(); - CLOUD_LOG_INFO("[CloudView] resetView: size=(%.3f, %.3f, %.3f) maxSize=%.3f distance=%.3f\n", + CLOUD_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); @@ -1110,18 +1136,36 @@ void PointCloudGLWidget::updateProjection(const QMatrix4x4& modelView) maximumDepth = qMax(maximumDepth, depth); } - constexpr float kMinimumNearPlane = 0.01f; - float nearPlane = kMinimumNearPlane; - float farPlane = 1.0f; - if (maximumDepth > kMinimumNearPlane) { + constexpr float kMinimumNearPlane = 0.01f; + float nearPlane = kMinimumNearPlane; + float farPlane = 1.0f; + const float boundsDiagonal = (m_maxBound - m_minBound).length(); + const float safeDiagonal = std::isfinite(boundsDiagonal) + ? qMax(boundsDiagonal, 1.0f) + : 1.0f; + const float safeCameraDistance = std::isfinite(m_distance) + ? qMax(m_distance, 1.0f) + : 1.0f; + if (std::isfinite(minimumDepth) && std::isfinite(maximumDepth) && + 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); - } + farPlane = qMax(nearPlane + 1.0f, maximumDepth * 1.5f); + nearPlane = qMax(nearPlane, farPlane / 100000.0f); + } else { + // The scan range can cross the current camera plane while the view + // is being updated. Keep a conservative frustum instead of letting + // all points fall outside a near/far clip interval. + nearPlane = kMinimumNearPlane; + farPlane = safeCameraDistance + safeDiagonal * 2.0f; + } + + if (!std::isfinite(farPlane) || farPlane <= nearPlane) { + farPlane = nearPlane + safeCameraDistance + safeDiagonal * 2.0f; + } const float aspect = static_cast(width()) / static_cast(height() > 0 ? height() : 1); @@ -1209,7 +1253,7 @@ void PointCloudGLWidget::setViewAngles(float rotX, float rotY, float rotZ) m_rotation = QQuaternion::fromEulerAngles(m_rotationX, m_rotationY, m_rotationZ); m_distance = qMax(m_distance, minimumCameraDistance()); - CLOUD_LOG_INFO("[CloudView] setViewAngles: rotX=%.1f, rotY=%.1f, rotZ=%.1f\n", rotX, rotY, rotZ); + CLOUD_LOG_INFO("[CloudView] setViewAngles: rotX=%.1f, rotY=%.1f, rotZ=%.1f\n", rotX, rotY, rotZ); emit viewAnglesChanged(m_rotationX, m_rotationY, m_rotationZ); update(); @@ -1325,7 +1369,7 @@ void PointCloudGLWidget::updateSelectedPointCoord(int index, float x, float y, f m_selectedPoints[index].y = y; m_selectedPoints[index].z = z; - CLOUD_LOG_INFO("[CloudView] Updated selected point %d to (%.3f, %.3f, %.3f)\n", index, x, y, z); + CLOUD_LOG_INFO("[CloudView] Updated selected point %d to (%.3f, %.3f, %.3f)\n", index, x, y, z); // 刷新显示 update(); @@ -1350,7 +1394,7 @@ void PointCloudGLWidget::setSelectedPointCoord(int index, float x, float y, floa m_selectedPoints[index].lineIndex = -1; m_selectedPoints[index].pointIndexInLine = -1; - CLOUD_LOG_INFO("[CloudView] Set selected point %d to (%.3f, %.3f, %.3f)\n", index, x, y, z); + CLOUD_LOG_INFO("[CloudView] Set selected point %d to (%.3f, %.3f, %.3f)\n", index, x, y, z); // 刷新显示 update(); @@ -1683,10 +1727,10 @@ SelectedPointInfo PointCloudGLWidget::pickPoint(int screenX, int screenY) } } - CLOUD_LOG_INFO("[CloudView] Point selected: (%.3f, %.3f, %.3f) lineIndex=%d pointIndexInLine=%d screenDist=%.1f\n", + CLOUD_LOG_INFO("[CloudView] Point selected: (%.3f, %.3f, %.3f) lineIndex=%d pointIndexInLine=%d screenDist=%.1f\n", bestX, bestY, bestZ, bestLineIndex, result.pointIndexInLine, std::sqrt(minScreenDist)); } else { - CLOUD_LOG_INFO("[CloudView] No point selected, minScreenDist=%.1f\n", std::sqrt(minScreenDist)); + CLOUD_LOG_INFO("[CloudView] No point selected, minScreenDist=%.1f\n", std::sqrt(minScreenDist)); } return result; @@ -1923,45 +1967,110 @@ void PointCloudGLWidget::drawAxisLabels() painter.end(); } +void PointCloudGLWidget::drawSelectionInfoOverlay() +{ + QVector validPoints; + for (const SelectedPointInfo& point : m_selectedPoints) { + if (point.valid) { + validPoints.append(point); + } + } + if (validPoints.isEmpty()) { + return; + } + + QStringList lines; + for (int i = 0; i < validPoints.size() && i < MAX_SELECTED_POINTS; ++i) { + const SelectedPointInfo& point = validPoints[i]; + lines.append(QString("P%1 X: %2 mm Y: %3 mm Z: %4 mm") + .arg(i + 1) + .arg(point.x, 0, 'f', 3) + .arg(point.y, 0, 'f', 3) + .arg(point.z, 0, 'f', 3)); + } + + if (validPoints.size() >= 2) { + const SelectedPointInfo& p1 = validPoints[0]; + const SelectedPointInfo& p2 = validPoints[1]; + const float dx = p2.x - p1.x; + const float dy = p2.y - p1.y; + const float dz = p2.z - p1.z; + const float distance = calculateDistance(p1, p2); + const int precision = distance < 0.001f ? 6 : (distance < 1.0f ? 4 : 3); + lines.append(QString("Distance: %1 mm").arg(distance, 0, 'f', precision)); + lines.append(QString("dX: %1 mm dY: %2 mm dZ: %3 mm") + .arg(dx, 0, 'f', precision) + .arg(dy, 0, 'f', precision) + .arg(dz, 0, 'f', precision)); + } + + QPainter painter(this); + painter.setRenderHint(QPainter::Antialiasing); + painter.setRenderHint(QPainter::TextAntialiasing); + + QFont font = painter.font(); + font.setPointSize(10); + painter.setFont(font); + const QFontMetrics metrics(font); + + int textWidth = 0; + for (const QString& line : lines) { + textWidth = qMax(textWidth, metrics.horizontalAdvance(line)); + } + + const int margin = 10; + const int lineHeight = metrics.lineSpacing(); + const int textX = qMax(margin, width() - textWidth - margin); + int textY = margin + metrics.ascent(); + for (int i = 0; i < lines.size(); ++i) { + painter.setPen(QColor(240, 244, 248)); + painter.drawText(textX, textY, lines[i]); + textY += lineHeight; + } +} + void PointCloudGLWidget::mousePressEvent(QMouseEvent* event) { - m_lastMousePos = event->pos(); - - if (event->button() == Qt::LeftButton) { - // Ctrl+左键:选点 - if (event->modifiers() & Qt::ControlModifier) { - makeCurrent(); - SelectedPointInfo point = pickPoint(event->pos().x(), event->pos().y()); - doneCurrent(); - - if (point.valid) { - if (m_measureDistanceEnabled) { - // 启用测距:最多保留两个点 - if (m_selectedPoints.size() >= MAX_SELECTED_POINTS) { - m_selectedPoints.clear(); - } - m_selectedPoints.append(point); - - 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); - } - } else { - // 未启用测距:只保留一个点 - m_selectedPoints.clear(); - m_selectedPoints.append(point); - emit pointSelected(point); - } - - update(); - } - } else if (event->modifiers() & Qt::ShiftModifier) { + m_lastMousePos = event->pos(); + + if (event->button() == Qt::LeftButton) { + // Alt+左键:依次选择两个点并测距 + if (event->modifiers() & Qt::AltModifier) { + makeCurrent(); + SelectedPointInfo point = pickPoint(event->pos().x(), event->pos().y()); + doneCurrent(); + + if (point.valid) { + if (m_selectedPoints.size() >= MAX_SELECTED_POINTS) { + m_selectedPoints.clear(); + } + m_selectedPoints.append(point); + 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); + } + + update(); + } + } else if (event->modifiers() & Qt::ControlModifier) { + // Ctrl+左键:选择一个点 + makeCurrent(); + SelectedPointInfo point = pickPoint(event->pos().x(), event->pos().y()); + doneCurrent(); + + if (point.valid) { + m_selectedPoints.clear(); + m_selectedPoints.append(point); + emit pointSelected(point); + update(); + } + } else if (event->modifiers() & Qt::ShiftModifier) { // Shift+左键:选线 makeCurrent(); SelectedPointInfo point = pickPoint(event->pos().x(), event->pos().y()); @@ -2055,38 +2164,29 @@ void PointCloudGLWidget::mouseDoubleClickEvent(QMouseEvent* event) void PointCloudGLWidget::mouseMoveEvent(QMouseEvent* event) { QPoint delta = event->pos() - m_lastMousePos; - m_lastMousePos = event->pos(); - - if (m_leftButtonPressed) { - // Alt+左键拖动:绕视线方向旋转(滚转) - if (event->modifiers() & Qt::AltModifier) { - // 绕Z轴(视线方向)旋转(修正方向) - float angle = -delta.x() * 0.5f; // 取反修正方向 - QQuaternion deltaRotation = QQuaternion::fromAxisAndAngle(QVector3D(0, 0, 1), angle); - m_rotation = deltaRotation * m_rotation; - m_rotationZ = NormalizeAngleDegrees(m_rotationZ + angle); - } else { - // 普通左键拖动:基于当前物体坐标系的旋转 - // 鼠标水平移动 -> 绕当前Y轴旋转 - // 鼠标垂直移动 -> 绕当前X轴旋转 - - // 计算旋转角度 - float angleX = delta.y() * 0.5f; // 垂直移动 - float angleY = delta.x() * 0.5f; // 水平移动 - - // 创建增量旋转四元数(在相机空间中) - // 先绕X轴旋转(俯仰),再绕Y轴旋转(偏航) - QQuaternion deltaRotationX = QQuaternion::fromAxisAndAngle(QVector3D(1, 0, 0), angleX); - QQuaternion deltaRotationY = QQuaternion::fromAxisAndAngle(QVector3D(0, 1, 0), angleY); - QQuaternion deltaRotation = deltaRotationY * deltaRotationX; - - // 应用增量旋转(左乘,相机空间旋转) - m_rotation = deltaRotation * m_rotation; - - // 更新欧拉角(用于显示),归一化到 ±180° - m_rotationX = NormalizeAngleDegrees(m_rotationX + angleX); - m_rotationY = NormalizeAngleDegrees(m_rotationY + angleY); - } + m_lastMousePos = event->pos(); + + if (m_leftButtonPressed) { + // 左键拖动:基于当前物体坐标系的旋转 + // 鼠标水平移动 -> 绕当前Y轴旋转 + // 鼠标垂直移动 -> 绕当前X轴旋转 + + // 计算旋转角度 + float angleX = delta.y() * 0.5f; // 垂直移动 + float angleY = delta.x() * 0.5f; // 水平移动 + + // 创建增量旋转四元数(在相机空间中) + // 先绕X轴旋转(俯仰),再绕Y轴旋转(偏航) + QQuaternion deltaRotationX = QQuaternion::fromAxisAndAngle(QVector3D(1, 0, 0), angleX); + QQuaternion deltaRotationY = QQuaternion::fromAxisAndAngle(QVector3D(0, 1, 0), angleY); + QQuaternion deltaRotation = deltaRotationY * deltaRotationX; + + // 应用增量旋转(左乘,相机空间旋转) + m_rotation = deltaRotation * m_rotation; + + // 更新欧拉角(用于显示),归一化到 ±180° + m_rotationX = NormalizeAngleDegrees(m_rotationX + angleX); + m_rotationY = NormalizeAngleDegrees(m_rotationY + angleY); // 发送角度变化信号 m_distance = qMax(m_distance, minimumCameraDistance()); @@ -2128,10 +2228,14 @@ void PointCloudGLWidget::wheelEvent(QWheelEvent* event) update(); } -void PointCloudGLWidget::keyPressEvent(QKeyEvent* event) -{ - if (event->key() == Qt::Key_Space) { - resetView(); +void PointCloudGLWidget::keyPressEvent(QKeyEvent* event) +{ + if (event->key() == Qt::Key_Escape) { + m_selectedPoints.clear(); + update(); + event->accept(); + } else if (event->key() == Qt::Key_Space) { + resetView(); } else { QOpenGLWidget::keyPressEvent(event); } diff --git a/VrUtils/CMakeLists.txt b/VrUtils/CMakeLists.txt index 2252999..2392855 100644 --- a/VrUtils/CMakeLists.txt +++ b/VrUtils/CMakeLists.txt @@ -1,34 +1,112 @@ -ADD_DEFINITIONS(-D__OPENVMS__) #log4cpp 编译选项 -if(NOT WIN32) - ADD_DEFINITIONS(-DLOG4CPP_HAVE_GETTIMEOFDAY) #ARM/Linux 上直接启用 gettimeofday,确保毫秒正确 -endif() -ADD_DEFINITIONS(-DVR_UTILS_EXPORTS) #VrUtils 导出宏定义 -# ADD_COMPILE_OPTIONS(-Wno-deprecated-declarations) #auto_ptr警告消除 - -# 设置CMake策略,允许AUTOMOC和AUTOUIC处理.hh文件 -# cmake_policy(SET CMP0100 NEW) - -INCLUDE_DIRECTORIES( - ./_Inc - ./Inc - ./MD5 - ./tinyxml2 - ./log4cpp/include -) - -AUX_SOURCE_DIRECTORY(./Src SrcS) -AUX_SOURCE_DIRECTORY(./MD5 MD5SrcS) -AUX_SOURCE_DIRECTORY(./tinyxml2 TinyxmlSrcS) -AUX_SOURCE_DIRECTORY(./log4cpp/src LogSrcS) - -# 对特定的.hh文件设置SKIP_AUTOGEN属性 -set_property(SOURCE log4cpp/src/StringUtil.hh PROPERTY SKIP_AUTOGEN ON) -set_property(SOURCE log4cpp/src/PortabilityImpl.hh PROPERTY SKIP_AUTOGEN ON) -set_property(SOURCE log4cpp/src/Properties.hh PROPERTY SKIP_AUTOGEN ON) -set_property(SOURCE log4cpp/src/Localtime.hh PROPERTY SKIP_AUTOGEN ON) -set_property(SOURCE log4cpp/src/PropertyConfiguratorImpl.hh PROPERTY SKIP_AUTOGEN ON) - -ADD_LIBRARY(VrUtils STATIC ${SrcS} ${MD5SrcS} - ${TinyxmlSrcS} - ${LogSrcS} - ) +cmake_minimum_required(VERSION 3.14) + +# Keep this source list aligned with VrUtils.pro. Do not glob Src/: it contains +# legacy standalone sources such as getopt.c that are not part of VrUtils. +set(VRUTILS_SOURCES + Src/VrDateUtils.cpp + Src/VrFileUtils.cpp + Src/VrLog.cpp + Src/VrNetUtils.cpp + Src/VrStringUtils.cpp + Src/VrTimeUtils.cpp + ini/ConvertUTF.c + log4cpp/src/AbortAppender.cpp + log4cpp/src/Appender.cpp + log4cpp/src/AppenderSkeleton.cpp + log4cpp/src/AppendersFactory.cpp + log4cpp/src/BasicConfigurator.cpp + log4cpp/src/BasicLayout.cpp + log4cpp/src/BufferingAppender.cpp + log4cpp/src/Category.cpp + log4cpp/src/CategoryStream.cpp + log4cpp/src/Configurator.cpp + log4cpp/src/DailyRollingFileAppender.cpp + log4cpp/src/DllMain.cpp + log4cpp/src/DummyThreads.cpp + log4cpp/src/FactoryParams.cpp + log4cpp/src/FileAppender.cpp + log4cpp/src/Filter.cpp + log4cpp/src/FixedContextCategory.cpp + log4cpp/src/HierarchyMaintainer.cpp + log4cpp/src/IdsaAppender.cpp + log4cpp/src/LayoutAppender.cpp + log4cpp/src/LayoutsFactory.cpp + log4cpp/src/LevelEvaluator.cpp + log4cpp/src/Localtime.cpp + log4cpp/src/LoggingEvent.cpp + log4cpp/src/MSThreads.cpp + log4cpp/src/Manipulator.cpp + log4cpp/src/NDC.cpp + log4cpp/src/NTEventLogAppender.cpp + log4cpp/src/OmniThreads.cpp + log4cpp/src/OstreamAppender.cpp + log4cpp/src/PThreads.cpp + log4cpp/src/PassThroughLayout.cpp + log4cpp/src/PatternLayout.cpp + log4cpp/src/PortabilityImpl.cpp + log4cpp/src/Priority.cpp + log4cpp/src/Properties.cpp + log4cpp/src/PropertyConfigurator.cpp + log4cpp/src/PropertyConfiguratorImpl.cpp + log4cpp/src/RemoteSyslogAppender.cpp + log4cpp/src/RollingFileAppender.cpp + log4cpp/src/SimpleConfigurator.cpp + log4cpp/src/SimpleLayout.cpp + log4cpp/src/SmtpAppender.cpp + log4cpp/src/StringQueueAppender.cpp + log4cpp/src/StringUtil.cpp + log4cpp/src/SyslogAppender.cpp + log4cpp/src/TimeStamp.cpp + log4cpp/src/TriggeringEventEvaluatorFactory.cpp + log4cpp/src/Win32DebugAppender.cpp + log4cpp/src/snprintf.c + tinyxml2/tinyxml2.cpp + jsoncpp/json_reader.cpp + jsoncpp/json_value.cpp + jsoncpp/json_writer.cpp +) + +add_library(VrUtils STATIC ${VRUTILS_SOURCES}) + +set_target_properties(VrUtils PROPERTIES + CXX_STANDARD 11 + CXX_STANDARD_REQUIRED ON +) + +target_include_directories(VrUtils + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/Inc + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/ini + ${CMAKE_CURRENT_SOURCE_DIR}/tinyxml2 + ${CMAKE_CURRENT_SOURCE_DIR}/jsoncpp + ${CMAKE_CURRENT_SOURCE_DIR}/log4cpp/include + ${CMAKE_CURRENT_SOURCE_DIR}/log4cpp/src +) + +target_compile_definitions(VrUtils PRIVATE + TINYXML2_EXPORT + HAVE_SNPRINTF + LOG4CPP_HAVE_SSTREAM=1 + VR_UTILS_EXPORTS +) + +if(UNIX AND NOT APPLE) + target_compile_definitions(VrUtils PRIVATE LOG4CPP_HAVE_UNISTD_H) + set_target_properties(VrUtils PROPERTIES POSITION_INDEPENDENT_CODE ON) +endif() + +find_package(Threads REQUIRED) +target_link_libraries(VrUtils PUBLIC Threads::Threads) + +if(WIN32) + # qmake's Windows mkspec supplies these definitions. log4cpp uses the + # generic Win32 API names and therefore must see the same character mode. + target_compile_definitions(VrUtils PRIVATE UNICODE _UNICODE) + target_link_libraries(VrUtils PUBLIC ws2_32 iphlpapi advapi32 shell32) +endif() + +if(MSVC) + target_compile_definitions(VrUtils PRIVATE _HAS_AUTO_PTR_ETC=1) + target_compile_options(VrUtils PRIVATE /utf-8) +endif()