From 76b63103af2c25833c05504058a4f559166d32f6 Mon Sep 17 00:00:00 2001 From: YangSn Date: Sat, 19 Sep 2026 09:54:22 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E5=9B=BE=E5=83=8F=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=A7=A6=E6=91=B8=E5=B1=8F=E4=B8=8B=E6=94=BE?= =?UTF-8?q?=E5=A4=A7=E7=BC=A9=E5=B0=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CloudView3D/Src/PointCloudGLWidget.cpp | 170 ++++++++++++++++++------- ImageView/Inc/ImageView.h | 9 ++ ImageView/Src/ImageView.cpp | 119 ++++++++++++++++- 3 files changed, 249 insertions(+), 49 deletions(-) diff --git a/CloudView3D/Src/PointCloudGLWidget.cpp b/CloudView3D/Src/PointCloudGLWidget.cpp index 3591efa..82dbd4e 100644 --- a/CloudView3D/Src/PointCloudGLWidget.cpp +++ b/CloudView3D/Src/PointCloudGLWidget.cpp @@ -620,6 +620,11 @@ void PointCloudGLWidget::drawPrimitives(const QMatrix4x4& mvp, glUniformMatrix4fv(m_mvpLocation, 1, GL_FALSE, mvp.constData()); glUniform1f(m_pointSizeLocation, pointSize); glUniform1f(m_alphaLocation, alpha); + if (!m_dynamicVbo.isCreated()) + { + m_dynamicVbo.create(); + } + m_dynamicVbo.setUsagePattern(QOpenGLBuffer::DynamicDraw); if (depthTest) { glEnable(GL_DEPTH_TEST); } else { @@ -2889,6 +2894,7 @@ void PointCloudGLWidget::clearBasicShapeSegments() void PointCloudGLWidget::addPosePoints(const QVector& poses) { m_posePoints.append(poses); + computeBoundingBox(); update(); } @@ -2993,66 +2999,138 @@ void PointCloudGLWidget::drawLineSegmentList(const QVector& segment void PointCloudGLWidget::drawPosePoints() { - if (m_posePoints.isEmpty()) { + if (m_posePoints.isEmpty()) + { return; } - glLineWidth(2.0f); + // 从视图矩阵取相机右/上方向,把轴线拉成面向屏幕的窄带,避免 GLES2 线宽被钳成 1 像素。 + const QMatrix4x4 invView = m_view.inverted(); + const QVector3D camRight = invView.column(0).toVector3D().normalized(); + const QVector3D camUp = invView.column(1).toVector3D().normalized(); + const float pixelWorld = m_distance * + std::tan(qDegreesToRadians(22.5f)) / + static_cast(qMax(1, height() / 2)); - for (const auto& pose : m_posePoints) { - // 移动到姿态点位置,并按欧拉角顺序旋转(QMatrix4x4::rotate 与 glRotatef 同为右乘) - QMatrix4x4 modelView; - modelView.setToIdentity(); - modelView.translate(pose.x, pose.y, pose.z); - switch (m_eulerRotationOrder) { - case EulerRotationOrder::ZYX: // Yaw-Pitch-Roll(最常用) - modelView.rotate(pose.rx, 1, 0, 0); - modelView.rotate(pose.ry, 0, 1, 0); - modelView.rotate(pose.rz, 0, 0, 1); + auto applyEulerRotation = [this](QMatrix4x4& poseLocal, const PosePoint& pose) + { + switch (m_eulerRotationOrder) + { + case EulerRotationOrder::ZYX: + poseLocal.rotate(pose.rx, 1, 0, 0); + poseLocal.rotate(pose.ry, 0, 1, 0); + poseLocal.rotate(pose.rz, 0, 0, 1); break; - case EulerRotationOrder::XYZ: // Roll-Pitch-Yaw - modelView.rotate(pose.rz, 0, 0, 1); - modelView.rotate(pose.ry, 0, 1, 0); - modelView.rotate(pose.rx, 1, 0, 0); + case EulerRotationOrder::XYZ: + poseLocal.rotate(pose.rz, 0, 0, 1); + poseLocal.rotate(pose.ry, 0, 1, 0); + poseLocal.rotate(pose.rx, 1, 0, 0); break; - case EulerRotationOrder::ZXY: // Yaw-Roll-Pitch - modelView.rotate(pose.ry, 0, 1, 0); - modelView.rotate(pose.rx, 1, 0, 0); - modelView.rotate(pose.rz, 0, 0, 1); + case EulerRotationOrder::ZXY: + poseLocal.rotate(pose.ry, 0, 1, 0); + poseLocal.rotate(pose.rx, 1, 0, 0); + poseLocal.rotate(pose.rz, 0, 0, 1); break; - case EulerRotationOrder::YXZ: // Pitch-Roll-Yaw - modelView.rotate(pose.rz, 0, 0, 1); - modelView.rotate(pose.rx, 1, 0, 0); - modelView.rotate(pose.ry, 0, 1, 0); + case EulerRotationOrder::YXZ: + poseLocal.rotate(pose.rz, 0, 0, 1); + poseLocal.rotate(pose.rx, 1, 0, 0); + poseLocal.rotate(pose.ry, 0, 1, 0); break; - case EulerRotationOrder::XZY: // Roll-Yaw-Pitch - modelView.rotate(pose.ry, 0, 1, 0); - modelView.rotate(pose.rz, 0, 0, 1); - modelView.rotate(pose.rx, 1, 0, 0); + case EulerRotationOrder::XZY: + poseLocal.rotate(pose.ry, 0, 1, 0); + poseLocal.rotate(pose.rz, 0, 0, 1); + poseLocal.rotate(pose.rx, 1, 0, 0); break; - case EulerRotationOrder::YZX: // Pitch-Yaw-Roll - modelView.rotate(pose.rx, 1, 0, 0); - modelView.rotate(pose.rz, 0, 0, 1); - modelView.rotate(pose.ry, 0, 1, 0); + case EulerRotationOrder::YZX: + poseLocal.rotate(pose.rx, 1, 0, 0); + poseLocal.rotate(pose.rz, 0, 0, 1); + poseLocal.rotate(pose.ry, 0, 1, 0); break; } + }; - const QMatrix4x4 mvp = m_projection * modelView; + auto appendWorld = [](QVector& dst, const QVector3D& p) + { + dst.append(p.x()); + dst.append(p.y()); + dst.append(p.z()); + }; - // 绘制坐标系(右手坐标系:X红右,Y绿上,Z蓝前) - const float positions[] = { - 0.0f, 0.0f, 0.0f, pose.scale, 0.0f, 0.0f, // X轴 - 红色 - 0.0f, 0.0f, 0.0f, 0.0f, pose.scale, 0.0f, // Y轴 - 绿色 - 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, pose.scale // Z轴 - 蓝色 + auto appendAxisBand = [&](QVector& positions, + QVector& colors, + const QVector3D& start, + const QVector3D& end, + const QVector3D& color, + float halfWidth) + { + QVector3D side = QVector3D::crossProduct(end - start, camUp); + if (side.lengthSquared() < 1.0e-12f) + { + side = QVector3D::crossProduct(end - start, camRight); + } + if (!side.isNull()) + { + side.normalize(); + side *= halfWidth; + } + const QVector3D s0 = start - side; + const QVector3D s1 = start + side; + const QVector3D e0 = end - side; + const QVector3D e1 = end + side; + const QVector3D quad[6] = {s0, e0, e1, s0, e1, s1}; + for (const QVector3D& p : quad) + { + appendWorld(positions, p); + appendWorld(colors, color); + } + }; + + for (const auto& pose : m_posePoints) + { + QMatrix4x4 poseLocal; + poseLocal.setToIdentity(); + poseLocal.translate(pose.x, pose.y, pose.z); + applyEulerRotation(poseLocal, pose); + + const float axisLength = qMax(1.0f, pose.scale); + const QVector3D origin = poseLocal.map(QVector3D(0.0f, 0.0f, 0.0f)); + const QVector3D xEnd = poseLocal.map(QVector3D(axisLength, 0.0f, 0.0f)); + const QVector3D yEnd = poseLocal.map(QVector3D(0.0f, axisLength, 0.0f)); + const QVector3D zEnd = poseLocal.map(QVector3D(0.0f, 0.0f, axisLength)); + const float halfWidth = qMax(axisLength * 0.02f, pixelWorld * 2.0f); + + QVector bandPositions; + QVector bandColors; + bandPositions.reserve(54); + bandColors.reserve(54); + appendAxisBand(bandPositions, bandColors, origin, xEnd, QVector3D(1.0f, 0.0f, 0.0f), halfWidth); + appendAxisBand(bandPositions, bandColors, origin, yEnd, QVector3D(0.0f, 1.0f, 0.0f), halfWidth); + appendAxisBand(bandPositions, bandColors, origin, zEnd, QVector3D(0.0f, 0.0f, 1.0f), halfWidth); + + // 关闭深度测试,避免姿态轴被周围点云挡住。 + drawPrimitives(m_mvp, + bandPositions.constData(), + bandPositions.size() / 3, + GL_TRIANGLES, + bandColors.constData(), + QVector3D(1.0f, 1.0f, 1.0f), + 1.0f, + 1.0f, + false); + + const float tipPositions[] = { + origin.x(), origin.y(), origin.z(), + xEnd.x(), xEnd.y(), xEnd.z(), + yEnd.x(), yEnd.y(), yEnd.z(), + zEnd.x(), zEnd.y(), zEnd.z() }; - const float colors[] = { - 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, - 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, - 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f + const float tipColors[] = { + 1.0f, 1.0f, 1.0f, + 1.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 1.0f }; - drawPrimitives(mvp, positions, 6, GL_LINES, colors, - QVector3D(1.0f, 1.0f, 1.0f), 1.0f, 1.0f, true); + drawPrimitives(m_mvp, tipPositions, 4, GL_POINTS, tipColors, + QVector3D(1.0f, 1.0f, 1.0f), 10.0f, 1.0f, false); } - - glLineWidth(1.0f); } diff --git a/ImageView/Inc/ImageView.h b/ImageView/Inc/ImageView.h index a9b81b2..db48093 100644 --- a/ImageView/Inc/ImageView.h +++ b/ImageView/Inc/ImageView.h @@ -6,9 +6,12 @@ #include class QLabel; +class QEvent; +class QGestureEvent; class QGraphicsPixmapItem; class QGraphicsScene; class QMouseEvent; +class QPinchGesture; class QWheelEvent; // 可缩放、可平移的图片显示控件。 @@ -48,6 +51,7 @@ signals: void zoomRatioChanged(double ratio); protected: + bool viewportEvent(QEvent* event) override; void resizeEvent(QResizeEvent* event) override; void wheelEvent(QWheelEvent* event) override; void mousePressEvent(QMouseEvent* event) override; @@ -56,8 +60,11 @@ protected: void mouseDoubleClickEvent(QMouseEvent* event) override; private: + bool handleGestureEvent(QGestureEvent* event); + void handlePinchGesture(QPinchGesture* gesture); void updateZoomLabel(); void updateZoomRatio(double ratio, bool keepCenter); + void updateZoomRatioAt(double ratio, const QPoint& viewportAnchor); void repositionZoomLabel(); QGraphicsScene* m_scene; @@ -69,6 +76,8 @@ private: double m_maximumZoomRatio; Qt::Alignment m_imageAlignment; bool m_panning; + bool m_pinching; bool m_needsInitialFit; + double m_pinchStartZoomRatio; QPoint m_lastMousePosition; }; diff --git a/ImageView/Src/ImageView.cpp b/ImageView/Src/ImageView.cpp index 4d53245..6ea9568 100644 --- a/ImageView/Src/ImageView.cpp +++ b/ImageView/Src/ImageView.cpp @@ -1,19 +1,24 @@ #include "ImageView.h" +#include +#include #include #include #include #include +#include #include #include #include #include #include -namespace { +namespace +{ constexpr double kDefaultMinimumZoom = 0.02; constexpr double kDefaultMaximumZoom = 20.0; constexpr double kZoomStep = 1.25; +const QColor kDefaultBackgroundColor(37, 38, 42); } ImageView::ImageView(QWidget* parent) @@ -26,10 +31,12 @@ ImageView::ImageView(QWidget* parent) m_maximumZoomRatio(kDefaultMaximumZoom), m_imageAlignment(Qt::AlignCenter), m_panning(false), - m_needsInitialFit(false) + m_pinching(false), + m_needsInitialFit(false), + m_pinchStartZoomRatio(1.0) { setScene(m_scene); - setBackgroundBrush(QColor(35, 35, 35)); + setBackgroundBrush(kDefaultBackgroundColor); setFrameShape(QFrame::NoFrame); setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); @@ -39,6 +46,10 @@ ImageView::ImageView(QWidget* parent) setDragMode(QGraphicsView::NoDrag); setCursor(Qt::OpenHandCursor); + // 手势由 QGraphicsView 的视口接收,支持触摸屏双指捏合缩放。 + viewport()->setAttribute(Qt::WA_AcceptTouchEvents, true); + viewport()->grabGesture(Qt::PinchGesture); + m_zoomLabel->setObjectName(QStringLiteral("imageViewZoomLabel")); m_zoomLabel->setAlignment(Qt::AlignCenter); m_zoomLabel->setMinimumWidth(58); @@ -197,6 +208,65 @@ Qt::Alignment ImageView::imageAlignment() const return m_imageAlignment; } +bool ImageView::viewportEvent(QEvent* event) +{ + if (event->type() == QEvent::Gesture) + { + return handleGestureEvent(static_cast(event)); + } + return QGraphicsView::viewportEvent(event); +} + +bool ImageView::handleGestureEvent(QGestureEvent* event) +{ + QGesture* gesture = event->gesture(Qt::PinchGesture); + if (!gesture) + { + return false; + } + + QPinchGesture* pinchGesture = static_cast(gesture); + handlePinchGesture(pinchGesture); + event->accept(pinchGesture); + return true; +} + +void ImageView::handlePinchGesture(QPinchGesture* gesture) +{ + if (!gesture) + { + return; + } + + if (gesture->state() == Qt::GestureStarted) + { + // 双指手势开始后停止单指模拟产生的鼠标拖拽。 + m_pinching = true; + m_panning = false; + m_pinchStartZoomRatio = m_zoomRatio; + setCursor(Qt::OpenHandCursor); + } + + if (hasImage() && + (gesture->state() == Qt::GestureStarted || gesture->state() == Qt::GestureUpdated)) + { + const double scaleFactor = gesture->totalScaleFactor(); + if (qIsFinite(scaleFactor) && scaleFactor > 0.0) + { + // 使用累计缩放比例,避免连续事件带来的比例漂移。 + updateZoomRatioAt(m_pinchStartZoomRatio * scaleFactor, + gesture->centerPoint().toPoint()); + } + } + + if (gesture->state() == Qt::GestureFinished || + gesture->state() == Qt::GestureCanceled) + { + m_pinching = false; + setCursor(Qt::OpenHandCursor); + } +} + void ImageView::resizeEvent(QResizeEvent* event) { QGraphicsView::resizeEvent(event); @@ -221,6 +291,11 @@ void ImageView::wheelEvent(QWheelEvent* event) void ImageView::mousePressEvent(QMouseEvent* event) { + if (m_pinching) + { + event->accept(); + return; + } if (event->button() == Qt::LeftButton && hasImage()) { m_panning = true; m_lastMousePosition = event->pos(); @@ -233,6 +308,11 @@ void ImageView::mousePressEvent(QMouseEvent* event) void ImageView::mouseMoveEvent(QMouseEvent* event) { + if (m_pinching) + { + event->accept(); + return; + } if (m_panning) { const QPoint delta = event->pos() - m_lastMousePosition; m_lastMousePosition = event->pos(); @@ -246,6 +326,11 @@ void ImageView::mouseMoveEvent(QMouseEvent* event) void ImageView::mouseReleaseEvent(QMouseEvent* event) { + if (m_pinching) + { + event->accept(); + return; + } if (event->button() == Qt::LeftButton && m_panning) { m_panning = false; setCursor(Qt::OpenHandCursor); @@ -300,6 +385,34 @@ void ImageView::updateZoomRatio(double ratio, bool keepCenter) emit zoomRatioChanged(m_zoomRatio); } +void ImageView::updateZoomRatioAt(double ratio, const QPoint& viewportAnchor) +{ + if (!hasImage()) + { + return; + } + + ratio = qBound(m_minimumZoomRatio, ratio, m_maximumZoomRatio); + if (qFuzzyCompare(ratio, m_zoomRatio) && !transform().isIdentity()) + { + return; + } + + // 缩放前后保持双指中心对应同一个图像位置。 + const QPointF sceneAnchor = mapToScene(viewportAnchor); + resetTransform(); + scale(ratio, ratio); + m_zoomRatio = ratio; + const QPointF mappedAnchor = mapFromScene(sceneAnchor); + const QPointF viewportDelta = mappedAnchor - viewportAnchor; + horizontalScrollBar()->setValue( + horizontalScrollBar()->value() + qRound(viewportDelta.x())); + verticalScrollBar()->setValue( + verticalScrollBar()->value() + qRound(viewportDelta.y())); + updateZoomLabel(); + emit zoomRatioChanged(m_zoomRatio); +} + void ImageView::repositionZoomLabel() { if (!m_zoomLabel) {