From 2ee6bcd41498b340b8621301743ae598e2862b41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=B0=E4=BB=94?= Date: Fri, 11 Sep 2026 15:18:33 +0800 Subject: [PATCH] =?UTF-8?q?refactor(CloudView):=20=E6=8B=86=E5=88=86?= =?UTF-8?q?=E7=BC=A9=E6=94=BE=E6=8C=89=E9=92=AE=E5=B8=83=E5=B1=80=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E4=B8=BA=E7=8B=AC=E7=AB=8B=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 将原eventFilter中的缩放按钮位置计算代码抽离为updateZoomButtonGeometry独立函数 2. 在创建视图区域时主动调用该函数初始化按钮位置 3. 统一处理GL控件resize时的按钮重定位逻辑,提升代码可读性 4. 修复原代码未在初始化时设置按钮位置的问题 5. 新增按钮置顶操作确保缩放按钮始终显示在最上层 --- CloudUtils/CMakeLists.txt | 9 + CloudView3D/Inc/PointCloudGLWidget.h | 167 ++-- CloudView3D/Src/PointCloudGLWidget.cpp | 1017 +++++++++++++++++----- CloudViewApp/Inc/CloudViewMainWindow.h | 7 +- CloudViewApp/Src/CloudViewMainWindow.cpp | 64 +- 5 files changed, 933 insertions(+), 331 deletions(-) diff --git a/CloudUtils/CMakeLists.txt b/CloudUtils/CMakeLists.txt index 4f36108..f716080 100644 --- a/CloudUtils/CMakeLists.txt +++ b/CloudUtils/CMakeLists.txt @@ -32,6 +32,15 @@ target_include_directories(CloudUtils PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/Inc ) +# When CamTool is built with WoodSDK, use its VZ compatibility types first. +# This keeps LaserDataLoader's SVzNL* signatures identical to the application +# translation units, where WoodSDK maps them to the public SWD* structures. +if(TARGET WoodSDK) + target_include_directories(CloudUtils BEFORE PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/../../App/woodcam-sdk/WoodSDK/VzToWDInc" + ) +endif() + target_link_libraries(CloudUtils PUBLIC VrCommon diff --git a/CloudView3D/Inc/PointCloudGLWidget.h b/CloudView3D/Inc/PointCloudGLWidget.h index 7de9781..b5832be 100644 --- a/CloudView3D/Inc/PointCloudGLWidget.h +++ b/CloudView3D/Inc/PointCloudGLWidget.h @@ -6,27 +6,23 @@ #include #include #include -#include -#include -#include -<<<<<<< HEAD -#include -#include -#include -======= +#include +#include +#include +#include #include #include #include #include ->>>>>>> c6df042456fa4411f7b5269f8781f91c45da70c2 #include "CloudView3DTypes.h" #include "VZNL_Types.h" class QToolButton; -class QPushButton; -class QPinchGesture; -class QResizeEvent; +class QPushButton; +class QPinchGesture; +class QResizeEvent; +class QTimer; /** * @brief 点云显示颜色枚举 @@ -185,25 +181,36 @@ public: PointCloudGLWidget(QWidget* parent, bool showControlButtons); ~PointCloudGLWidget() override; -<<<<<<< HEAD - 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 setControlButtonsVisible(bool visible); bool controlButtonsVisible() const { return m_showControlButtons; } void addPointCloud(const PointCloudXYZ& cloud, const QString& name = "", bool resetView = true); void addPointCloud(const PointCloudXYZRGB& cloud, const QString& name = "", bool resetView = true); /** 删除最早添加的一组点云,用于维护滚动显示窗口。 */ - bool removeOldestPointCloud(); + bool removeOldestPointCloud(bool refreshDisplay = true); /** 删除最近添加的一组点云,用于回滚未完成的增量加载。 */ - bool removeNewestPointCloud(); - void clearPointClouds(); ->>>>>>> c6df042456fa4411f7b5269f8781f91c45da70c2 + bool removeNewestPointCloud(bool refreshDisplay = true); + void clearPointClouds(); + + /** 配置流式点云刷新频率;CamTool 使用 30 Hz。 */ + void setStreamRefreshRate(int refreshRateHz); + int streamRefreshRate() const + { + return m_streamRefreshRate; + } + /** 配置流式点云最多保留的源扫描线数。 */ + void setStreamLineLimit(int maxLines); + int streamLineLimit() const + { + return m_streamLineLimit; + } + void startStreamRefresh(); + void stopStreamRefresh(bool flushPending = true); + /** 缓存一批本地线号从 0 开始的扫描线,下一次刷新时统一提交。 */ + void queuePointCloudBatch(PointCloudXYZ cloud, int sourceLineCount, const QString& name = QString()); + void flushPointCloudBatches(); + void clearPointCloudBatches(); + void setPointCloudColor(PointCloudColor color); void setPointSize(float size); @@ -406,8 +413,10 @@ signals: float dx, float dy, float dz); - void lineSelected(const SelectedLineInfo& line); - void viewAnglesChanged(float rotX, float rotY, float rotZ); + void lineSelected(const SelectedLineInfo& line); + void viewAnglesChanged(float rotX, float rotY, float rotZ); + /** 30 Hz 刷新边界;数据源应在该信号处理中批量填充缓存。 */ + void streamRefreshRequested(); protected: bool event(QEvent* event) override; @@ -423,6 +432,13 @@ protected: void resizeEvent(QResizeEvent* event) override; private: + struct QueuedPointCloudBatch + { + PointCloudXYZ cloud; + QString name; + int sourceLineCount; + }; + struct PointCloudData { std::vector vertices; @@ -434,9 +450,11 @@ private: bool pseudoColor; bool hasLineInfo; // 是否有线信息 QString name; - int colorIndex; // 点云颜色索引 - int totalLines; // 总线数 - int pointsPerLine; // 每线点数(网格化点云) + int colorIndex; // 点云颜色索引 + int totalLines; // 总线数 + int pointsPerLine; // 每线点数(网格化点云) + bool streamBatch; + int sourceLineCount; // 自定义点大小分组(RGBA 中 A > 1 的点按大小分组) struct PointSizeGroup { @@ -450,14 +468,19 @@ private: QOpenGLBuffer vertexBuffer; // 顶点 VBO QOpenGLBuffer colorBuffer; // 颜色 VBO bool vboCreated; // VBO 是否已创建 + QVector3D minBound; + QVector3D maxBound; + bool hasBounds; PointCloudData() - : hasColor(false), pseudoColor(false), hasLineInfo(false), colorIndex(0) - , totalLines(0), pointsPerLine(0) - , hasCustomPointSizes(false) + : hasColor(false), pseudoColor(false), hasLineInfo(false), colorIndex(0) + , totalLines(0), pointsPerLine(0) + , streamBatch(false), sourceLineCount(0) + , hasCustomPointSizes(false) , vertexBuffer(QOpenGLBuffer::VertexBuffer) , colorBuffer(QOpenGLBuffer::VertexBuffer) , vboCreated(false) + , hasBounds(false) {} // 移动构造函数(QOpenGLBuffer 不支持拷贝) @@ -471,14 +494,19 @@ private: , pseudoColor(other.pseudoColor) , hasLineInfo(other.hasLineInfo) , name(std::move(other.name)) - , colorIndex(other.colorIndex) - , totalLines(other.totalLines) - , pointsPerLine(other.pointsPerLine) - , customPointSizeGroups(std::move(other.customPointSizeGroups)) + , colorIndex(other.colorIndex) + , totalLines(other.totalLines) + , pointsPerLine(other.pointsPerLine) + , streamBatch(other.streamBatch) + , sourceLineCount(other.sourceLineCount) + , customPointSizeGroups(std::move(other.customPointSizeGroups)) , hasCustomPointSizes(other.hasCustomPointSizes) , vertexBuffer(QOpenGLBuffer::VertexBuffer) , colorBuffer(QOpenGLBuffer::VertexBuffer) , vboCreated(false) + , minBound(other.minBound) + , maxBound(other.maxBound) + , hasBounds(other.hasBounds) { // VBO 需要在 GL 上下文中重建,标记为未创建 other.vboCreated = false; @@ -502,11 +530,16 @@ private: pseudoColor = other.pseudoColor; hasLineInfo = other.hasLineInfo; name = std::move(other.name); - colorIndex = other.colorIndex; - totalLines = other.totalLines; - pointsPerLine = other.pointsPerLine; - customPointSizeGroups = std::move(other.customPointSizeGroups); + colorIndex = other.colorIndex; + totalLines = other.totalLines; + pointsPerLine = other.pointsPerLine; + streamBatch = other.streamBatch; + sourceLineCount = other.sourceLineCount; + customPointSizeGroups = std::move(other.customPointSizeGroups); hasCustomPointSizes = other.hasCustomPointSizes; + minBound = other.minBound; + maxBound = other.maxBound; + hasBounds = other.hasBounds; // VBO 需要重建 other.vboCreated = false; } @@ -515,13 +548,15 @@ 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; + /** 按当前缩放把点大小从逻辑像素换到实际绘制像素,避免放大后点散成看不见的碎点。 */ + float effectivePointSize() const; QVector3D colorForIndex(int colorIndex) const; SelectedPointInfo pickPoint(int screenX, int screenY); void drawSelectedPoints(); @@ -534,21 +569,26 @@ private: void drawBasicShapeSegments(); // 绘制基础图形 void drawBasicShapePoints(); void drawLineSegmentList(const QVector& segments); -<<<<<<< HEAD - void drawPosePoints(); // 绘制姿态点 - void drawAxisLabels(); // 绘制坐标轴 XYZ 标注 - void drawSelectionInfoOverlay(); // 右上角显示选点和测量结果 -======= + void drawPosePoints(); // 绘制姿态点 void drawAxisLabels(); // 绘制坐标轴 XYZ 标注 + void drawSelectionInfoOverlay(); // 右上角显示选点和测量结果 QVector3D pseudoColorForZ(float z) const; void rebuildPseudoColors(PointCloudData& data, size_t firstVertex); void updatePseudoColors(); void createControlButtons(); void updateControlButtonGeometry(); ->>>>>>> c6df042456fa4411f7b5269f8781f91c45da70c2 - void uploadToVBO(PointCloudData& data); // 上传数据到 VBO - void releaseVBO(PointCloudData& data); // 释放 VBO 资源 + + void uploadToVBO(PointCloudData& data); // 上传数据到 VBO + void releaseVBO(PointCloudData& data); // 释放 VBO 资源 + void expandCloudBounds(PointCloudData& data, float x, float y, float z); + void addPointCloudInternal(const PointCloudXYZ& cloud, const QString& name, bool resetViewAfterAdd, bool refreshDisplay); + void trimQueuedPointCloudBatches(); + void trimDisplayedStreamBatches(bool refreshDisplay); + bool removeOldestStreamPointCloud(bool refreshDisplay); + void discardLeadingLines(QueuedPointCloudBatch& batch, int lineCount); + void discardLeadingStreamLines(PointCloudData& data, int lineCount); + void recomputeCloudBounds(PointCloudData& data); std::deque m_pointClouds; @@ -570,17 +610,8 @@ private: void createOverlayControls(); void repositionOverlayControls(); void showViewAngleDialog(); -<<<<<<< HEAD - void updateViewAngleDisplay(); - bool handlePinchGesture(QPinchGesture* gesture); - - // 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; -======= void updateViewAngleDisplay(); bool handlePinchGesture(QPinchGesture* gesture); ->>>>>>> c6df042456fa4411f7b5269f8781f91c45da70c2 QMatrix4x4 m_projection; QMatrix4x4 m_view; @@ -653,8 +684,16 @@ private: QVector m_viewButtons; QPushButton* m_viewAngleButton; // 左下角旋转角度显示按钮 - QToolButton* m_zoomInButton; - QToolButton* m_zoomOutButton; -}; + QToolButton* m_zoomInButton; + QToolButton* m_zoomOutButton; + + // 高速输入先进入 CPU 有界缓存,再由 GUI 线程按固定频率批量提交 OpenGL。 + QTimer* m_streamRefreshTimer; + std::deque m_queuedPointCloudBatches; + int m_queuedPointCloudLineCount; + int m_streamDisplayedLineCount; + int m_streamRefreshRate; + int m_streamLineLimit; +}; #endif // POINT_CLOUD_GL_WIDGET_H diff --git a/CloudView3D/Src/PointCloudGLWidget.cpp b/CloudView3D/Src/PointCloudGLWidget.cpp index 1d287b9..271d1a8 100644 --- a/CloudView3D/Src/PointCloudGLWidget.cpp +++ b/CloudView3D/Src/PointCloudGLWidget.cpp @@ -15,13 +15,16 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include +#include #define CLOUD_LOG_INFO(...) \ qInfo().noquote() << QString::asprintf(__VA_ARGS__).trimmed() @@ -38,6 +41,10 @@ #include #endif +#ifndef GL_PROGRAM_POINT_SIZE +#define GL_PROGRAM_POINT_SIZE 0x8642 +#endif + PointCloudGLWidget::PointCloudGLWidget(QWidget* parent) : PointCloudGLWidget(parent, true) { @@ -145,6 +152,11 @@ PointCloudGLWidget::PointCloudGLWidget(QWidget* parent, bool showControlButtons) , m_viewAngleButton(nullptr) , m_zoomInButton(nullptr) , m_zoomOutButton(nullptr) + , m_streamRefreshTimer(new QTimer(this)) + , m_queuedPointCloudLineCount(0) + , m_streamDisplayedLineCount(0) + , m_streamRefreshRate(30) + , m_streamLineLimit(100) { setFocusPolicy(Qt::StrongFocus); setMouseTracking(true); @@ -158,6 +170,15 @@ PointCloudGLWidget::PointCloudGLWidget(QWidget* parent, bool showControlButtons) createOverlayControls(); connect(this, &PointCloudGLWidget::viewAnglesChanged, this, [this](float, float, float) { updateViewAngleDisplay(); }); + + m_streamRefreshTimer->setTimerType(Qt::PreciseTimer); + m_streamRefreshTimer->setInterval(33); + connect(m_streamRefreshTimer, &QTimer::timeout, this, [this]() + { + // 先让数据源批量填充缓存,再在同一个 GUI tick 内提交一次显示。 + emit streamRefreshRequested(); + flushPointCloudBatches(); + }); } bool PointCloudGLWidget::event(QEvent* event) @@ -258,6 +279,37 @@ void PointCloudGLWidget::releaseVBO(PointCloudData& data) data.vboCreated = false; } +void PointCloudGLWidget::expandCloudBounds(PointCloudData& data, float x, float y, float z) +{ + if (!data.hasBounds) { + data.minBound = QVector3D(x, y, z); + data.maxBound = QVector3D(x, y, z); + data.hasBounds = true; + return; + } + data.minBound.setX(qMin(data.minBound.x(), x)); + data.minBound.setY(qMin(data.minBound.y(), y)); + data.minBound.setZ(qMin(data.minBound.z(), z)); + data.maxBound.setX(qMax(data.maxBound.x(), x)); + data.maxBound.setY(qMax(data.maxBound.y(), y)); + data.maxBound.setZ(qMax(data.maxBound.z(), z)); +} + +void PointCloudGLWidget::recomputeCloudBounds(PointCloudData& data) +{ + data.hasBounds = false; + for (size_t index = 0; index + 2 < data.vertices.size(); index += 3) + { + const float x = data.vertices[index]; + const float y = data.vertices[index + 1]; + const float z = data.vertices[index + 2]; + if (std::isfinite(x) && std::isfinite(y) && std::isfinite(z)) + { + expandCloudBounds(data, x, y, z); + } + } +} + void PointCloudGLWidget::createOverlayControls() { const int buttonSize = 32; @@ -383,33 +435,47 @@ void PointCloudGLWidget::repositionOverlayControls() { const int widgetWidth = width(); const int widgetHeight = height(); - if (widgetWidth <= 0 || widgetHeight <= 0) { + if (widgetWidth <= 0 || widgetHeight <= 0) + { return; } // 左侧视图工具栏:垂直居中 - if (m_viewToolbar) { + if (m_viewToolbar) + { + // 首次显示和布局切换时先刷新内容尺寸,再按实际高度居中。 + if (m_viewToolbar->layout()) + { + m_viewToolbar->layout()->activate(); + } + m_viewToolbar->adjustSize(); m_viewToolbar->move(8, qMax(0, (widgetHeight - m_viewToolbar->height()) / 2)); + m_viewToolbar->raise(); } // 左下角旋转角度按钮 - if (m_viewAngleButton) { - m_viewAngleButton->adjustSize(); + if (m_viewAngleButton) + { const int margin = 8; - m_viewAngleButton->move(margin, - widgetHeight - m_viewAngleButton->height() - margin); + const int x = qMax(0, qMin(margin, widgetWidth - m_viewAngleButton->width())); + const int y = qMax(0, widgetHeight - m_viewAngleButton->height() - margin); + m_viewAngleButton->move(x, y); + m_viewAngleButton->raise(); } // 右侧缩放按钮:垂直居中 - if (m_zoomInButton && m_zoomOutButton) { + if (m_zoomInButton && m_zoomOutButton) + { const int buttonWidth = m_zoomInButton->width(); const int buttonHeight = m_zoomInButton->height(); const int margin = 8; const int gap = 4; - const int x = widgetWidth - buttonWidth - margin; + const int x = qMax(0, widgetWidth - buttonWidth - margin); const int y = qMax(0, (widgetHeight - buttonHeight * 2 - gap) / 2); m_zoomInButton->move(x, y); m_zoomOutButton->move(x, y + buttonHeight + gap); + m_zoomInButton->raise(); + m_zoomOutButton->raise(); } } @@ -475,9 +541,7 @@ void PointCloudGLWidget::initializeGL() // GLES2 总是使用 shader 点大小,无需此开关。 if (QOpenGLContext::currentContext() && !QOpenGLContext::currentContext()->isOpenGLES()) { -#ifdef GL_PROGRAM_POINT_SIZE glEnable(GL_PROGRAM_POINT_SIZE); -#endif } if (!m_dynamicVbo.isCreated()) { m_dynamicVbo.create(); @@ -491,7 +555,6 @@ bool PointCloudGLWidget::ensureShader() if (m_shaderReady) { return m_shader != nullptr; } - m_shaderReady = true; if (!m_shader) { auto* program = new QOpenGLShaderProgram(); @@ -529,11 +592,13 @@ bool PointCloudGLWidget::ensureShader() " gl_FragColor = vec4(vColor, vAlpha);\n" "}\n")) { delete program; + m_shaderReady = false; return false; } if (!program->link()) { delete program; + m_shaderReady = false; return false; } @@ -546,7 +611,8 @@ bool PointCloudGLWidget::ensureShader() m_useVertexColorLocation = m_shader->uniformLocation("uUseVertexColor"); m_uniformColorLocation = m_shader->uniformLocation("uColor"); } - return m_shader != nullptr; + m_shaderReady = m_shader != nullptr; + return m_shaderReady; } QVector3D PointCloudGLWidget::colorForIndex(int colorIndex) const @@ -572,6 +638,8 @@ void PointCloudGLWidget::drawCloud(PointCloudData& data, float pointSize) glUniformMatrix4fv(m_mvpLocation, 1, GL_FALSE, m_mvp.constData()); glUniform1f(m_pointSizeLocation, pointSize); glUniform1f(m_alphaLocation, 1.0f); + // 部分驱动忽略 shader 里的 gl_PointSize,固定管线点大小作为兜底。 + glPointSize(pointSize); data.vertexBuffer.bind(); glEnableVertexAttribArray(m_positionLocation); @@ -686,6 +754,8 @@ void PointCloudGLWidget::resizeGL(int w, int h) void PointCloudGLWidget::resizeEvent(QResizeEvent* event) { QOpenGLWidget::resizeEvent(event); + // 覆盖控件依赖视口尺寸,父布局变化后必须重新计算位置。 + repositionOverlayControls(); updateControlButtonGeometry(); } @@ -705,18 +775,10 @@ void PointCloudGLWidget::paintGL() m_view.translate(-m_center); - // 调试输出:每100帧输出一次当前旋转角度 - static int frameCount = 0; - if (++frameCount % 100 == 0) { - // 从四元数转换为欧拉角用于显示 - QVector3D euler = m_rotation.toEulerAngles(); - CLOUD_LOG_INFO("[CloudView] Current rotation: pitch=%.1f, yaw=%.1f, roll=%.1f\n", - euler.x(), euler.y(), euler.z()); - } - QMatrix4x4 modelView = m_view * m_model; updateProjection(modelView); m_mvp = m_projection * modelView; + const float pointSize = effectivePointSize(); for (size_t cloudIdx = 0; cloudIdx < m_pointClouds.size(); ++cloudIdx) { auto& cloudData = m_pointClouds[cloudIdx]; @@ -729,7 +791,7 @@ void PointCloudGLWidget::paintGL() uploadToVBO(cloudData); } - drawCloud(cloudData, m_pointSize); + drawCloud(cloudData, pointSize); // 绘制自定义大小的点(RGBA 中 A > 1 的点) if (cloudData.hasCustomPointSizes) { @@ -795,8 +857,11 @@ void PointCloudGLWidget::paintGL() void PointCloudGLWidget::addPointCloud(const PointCloudXYZ& cloud, const QString& name, bool resetViewAfterAdd) { - CLOUD_LOG_INFO("[CloudView] addPointCloud called, cloud size: %zu\n", cloud.size()); + addPointCloudInternal(cloud, name, resetViewAfterAdd, true); +} +void PointCloudGLWidget::addPointCloudInternal(const PointCloudXYZ& cloud, const QString& name, bool resetViewAfterAdd, bool refreshDisplay) +{ if (cloud.empty()) { CLOUD_LOG_WARN("[CloudView] Cloud is empty, returning\n"); return; @@ -847,6 +912,7 @@ void PointCloudGLWidget::addPointCloud(const PointCloudXYZ& cloud, const QString data.vertices.push_back(pt.x); data.vertices.push_back(pt.y); data.vertices.push_back(pt.z); + expandCloudBounds(data, pt.x, pt.y, pt.z); // 保存原始索引 data.originalIndices.push_back(static_cast(i)); @@ -870,40 +936,30 @@ void PointCloudGLWidget::addPointCloud(const PointCloudXYZ& cloud, const QString data.pointsPerLine = maxPointCount; } -<<<<<<< HEAD - CLOUD_LOG_INFO("[CloudView] Valid vertices count: %zu, colorIndex: %d, totalLines: %d, pointsPerLine: %d\n", -======= // 新建纯 XYZ 点云时直接生成颜色,避免随后再次上传同一份 VBO。 rebuildPseudoColors(data, 0); - LOG_INFO("[CloudView] Valid vertices count: %zu, colorIndex: %d, totalLines: %d, pointsPerLine: %d\n", ->>>>>>> c6df042456fa4411f7b5269f8781f91c45da70c2 - data.vertices.size() / 3, data.colorIndex, data.totalLines, data.pointsPerLine); + m_pointClouds.push_back(std::move(data)); + // VBO 推迟到 paintGL,避免高频 add 时反复 makeCurrent。 - m_pointClouds.push_back(std::move(data)); - - // 上传 VBO(如果当前在 GL 上下文中) - makeCurrent(); - uploadToVBO(m_pointClouds.back()); - doneCurrent(); - - computeBoundingBox(); - 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", - m_center.x(), m_center.y(), m_center.z(), m_distance); + if (!refreshDisplay) + { + return; + } if (resetViewAfterAdd) { + computeBoundingBox(); resetView(); + } else { + computeBoundingBox(); + const float fittedDistance = fittedCameraDistance(); + if (std::isfinite(fittedDistance)) { + m_distance = qMax(m_distance, fittedDistance); + } } update(); } -<<<<<<< HEAD -void PointCloudGLWidget::addPointCloud(const PointCloudXYZRGB& cloud, const QString& name, - bool resetCamera) -======= void PointCloudGLWidget::setControlButtonsVisible(bool visible) { m_showControlButtons = visible; @@ -922,6 +978,11 @@ void PointCloudGLWidget::setControlButtonsVisible(bool visible) if (m_zoomOutButton) { m_zoomOutButton->setVisible(visible); } + if (visible) + { + repositionOverlayControls(); + updateControlButtonGeometry(); + } } void PointCloudGLWidget::createControlButtons() @@ -1038,7 +1099,6 @@ void PointCloudGLWidget::updateControlButtonGeometry() } void PointCloudGLWidget::addPointCloud(const PointCloudXYZRGB& cloud, const QString& name, bool resetViewAfterAdd) ->>>>>>> c6df042456fa4411f7b5269f8781f91c45da70c2 { if (cloud.empty()) { return; @@ -1100,6 +1160,7 @@ void PointCloudGLWidget::addPointCloud(const PointCloudXYZRGB& cloud, const QStr data.vertices.push_back(pt.x); data.vertices.push_back(pt.y); data.vertices.push_back(pt.z); + expandCloudBounds(data, pt.x, pt.y, pt.z); data.colors.push_back(pt.r / 255.0f); data.colors.push_back(pt.g / 255.0f); @@ -1144,38 +1205,23 @@ void PointCloudGLWidget::addPointCloud(const PointCloudXYZRGB& cloud, const QStr data.pointsPerLine = maxPointCount; } - m_pointClouds.push_back(std::move(data)); - - // 上传 VBO - makeCurrent(); - uploadToVBO(m_pointClouds.back()); -<<<<<<< HEAD - 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(); -} -======= - doneCurrent(); + m_pointClouds.push_back(std::move(data)); + // VBO 推迟到 paintGL,避免高频 add 时反复 makeCurrent。 computeBoundingBox(); if (resetViewAfterAdd) { 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(); } ->>>>>>> c6df042456fa4411f7b5269f8781f91c45da70c2 void PointCloudGLWidget::appendPointCloud(const PointCloudXYZRGB& cloud, const QString& name) @@ -1238,6 +1284,7 @@ void PointCloudGLWidget::appendPointCloud(const PointCloudXYZRGB& cloud, data.vertices.push_back(point.x); data.vertices.push_back(point.y); data.vertices.push_back(point.z); + expandCloudBounds(data, point.x, point.y, point.z); data.colors.push_back(point.r / 255.0f); data.colors.push_back(point.g / 255.0f); data.colors.push_back(point.b / 255.0f); @@ -1275,28 +1322,20 @@ void PointCloudGLWidget::appendPointCloud(const PointCloudXYZRGB& cloud, data.pointsPerLine = maxPointCount; } - makeCurrent(); - uploadToVBO(data); - doneCurrent(); - - computeBoundingBox(); - const float fittedDistance = fittedCameraDistance(); - if (std::isfinite(fittedDistance)) { - m_distance = qMax(m_distance, fittedDistance); - } - update(); -} + makeCurrent(); + uploadToVBO(data); + doneCurrent(); -<<<<<<< HEAD -void PointCloudGLWidget::clearPointClouds() -======= computeBoundingBox(); - m_distance = qMax(m_distance, fittedCameraDistance()); + const float fittedDistance = fittedCameraDistance(); + if (std::isfinite(fittedDistance)) { + m_distance = qMax(m_distance, fittedDistance); + } update(); } -void PointCloudGLWidget::appendPointCloud(const PointCloudXYZ& cloud, - const QString& name) + +void PointCloudGLWidget::appendPointCloud(const PointCloudXYZ& cloud, const QString& name) { if (cloud.empty()) { return; @@ -1356,6 +1395,7 @@ void PointCloudGLWidget::appendPointCloud(const PointCloudXYZ& cloud, data.vertices.push_back(point.x); data.vertices.push_back(point.y); data.vertices.push_back(point.z); + expandCloudBounds(data, point.x, point.y, point.z); data.originalIndices.push_back(static_cast(originalIndexOffset + index)); data.pointInLineIndices.push_back(sourcePointIndex); if (data.hasLineInfo) { @@ -1403,8 +1443,10 @@ void PointCloudGLWidget::setNewestPointCloudLineLayout(int totalLines, int point } void PointCloudGLWidget::clearPointClouds() ->>>>>>> c6df042456fa4411f7b5269f8781f91c45da70c2 { + clearPointCloudBatches(); + m_streamDisplayedLineCount = 0; + // 释放所有 VBO makeCurrent(); for (auto& cloudData : m_pointClouds) { @@ -1425,65 +1467,536 @@ void PointCloudGLWidget::clearPointClouds() m_center = QVector3D(0, 0, 0); m_colorIndex = 0; // 重置颜色索引 resetView(); - 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(); -} + update(); +} -bool PointCloudGLWidget::removeOldestPointCloud() +bool PointCloudGLWidget::removeOldestPointCloud(bool refreshDisplay) { if (m_pointClouds.empty()) { return false; } - makeCurrent(); - releaseVBO(m_pointClouds.front()); - doneCurrent(); + const int removedStreamLines = m_pointClouds.front().streamBatch + ? m_pointClouds.front().sourceLineCount + : 0; + if (m_pointClouds.front().vboCreated) { + makeCurrent(); + releaseVBO(m_pointClouds.front()); + doneCurrent(); + } m_pointClouds.pop_front(); + m_streamDisplayedLineCount = + qMax(0, m_streamDisplayedLineCount - removedStreamLines); // Cloud indices change after removing the first item, so stale selection // information must not be kept. m_selectedPoints.clear(); m_selectedLine = SelectedLineInfo(); - computeBoundingBox(); - updatePseudoColors(); - update(); + // 伪彩按绝对 Z 周期着色,删块不必重算其余点云颜色/VBO。 + if (refreshDisplay) { + computeBoundingBox(); + update(); + } return true; } -bool PointCloudGLWidget::removeNewestPointCloud() +bool PointCloudGLWidget::removeNewestPointCloud(bool refreshDisplay) { if (m_pointClouds.empty()) { return false; } - makeCurrent(); - releaseVBO(m_pointClouds.back()); - doneCurrent(); + const int removedStreamLines = m_pointClouds.back().streamBatch + ? m_pointClouds.back().sourceLineCount + : 0; + if (m_pointClouds.back().vboCreated) { + makeCurrent(); + releaseVBO(m_pointClouds.back()); + doneCurrent(); + } m_pointClouds.pop_back(); + m_streamDisplayedLineCount = + qMax(0, m_streamDisplayedLineCount - removedStreamLines); m_selectedPoints.clear(); m_selectedLine = SelectedLineInfo(); - computeBoundingBox(); - updatePseudoColors(); - update(); + if (refreshDisplay) { + computeBoundingBox(); + update(); + } return true; } +void PointCloudGLWidget::setStreamRefreshRate(int refreshRateHz) +{ + m_streamRefreshRate = qBound(1, refreshRateHz, 1000); + const int intervalMs = + qMax(1, qRound(1000.0 / static_cast(m_streamRefreshRate))); + m_streamRefreshTimer->setInterval(intervalMs); +} + +void PointCloudGLWidget::setStreamLineLimit(int maxLines) +{ + m_streamLineLimit = qMax(1, maxLines); + trimQueuedPointCloudBatches(); + trimDisplayedStreamBatches(true); +} + +void PointCloudGLWidget::startStreamRefresh() +{ + if (!m_streamRefreshTimer->isActive()) + { + m_streamRefreshTimer->start(); + } +} + +void PointCloudGLWidget::stopStreamRefresh(bool flushPending) +{ + m_streamRefreshTimer->stop(); + if (flushPending) + { + flushPointCloudBatches(); + } +} + +void PointCloudGLWidget::queuePointCloudBatch(PointCloudXYZ cloud, int sourceLineCount, const QString& name) +{ + if (cloud.empty() || sourceLineCount <= 0) + { + return; + } + + // 流式批次使用局部连续线号;缺失线号时只能安全地按单线处理。 + if (cloud.lineIndices.size() != cloud.points.size()) + { + if (sourceLineCount != 1) + { + CLOUD_LOG_WARN("[CloudView] Stream batch has no complete line index map; treating it as one line\n"); + } + sourceLineCount = 1; + cloud.lineIndices.assign(cloud.points.size(), 0); + } + else + { + for (int& lineIndex : cloud.lineIndices) + { + lineIndex = qBound(0, lineIndex, sourceLineCount - 1); + } + } + + if (cloud.pointInLineIndices.size() != cloud.points.size()) + { + cloud.pointInLineIndices.assign(cloud.points.size(), -1); + } + + QueuedPointCloudBatch batch; + batch.cloud = std::move(cloud); + batch.name = name; + batch.sourceLineCount = sourceLineCount; + m_queuedPointCloudLineCount += sourceLineCount; + m_queuedPointCloudBatches.push_back(std::move(batch)); + + // 输入缓存有界,GUI 暂时跟不上时丢最旧扫描线以保持实时性。 + trimQueuedPointCloudBatches(); + startStreamRefresh(); +} + +void PointCloudGLWidget::discardLeadingLines(QueuedPointCloudBatch& batch, int lineCount) +{ + const int discardCount = qBound(0, lineCount, batch.sourceLineCount); + if (discardCount == 0) + { + return; + } + if (discardCount == batch.sourceLineCount) + { + batch.cloud.clear(); + batch.sourceLineCount = 0; + return; + } + + size_t writeIndex = 0; + for (size_t readIndex = 0; + readIndex < batch.cloud.points.size(); + ++readIndex) + { + const int lineIndex = batch.cloud.lineIndices[readIndex]; + if (lineIndex < discardCount) + { + continue; + } + + if (writeIndex != readIndex) + { + batch.cloud.points[writeIndex] = + std::move(batch.cloud.points[readIndex]); + batch.cloud.pointInLineIndices[writeIndex] = + batch.cloud.pointInLineIndices[readIndex]; + } + batch.cloud.lineIndices[writeIndex] = lineIndex - discardCount; + ++writeIndex; + } + batch.cloud.points.resize(writeIndex); + batch.cloud.lineIndices.resize(writeIndex); + batch.cloud.pointInLineIndices.resize(writeIndex); + batch.sourceLineCount -= discardCount; +} + +void PointCloudGLWidget::trimQueuedPointCloudBatches() +{ + while (m_queuedPointCloudLineCount > m_streamLineLimit && + !m_queuedPointCloudBatches.empty()) + { + const int overflow = + m_queuedPointCloudLineCount - m_streamLineLimit; + QueuedPointCloudBatch& oldest = m_queuedPointCloudBatches.front(); + if (oldest.sourceLineCount <= overflow) + { + m_queuedPointCloudLineCount -= oldest.sourceLineCount; + m_queuedPointCloudBatches.pop_front(); + continue; + } + + discardLeadingLines(oldest, overflow); + m_queuedPointCloudLineCount -= overflow; + } +} + +void PointCloudGLWidget::flushPointCloudBatches() +{ + if (m_queuedPointCloudBatches.empty()) + { + return; + } + + size_t totalPointCount = 0; + for (const QueuedPointCloudBatch& batch : m_queuedPointCloudBatches) + { + totalPointCount += batch.cloud.size(); + } + + PointCloudXYZ mergedCloud; + mergedCloud.reserve(totalPointCount); + QString cloudName; + int mergedLineCount = 0; + for (QueuedPointCloudBatch& batch : m_queuedPointCloudBatches) + { + mergedCloud.points.insert( + mergedCloud.points.end(), + std::make_move_iterator(batch.cloud.points.begin()), + std::make_move_iterator(batch.cloud.points.end())); + for (int lineIndex : batch.cloud.lineIndices) + { + mergedCloud.lineIndices.push_back(lineIndex + mergedLineCount); + } + mergedCloud.pointInLineIndices.insert( + mergedCloud.pointInLineIndices.end(), + std::make_move_iterator(batch.cloud.pointInLineIndices.begin()), + std::make_move_iterator(batch.cloud.pointInLineIndices.end())); + if (!batch.name.isEmpty()) + { + cloudName = batch.name; + } + mergedLineCount += batch.sourceLineCount; + } + m_queuedPointCloudBatches.clear(); + m_queuedPointCloudLineCount = 0; + + if (mergedCloud.empty() || mergedLineCount <= 0) + { + return; + } + + const bool resetViewAfterAdd = m_pointClouds.empty(); + const int maxLinesPerBatch = qMax(1, m_streamLineLimit); + bool addedStreamBatch = false; + + // 一次刷新可能包含的线数大于显示窗口;按源线拆成小块,保证淘汰 + // 最旧数据时不会把本次唯一的新数据整块删除。 + for (int firstLine = 0; firstLine < mergedLineCount; + firstLine += maxLinesPerBatch) + { + const int chunkLineCount = + qMin(maxLinesPerBatch, mergedLineCount - firstLine); + PointCloudXYZ chunk; + chunk.lineIndices.reserve(mergedCloud.points.size()); + chunk.pointInLineIndices.reserve(mergedCloud.points.size()); + chunk.points.reserve(mergedCloud.points.size()); + + for (size_t pointIndex = 0; + pointIndex < mergedCloud.points.size(); ++pointIndex) + { + if (pointIndex >= mergedCloud.lineIndices.size()) + { + continue; + } + const int lineIndex = mergedCloud.lineIndices[pointIndex]; + if (lineIndex < firstLine || + lineIndex >= firstLine + chunkLineCount) + { + continue; + } + + chunk.points.push_back(std::move(mergedCloud.points[pointIndex])); + chunk.lineIndices.push_back(lineIndex - firstLine); + const int pointInLine = + pointIndex < mergedCloud.pointInLineIndices.size() + ? mergedCloud.pointInLineIndices[pointIndex] + : -1; + chunk.pointInLineIndices.push_back(pointInLine); + } + + if (chunk.empty()) + { + continue; + } + + const size_t cloudCountBeforeAdd = m_pointClouds.size(); + // 33 ms 内的每个源线块只构建一个 CPU 块,并延迟到 paintGL + // 上传 VBO;同一 tick 仍只触发一次最终刷新。 + addPointCloudInternal(chunk, cloudName, + resetViewAfterAdd && !addedStreamBatch, false); + if (m_pointClouds.size() == cloudCountBeforeAdd) + { + continue; + } + + PointCloudData& displayedBatch = m_pointClouds.back(); + displayedBatch.streamBatch = true; + displayedBatch.sourceLineCount = chunkLineCount; + // addPointCloudInternal 只能从有效点推断线数;这里恢复完整的 + // 源布局,包含被过滤为空的尾线。 + displayedBatch.totalLines = chunkLineCount; + m_streamDisplayedLineCount += chunkLineCount; + trimDisplayedStreamBatches(false); + addedStreamBatch = true; + } + + if (!addedStreamBatch) + { + return; + } + + computeBoundingBox(); + if (resetViewAfterAdd) + { + resetView(); + } + else + { + const float fittedDistance = fittedCameraDistance(); + if (std::isfinite(fittedDistance)) + { + m_distance = qMax(m_distance, fittedDistance); + } + update(); + } +} + +void PointCloudGLWidget::clearPointCloudBatches() +{ + m_queuedPointCloudBatches.clear(); + m_queuedPointCloudLineCount = 0; +} + +bool PointCloudGLWidget::removeOldestStreamPointCloud(bool refreshDisplay) +{ + const auto oldest = std::find_if( + m_pointClouds.begin(), m_pointClouds.end(), + [](const PointCloudData& data) + { + return data.streamBatch; + }); + if (oldest == m_pointClouds.end()) + { + return false; + } + + const int removedLineCount = oldest->sourceLineCount; + // deque 中间删除会移动后续对象;先释放这些 VBO,移动后由 paintGL 重建。 + makeCurrent(); + for (auto data = oldest; data != m_pointClouds.end(); ++data) + { + releaseVBO(*data); + } + m_pointClouds.erase(oldest); + doneCurrent(); + + m_streamDisplayedLineCount = + qMax(0, m_streamDisplayedLineCount - removedLineCount); + m_selectedPoints.clear(); + m_selectedLine = SelectedLineInfo(); + if (refreshDisplay) + { + computeBoundingBox(); + update(); + } + return true; +} + +void PointCloudGLWidget::trimDisplayedStreamBatches(bool refreshDisplay) +{ + bool removed = false; + while (m_streamDisplayedLineCount > m_streamLineLimit) + { + const int overflow = m_streamDisplayedLineCount - m_streamLineLimit; + auto oldest = std::find_if( + m_pointClouds.begin(), m_pointClouds.end(), + [](const PointCloudData& data) + { + return data.streamBatch; + }); + if (oldest != m_pointClouds.end() && + oldest->sourceLineCount > overflow && + (oldest->vertices.empty() || + (oldest->hasLineInfo && + oldest->lineIndices.size() == oldest->vertices.size() / 3))) + { + // 当前批次可能包含整个刷新周期;只裁掉其前缀,保留最新扫描线。 + makeCurrent(); + releaseVBO(*oldest); + discardLeadingStreamLines(*oldest, overflow); + doneCurrent(); + m_streamDisplayedLineCount -= overflow; + // 前缀裁剪后顶点索引已重排,旧选择不能继续指向当前数据。 + m_selectedPoints.clear(); + m_selectedLine = SelectedLineInfo(); + removed = true; + break; + } + + if (!removeOldestStreamPointCloud(false)) + { + m_streamDisplayedLineCount = 0; + break; + } + removed = true; + } + + if (removed && refreshDisplay) + { + computeBoundingBox(); + update(); + } +} + +void PointCloudGLWidget::discardLeadingStreamLines(PointCloudData& data, + int lineCount) +{ + const int discardCount = qBound(0, lineCount, data.sourceLineCount); + const size_t oldVertexCount = data.vertices.size() / 3; + if (discardCount == 0) + { + return; + } + + if (discardCount == data.sourceLineCount) + { + data.vertices.clear(); + data.colors.clear(); + data.lineIndices.clear(); + data.originalIndices.clear(); + data.pointInLineIndices.clear(); + data.customPointSizeGroups.clear(); + data.hasCustomPointSizes = false; + data.sourceLineCount = 0; + data.totalLines = 0; + data.hasBounds = false; + return; + } + + if (oldVertexCount == 0) + { + data.sourceLineCount -= discardCount; + data.totalLines = qMax(0, data.totalLines - discardCount); + return; + } + + if (!data.hasLineInfo || data.lineIndices.size() != oldVertexCount) + { + return; + } + + std::vector vertices; + std::vector colors; + std::vector lineIndices; + std::vector originalIndices; + std::vector pointInLineIndices; + std::vector remappedIndices(oldVertexCount, -1); + vertices.reserve(data.vertices.size()); + if (data.colors.size() == data.vertices.size()) + { + colors.reserve(data.colors.size()); + } + lineIndices.reserve(oldVertexCount); + originalIndices.reserve(data.originalIndices.size()); + pointInLineIndices.reserve(data.pointInLineIndices.size()); + + for (size_t oldIndex = 0; oldIndex < oldVertexCount; ++oldIndex) + { + const int oldLineIndex = data.lineIndices[oldIndex]; + if (oldLineIndex < discardCount) + { + continue; + } + + const size_t newIndex = vertices.size() / 3; + remappedIndices[oldIndex] = static_cast(newIndex); + const size_t vertexOffset = oldIndex * 3; + vertices.push_back(data.vertices[vertexOffset]); + vertices.push_back(data.vertices[vertexOffset + 1]); + vertices.push_back(data.vertices[vertexOffset + 2]); + if (data.colors.size() == data.vertices.size()) + { + colors.push_back(data.colors[vertexOffset]); + colors.push_back(data.colors[vertexOffset + 1]); + colors.push_back(data.colors[vertexOffset + 2]); + } + lineIndices.push_back(oldLineIndex - discardCount); + if (oldIndex < data.originalIndices.size()) + { + originalIndices.push_back(data.originalIndices[oldIndex]); + } + if (oldIndex < data.pointInLineIndices.size()) + { + pointInLineIndices.push_back(data.pointInLineIndices[oldIndex]); + } + } + + data.vertices = std::move(vertices); + data.colors = std::move(colors); + data.lineIndices = std::move(lineIndices); + data.originalIndices = std::move(originalIndices); + data.pointInLineIndices = std::move(pointInLineIndices); + + std::vector sizeGroups; + for (const PointCloudData::PointSizeGroup& oldGroup : + data.customPointSizeGroups) + { + PointCloudData::PointSizeGroup newGroup; + newGroup.pointSize = oldGroup.pointSize; + for (const size_t oldIndex : oldGroup.indices) + { + if (oldIndex < remappedIndices.size() && + remappedIndices[oldIndex] >= 0) + { + newGroup.indices.push_back( + static_cast(remappedIndices[oldIndex])); + } + } + if (!newGroup.indices.empty()) + { + sizeGroups.push_back(std::move(newGroup)); + } + } + data.customPointSizeGroups = std::move(sizeGroups); + data.hasCustomPointSizes = !data.customPointSizeGroups.empty(); + data.sourceLineCount -= discardCount; + data.totalLines = qMax(0, data.totalLines - discardCount); + + recomputeCloudBounds(data); + rebuildPseudoColors(data, 0); +} + void PointCloudGLWidget::setPointCloudColor(PointCloudColor color) { m_currentColor = color; @@ -1496,6 +2009,23 @@ void PointCloudGLWidget::setPointSize(float size) update(); } +float PointCloudGLWidget::effectivePointSize() const +{ + const float userSize = qMax(m_pointSize, 1.0f); + const float pixelRatio = static_cast(devicePixelRatioF()); + const float baseSize = userSize * qMax(pixelRatio, 1.0f); + + const float fitted = fittedCameraDistance(); + if (!std::isfinite(fitted) || !std::isfinite(m_distance) || + fitted <= 1.0f || m_distance <= 1e-3f) { + return baseSize; + } + + // 点精灵尺寸是屏幕像素;减半缩放系数和上限,避免拉近后点过大。 + const float zoomScale = qBound(1.0f, 0.1f * fitted / m_distance, 6.0f); + return qMin(baseSize * zoomScale, 8.0f); +} + QVector3D PointCloudGLWidget::pseudoColorForZ(float z) const { if (!std::isfinite(z)) @@ -1730,36 +2260,36 @@ void PointCloudGLWidget::updateProjection(const QMatrix4x4& modelView) maximumDepth = qMax(maximumDepth, depth); } - 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) { + 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); - } 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; - } + 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); @@ -2078,6 +2608,7 @@ void PointCloudGLWidget::transformAllClouds(const QMatrix4x4& matrix) cloudData.vertices[i + 1] = transformed.y(); cloudData.vertices[i + 2] = transformed.z(); } + recomputeCloudBounds(cloudData); } // 重新上传 VBO(顶点数据已变更) @@ -2108,6 +2639,7 @@ size_t PointCloudGLWidget::rotateCloudsByZThreshold(const QMatrix4x4& rotMatrix, ++rotatedCount; } } + recomputeCloudBounds(cloudData); } if (rotatedCount > 0) { @@ -2208,9 +2740,11 @@ void PointCloudGLWidget::computeBoundingBox() }; for (const auto& cloudData : m_pointClouds) { - for (size_t i = 0; i < cloudData.vertices.size(); i += 3) { - includePoint(cloudData.vertices[i], cloudData.vertices[i + 1], cloudData.vertices[i + 2]); + if (!cloudData.hasBounds) { + continue; } + includePoint(cloudData.minBound.x(), cloudData.minBound.y(), cloudData.minBound.z()); + includePoint(cloudData.maxBound.x(), cloudData.maxBound.y(), cloudData.maxBound.z()); } for (const LineSegment& segment : m_lineSegments) { @@ -2583,11 +3117,11 @@ void PointCloudGLWidget::drawSelectionInfoOverlay() 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) + 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)); @@ -2599,13 +3133,13 @@ void PointCloudGLWidget::drawSelectionInfoOverlay() 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)); + 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); @@ -2623,58 +3157,58 @@ void PointCloudGLWidget::drawSelectionInfoOverlay() } 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; - } + 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) { - // 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) { + 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()); @@ -2768,29 +3302,29 @@ void PointCloudGLWidget::mouseDoubleClickEvent(QMouseEvent* event) void PointCloudGLWidget::mouseMoveEvent(QMouseEvent* event) { QPoint delta = event->pos() - m_lastMousePos; - 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_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()); @@ -2832,14 +3366,14 @@ void PointCloudGLWidget::wheelEvent(QWheelEvent* event) update(); } -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(); +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); } @@ -2922,6 +3456,7 @@ void PointCloudGLWidget::replaceFirstCloud(const PointCloudXYZ& cloud, const QSt data.vertices.push_back(pt.x); data.vertices.push_back(pt.y); data.vertices.push_back(pt.z); + expandCloudBounds(data, pt.x, pt.y, pt.z); // 保存原始索引 data.originalIndices.push_back(static_cast(i)); diff --git a/CloudViewApp/Inc/CloudViewMainWindow.h b/CloudViewApp/Inc/CloudViewMainWindow.h index d637f4c..c1f9f44 100644 --- a/CloudViewApp/Inc/CloudViewMainWindow.h +++ b/CloudViewApp/Inc/CloudViewMainWindow.h @@ -83,9 +83,10 @@ private: QString fileName; }; - void buildStartupSequence(const StartupFiles& files); - bool loadStartupItem(const StartupItem& item); - bool loadCurrentStartupSequenceItem(); + void buildStartupSequence(const StartupFiles& files); + bool loadStartupItem(const StartupItem& item); + bool loadCurrentStartupSequenceItem(); + void updateZoomButtonGeometry(); private slots: /** diff --git a/CloudViewApp/Src/CloudViewMainWindow.cpp b/CloudViewApp/Src/CloudViewMainWindow.cpp index de71394..9d80ef4 100644 --- a/CloudViewApp/Src/CloudViewMainWindow.cpp +++ b/CloudViewApp/Src/CloudViewMainWindow.cpp @@ -1245,24 +1245,40 @@ void CloudViewMainWindow::setPointCloudRange(const PointCloudXYZRange& range) } } -bool CloudViewMainWindow::eventFilter(QObject* obj, QEvent* event) -{ - if (obj == m_glWidget && event->type() == QEvent::Resize && - m_btnZoomIn && m_btnZoomOut) { - const auto* resizeEvent = static_cast(event); - const int width = resizeEvent->size().width(); - const int height = resizeEvent->size().height(); - const int buttonWidth = m_btnZoomIn->width(); - const int buttonHeight = m_btnZoomIn->height(); - const int margin = 8; - const int gap = 4; - const int x = width - buttonWidth - margin; - const int y = qMax(0, (height - buttonHeight * 2 - gap) / 2); - m_btnZoomIn->move(x, y); - m_btnZoomOut->move(x, y + buttonHeight + gap); - } - return QMainWindow::eventFilter(obj, event); -} +bool CloudViewMainWindow::eventFilter(QObject* obj, QEvent* event) +{ + if (obj == m_glWidget && event->type() == QEvent::Resize && + m_btnZoomIn && m_btnZoomOut) { + updateZoomButtonGeometry(); + } + return QMainWindow::eventFilter(obj, event); +} + +void CloudViewMainWindow::updateZoomButtonGeometry() +{ + if (!m_glWidget || !m_btnZoomIn || !m_btnZoomOut) + { + return; + } + + const int widgetWidth = m_glWidget->width(); + const int widgetHeight = m_glWidget->height(); + if (widgetWidth <= 0 || widgetHeight <= 0) + { + return; + } + + const int buttonWidth = m_btnZoomIn->width(); + const int buttonHeight = m_btnZoomIn->height(); + const int margin = 8; + const int gap = 4; + const int x = qMax(0, widgetWidth - buttonWidth - margin); + const int y = qMax(0, (widgetHeight - buttonHeight * 2 - gap) / 2); + m_btnZoomIn->move(x, y); + m_btnZoomOut->move(x, y + buttonHeight + gap); + m_btnZoomIn->raise(); + m_btnZoomOut->raise(); +} void CloudViewMainWindow::closeEvent(QCloseEvent* event) { @@ -1417,11 +1433,13 @@ QWidget* CloudViewMainWindow::createViewerArea() m_btnZoomOut->setAutoRepeat(true); m_btnZoomOut->setAutoRepeatDelay(300); m_btnZoomOut->setAutoRepeatInterval(50); - connect(m_btnZoomOut, &QToolButton::clicked, m_glWidget, &PointCloudGLWidget::zoomOut); - - m_glWidget->installEventFilter(this); - - // 连接信号 + connect(m_btnZoomOut, &QToolButton::clicked, m_glWidget, &PointCloudGLWidget::zoomOut); + + m_glWidget->installEventFilter(this); + // 初始创建时可能没有收到 resize 事件,立即将按钮放到视口右侧居中。 + updateZoomButtonGeometry(); + + // 连接信号 connect(m_glWidget, &PointCloudGLWidget::pointSelected, this, &CloudViewMainWindow::onPointSelected); connect(m_glWidget, &PointCloudGLWidget::twoPointsSelected, this, &CloudViewMainWindow::onTwoPointsSelected); connect(m_glWidget, &PointCloudGLWidget::lineSelected, this, &CloudViewMainWindow::onLineSelected);