3764 lines
128 KiB
C++
3764 lines
128 KiB
C++
#include "PointCloudGLWidget.h"
|
||
#include <QColor>
|
||
#include <QDebug>
|
||
#include <QtMath>
|
||
#include <QDialog>
|
||
#include <QDialogButtonBox>
|
||
#include <QDoubleSpinBox>
|
||
#include <QFormLayout>
|
||
#include <QGestureEvent>
|
||
#include <QHBoxLayout>
|
||
#include <QFontMetrics>
|
||
#include <QOpenGLContext>
|
||
#include <QPainter>
|
||
#include <QPinchGesture>
|
||
#include <QPushButton>
|
||
#include <QResizeEvent>
|
||
#include <QStringList>
|
||
#include <QTimer>
|
||
#include <QToolButton>
|
||
#include <QVBoxLayout>
|
||
#include <algorithm>
|
||
#include <array>
|
||
#include <cfloat>
|
||
#include <cmath>
|
||
#include <iterator>
|
||
#include <map>
|
||
#include <utility>
|
||
|
||
#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
|
||
#include <windows.h>
|
||
#include <GL/gl.h>
|
||
#include <GL/glu.h>
|
||
#else
|
||
#include <GL/gl.h>
|
||
#include <GL/glu.h>
|
||
#endif
|
||
|
||
#ifndef GL_PROGRAM_POINT_SIZE
|
||
#define GL_PROGRAM_POINT_SIZE 0x8642
|
||
#endif
|
||
|
||
PointCloudGLWidget::PointCloudGLWidget(QWidget* parent)
|
||
: PointCloudGLWidget(parent, true)
|
||
{
|
||
}
|
||
|
||
namespace
|
||
{
|
||
constexpr float kDefaultPseudoColorPeriod = 500.0f;
|
||
constexpr int kPseudoColorHueCount = 360;
|
||
|
||
// 将角度归一化到 [-180, 180],避免往同一方向旋转时无限累加
|
||
float NormalizeAngleDegrees(float angle)
|
||
{
|
||
float wrapped = std::fmod(angle, 360.0f);
|
||
if (wrapped > 180.0f) {
|
||
wrapped -= 360.0f;
|
||
} else if (wrapped < -180.0f) {
|
||
wrapped += 360.0f;
|
||
}
|
||
return wrapped;
|
||
}
|
||
|
||
QVector3D clampPseudoColor(const QVector3D& color)
|
||
{
|
||
const auto clampComponent = [](float component)
|
||
{
|
||
return std::isfinite(component) ? qBound(0.0f, component, 1.0f) : 0.0f;
|
||
};
|
||
return QVector3D(clampComponent(color.x()),
|
||
clampComponent(color.y()),
|
||
clampComponent(color.z()));
|
||
}
|
||
|
||
// 颜色选择器色相条(S/V 取最大)的 360 色查找表。
|
||
const std::array<QVector3D, kPseudoColorHueCount>& defaultPseudoColorLut()
|
||
{
|
||
static const std::array<QVector3D, kPseudoColorHueCount> lut = []()
|
||
{
|
||
std::array<QVector3D, kPseudoColorHueCount> colors{};
|
||
for (int hue = 0; hue < kPseudoColorHueCount; ++hue)
|
||
{
|
||
const QColor color = QColor::fromHsv(hue, 255, 255);
|
||
colors[static_cast<size_t>(hue)] = QVector3D(color.redF(),
|
||
color.greenF(),
|
||
color.blueF());
|
||
}
|
||
return colors;
|
||
}();
|
||
return lut;
|
||
}
|
||
|
||
QVector3D defaultHuePseudoColor(float t)
|
||
{
|
||
const float clamped = qBound(0.0f, t, 1.0f);
|
||
// 与颜色选择器色相条一致:红 → 品红 → 蓝 → 青 → 绿 → 黄 → 红。
|
||
int hue = static_cast<int>(std::floor(360.0f * (1.0f - clamped))) % kPseudoColorHueCount;
|
||
if (hue < 0)
|
||
{
|
||
hue += kPseudoColorHueCount;
|
||
}
|
||
return defaultPseudoColorLut()[static_cast<size_t>(hue)];
|
||
}
|
||
}
|
||
|
||
PointCloudGLWidget::PointCloudGLWidget(QWidget* parent, bool showControlButtons)
|
||
: QOpenGLWidget(parent)
|
||
, m_shader(nullptr)
|
||
, m_shaderReady(false)
|
||
, m_mvpLocation(0)
|
||
, m_positionLocation(0)
|
||
, m_colorLocation(0)
|
||
, m_pointSizeLocation(0)
|
||
, m_alphaLocation(0)
|
||
, m_useVertexColorLocation(0)
|
||
, m_uniformColorLocation(0)
|
||
, m_distance(100.0f)
|
||
, m_pinchStartDistance(100.0f)
|
||
, m_rotationX(0.0f)
|
||
, m_rotationY(0.0f)
|
||
, m_rotationZ(0.0f)
|
||
, m_rotation(QQuaternion()) // 初始化为单位四元数
|
||
, m_center(0, 0, 0)
|
||
, m_pan(0, 0, 0)
|
||
, m_minBound(-50, -50, -50)
|
||
, m_maxBound(50, 50, 50)
|
||
, m_leftButtonPressed(false)
|
||
, m_rightButtonPressed(false)
|
||
, m_middleButtonPressed(false)
|
||
, m_currentColor(PointCloudColor::White)
|
||
, m_pointSize(1.0f)
|
||
// 纯 XYZ 点云默认启用参考色环;带 RGB 的点云仍使用原始颜色。
|
||
, m_pseudoColorEnabled(true)
|
||
, m_pseudoColorPeriod(kDefaultPseudoColorPeriod)
|
||
, m_pseudoColorStartColor(0.0f, 0.0f, 1.0f)
|
||
, m_pseudoColorEndColor(1.0f, 0.0f, 0.0f)
|
||
, m_pseudoColorDefaultPalette(true)
|
||
, m_lineSelectMode(LineSelectMode::Vertical)
|
||
, m_eulerRotationOrder(EulerRotationOrder::ZYX)
|
||
, m_measureDistanceEnabled(false)
|
||
, m_hasListHighlightPoint(false)
|
||
, m_colorIndex(0)
|
||
, m_showControlButtons(showControlButtons)
|
||
, m_controlPanel(nullptr)
|
||
, m_viewToolbar(nullptr)
|
||
, 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);
|
||
setAttribute(Qt::WA_AcceptTouchEvents, true);
|
||
grabGesture(Qt::PinchGesture);
|
||
|
||
// 确保 m_model 是单位矩阵
|
||
m_model.setToIdentity();
|
||
|
||
// 默认按固定 Z 周期走颜色选择器色相条。
|
||
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)
|
||
{
|
||
if (event->type() == QEvent::Gesture) {
|
||
auto* gestureEvent = static_cast<QGestureEvent*>(event);
|
||
if (auto* pinch = static_cast<QPinchGesture*>(
|
||
gestureEvent->gesture(Qt::PinchGesture))) {
|
||
const bool handled = handlePinchGesture(pinch);
|
||
if (handled) {
|
||
gestureEvent->accept(pinch);
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
return QOpenGLWidget::event(event);
|
||
}
|
||
|
||
bool PointCloudGLWidget::handlePinchGesture(QPinchGesture* gesture)
|
||
{
|
||
if (!gesture) {
|
||
return false;
|
||
}
|
||
|
||
if (gesture->state() == Qt::GestureStarted) {
|
||
m_pinchStartDistance = m_distance;
|
||
}
|
||
|
||
const qreal scale = gesture->totalScaleFactor();
|
||
if (scale <= 0.0 || !std::isfinite(static_cast<double>(scale))) {
|
||
return false;
|
||
}
|
||
|
||
m_distance = qBound(minimumCameraDistance(),
|
||
m_pinchStartDistance / static_cast<float>(scale),
|
||
1.0e7f);
|
||
update();
|
||
return true;
|
||
}
|
||
|
||
PointCloudGLWidget::~PointCloudGLWidget()
|
||
{
|
||
// 在 GL 上下文中释放所有 VBO
|
||
makeCurrent();
|
||
for (auto& cloudData : m_pointClouds) {
|
||
releaseVBO(cloudData);
|
||
}
|
||
if (m_shader) {
|
||
delete m_shader;
|
||
m_shader = nullptr;
|
||
}
|
||
doneCurrent();
|
||
}
|
||
|
||
void PointCloudGLWidget::uploadToVBO(PointCloudData& data)
|
||
{
|
||
// 先释放旧的 VBO
|
||
releaseVBO(data);
|
||
|
||
if (data.vertices.empty()) {
|
||
return;
|
||
}
|
||
|
||
// 创建并上传顶点 VBO
|
||
data.vertexBuffer.create();
|
||
data.vertexBuffer.bind();
|
||
data.vertexBuffer.setUsagePattern(QOpenGLBuffer::StaticDraw);
|
||
data.vertexBuffer.allocate(data.vertices.data(),
|
||
static_cast<int>(data.vertices.size() * sizeof(float)));
|
||
data.vertexBuffer.release();
|
||
|
||
// 如果有颜色数据,创建并上传颜色 VBO
|
||
if ((data.hasColor || data.pseudoColor) && !data.colors.empty()) {
|
||
data.colorBuffer.create();
|
||
data.colorBuffer.bind();
|
||
data.colorBuffer.setUsagePattern(QOpenGLBuffer::StaticDraw);
|
||
data.colorBuffer.allocate(data.colors.data(),
|
||
static_cast<int>(data.colors.size() * sizeof(float)));
|
||
data.colorBuffer.release();
|
||
}
|
||
|
||
data.vboCreated = true;
|
||
}
|
||
|
||
void PointCloudGLWidget::releaseVBO(PointCloudData& data)
|
||
{
|
||
if (!data.vboCreated) {
|
||
return;
|
||
}
|
||
|
||
if (data.vertexBuffer.isCreated()) {
|
||
data.vertexBuffer.destroy();
|
||
}
|
||
if (data.colorBuffer.isCreated()) {
|
||
data.colorBuffer.destroy();
|
||
}
|
||
|
||
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;
|
||
const QString toolButtonStyle =
|
||
"QToolButton {"
|
||
" background-color: rgba(20, 36, 58, 200);"
|
||
" color: #d8e8f4;"
|
||
" border: 1px solid #2b4f77;"
|
||
" border-radius: 4px;"
|
||
" font-size: 13px;"
|
||
"}"
|
||
"QToolButton:hover { background-color: #204a72; }"
|
||
"QToolButton:pressed { background-color: #10243a; }";
|
||
|
||
// ── 左侧视图方向工具栏(纵向排布) ──
|
||
struct ViewPreset {
|
||
const char* label;
|
||
const char* tooltip;
|
||
float rx, ry, rz;
|
||
};
|
||
// 坐标系定义:X向右,Y向下,Z朝后(与 CloudView 一致)
|
||
const ViewPreset presets[] = {
|
||
{"前", "正视 (XY面)", 180.0f, 0.0f, 0.0f},
|
||
{"后", "后视", 180.0f, 180.0f, 0.0f},
|
||
{"左", "左视 (YZ面)", 180.0f, 90.0f, 0.0f},
|
||
{"右", "右视", 180.0f, -90.0f, 0.0f},
|
||
{"上", "俯视 (XZ面)", 90.0f, 0.0f, 0.0f},
|
||
{"下", "仰视", -90.0f, 0.0f, 0.0f},
|
||
{"机", "机械臂", -90.0f, 90.0f, 0.0f},
|
||
};
|
||
|
||
m_viewToolbar = new QWidget(this);
|
||
m_viewToolbar->setStyleSheet("background: transparent;");
|
||
auto* toolbarLayout = new QVBoxLayout(m_viewToolbar);
|
||
toolbarLayout->setContentsMargins(4, 4, 4, 4);
|
||
toolbarLayout->setSpacing(4);
|
||
// 工具栏尺寸跟随内容,便于垂直居中定位
|
||
toolbarLayout->setSizeConstraint(QLayout::SetFixedSize);
|
||
|
||
for (const ViewPreset& preset : presets) {
|
||
auto* button = new QToolButton(m_viewToolbar);
|
||
button->setText(QString::fromUtf8(preset.label));
|
||
button->setToolTip(QString::fromUtf8(preset.tooltip));
|
||
button->setFixedSize(buttonSize, buttonSize);
|
||
button->setStyleSheet(toolButtonStyle);
|
||
const float rx = preset.rx;
|
||
const float ry = preset.ry;
|
||
const float rz = preset.rz;
|
||
connect(button, &QToolButton::clicked, this, [this, rx, ry, rz]() {
|
||
setViewAngles(rx, ry, rz);
|
||
});
|
||
m_viewButtons.append(button);
|
||
toolbarLayout->addWidget(button);
|
||
}
|
||
toolbarLayout->addStretch(1);
|
||
m_viewToolbar->setFixedWidth(buttonSize + 12);
|
||
m_viewToolbar->setVisible(m_showControlButtons);
|
||
|
||
// ── 左下角旋转角度显示按钮(点击弹窗输入角度) ──
|
||
m_viewAngleButton = new QPushButton(this);
|
||
m_viewAngleButton->setStyleSheet(
|
||
"QPushButton {"
|
||
" background-color: rgba(20, 36, 58, 200);"
|
||
" color: #d8e8f4;"
|
||
" border: 1px solid #2b4f77;"
|
||
" border-radius: 4px;"
|
||
" padding: 4px 8px;"
|
||
" text-align: left;"
|
||
"}"
|
||
"QPushButton:hover { background-color: #204a72; }");
|
||
connect(m_viewAngleButton, &QPushButton::clicked,
|
||
this, &PointCloudGLWidget::showViewAngleDialog);
|
||
// 显式指定字体,保证 fontMetrics 度量与实际渲染一致
|
||
QFont angleFont = m_viewAngleButton->font();
|
||
angleFont.setPixelSize(12);
|
||
m_viewAngleButton->setFont(angleFont);
|
||
// 竖向三行显示 RX/RY/RZ:宽度按最长一行算,高度按三行算
|
||
const QString longestAngleLine = QStringLiteral("RX:-360.0");
|
||
const int angleTextWidth =
|
||
m_viewAngleButton->fontMetrics().horizontalAdvance(longestAngleLine);
|
||
const int angleLineHeight = m_viewAngleButton->fontMetrics().lineSpacing();
|
||
m_viewAngleButton->setFixedSize(angleTextWidth + 24, angleLineHeight * 3 + 16);
|
||
m_viewAngleButton->setVisible(m_showControlButtons);
|
||
|
||
// ── 右侧悬浮缩放按钮(垂直居中,与 CloudView 位置一致) ──
|
||
const QString zoomButtonStyle =
|
||
"QToolButton {"
|
||
" background-color: rgba(20, 36, 58, 200);"
|
||
" color: #e8f2fb;"
|
||
" border: 1px solid #2b4f77;"
|
||
" border-radius: 4px;"
|
||
" font-size: 26px;"
|
||
" font-weight: bold;"
|
||
"}"
|
||
"QToolButton:hover { background-color: #204a72; }"
|
||
"QToolButton:pressed { background-color: #10243a; }";
|
||
const int zoomButtonSize = 28;
|
||
m_zoomInButton = new QToolButton(this);
|
||
m_zoomInButton->setText("+");
|
||
m_zoomInButton->setFixedSize(zoomButtonSize, zoomButtonSize);
|
||
m_zoomInButton->setStyleSheet(zoomButtonStyle);
|
||
m_zoomInButton->setAutoRepeat(true);
|
||
m_zoomInButton->setAutoRepeatDelay(300);
|
||
m_zoomInButton->setAutoRepeatInterval(50);
|
||
connect(m_zoomInButton, &QToolButton::clicked, this, &PointCloudGLWidget::zoomIn);
|
||
m_zoomInButton->setVisible(m_showControlButtons);
|
||
|
||
m_zoomOutButton = new QToolButton(this);
|
||
m_zoomOutButton->setText("-");
|
||
m_zoomOutButton->setFixedSize(zoomButtonSize, zoomButtonSize);
|
||
m_zoomOutButton->setStyleSheet(zoomButtonStyle);
|
||
m_zoomOutButton->setAutoRepeat(true);
|
||
m_zoomOutButton->setAutoRepeatDelay(300);
|
||
m_zoomOutButton->setAutoRepeatInterval(50);
|
||
connect(m_zoomOutButton, &QToolButton::clicked, this, &PointCloudGLWidget::zoomOut);
|
||
m_zoomOutButton->setVisible(m_showControlButtons);
|
||
|
||
updateViewAngleDisplay();
|
||
repositionOverlayControls();
|
||
}
|
||
|
||
void PointCloudGLWidget::repositionOverlayControls()
|
||
{
|
||
const int widgetWidth = width();
|
||
const int widgetHeight = height();
|
||
if (widgetWidth <= 0 || widgetHeight <= 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 左侧视图工具栏:垂直居中
|
||
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)
|
||
{
|
||
const int margin = 8;
|
||
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)
|
||
{
|
||
const int buttonWidth = m_zoomInButton->width();
|
||
const int buttonHeight = m_zoomInButton->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_zoomInButton->move(x, y);
|
||
m_zoomOutButton->move(x, y + buttonHeight + gap);
|
||
m_zoomInButton->raise();
|
||
m_zoomOutButton->raise();
|
||
}
|
||
}
|
||
|
||
|
||
void PointCloudGLWidget::updateViewAngleDisplay()
|
||
{
|
||
if (!m_viewAngleButton) {
|
||
return;
|
||
}
|
||
m_viewAngleButton->setText(QStringLiteral("RX:%1\nRY:%2\nRZ:%3")
|
||
.arg(m_rotationX, 0, 'f', 1)
|
||
.arg(m_rotationY, 0, 'f', 1)
|
||
.arg(m_rotationZ, 0, 'f', 1));
|
||
}
|
||
|
||
void PointCloudGLWidget::showViewAngleDialog()
|
||
{
|
||
QDialog dialog(this);
|
||
dialog.setWindowTitle(QStringLiteral("设置视图角度"));
|
||
dialog.setMinimumWidth(240);
|
||
|
||
auto* form = new QFormLayout(&dialog);
|
||
auto* spinX = new QDoubleSpinBox(&dialog);
|
||
auto* spinY = new QDoubleSpinBox(&dialog);
|
||
auto* spinZ = new QDoubleSpinBox(&dialog);
|
||
for (QDoubleSpinBox* spin : {spinX, spinY, spinZ}) {
|
||
spin->setRange(-180.0, 180.0);
|
||
spin->setDecimals(1);
|
||
spin->setSingleStep(1.0);
|
||
spin->setSuffix(QStringLiteral(" °"));
|
||
}
|
||
spinX->setValue(m_rotationX);
|
||
spinY->setValue(m_rotationY);
|
||
spinZ->setValue(m_rotationZ);
|
||
form->addRow(QStringLiteral("RX"), spinX);
|
||
form->addRow(QStringLiteral("RY"), spinY);
|
||
form->addRow(QStringLiteral("RZ"), spinZ);
|
||
|
||
auto* buttons = new QDialogButtonBox(
|
||
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog);
|
||
buttons->button(QDialogButtonBox::Ok)->setText(QStringLiteral("确认"));
|
||
buttons->button(QDialogButtonBox::Cancel)->setText(QStringLiteral("取消"));
|
||
connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
|
||
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||
form->addRow(buttons);
|
||
|
||
if (dialog.exec() == QDialog::Accepted) {
|
||
setViewAngles(static_cast<float>(spinX->value()),
|
||
static_cast<float>(spinY->value()),
|
||
static_cast<float>(spinZ->value()));
|
||
}
|
||
}
|
||
|
||
void PointCloudGLWidget::initializeGL()
|
||
{
|
||
initializeOpenGLFunctions();
|
||
|
||
CLOUD_LOG_INFO("[CloudView] OpenGL initialized, version: %s\n", reinterpret_cast<const char*>(glGetString(GL_VERSION)));
|
||
|
||
glClearColor(0.15f, 0.15f, 0.15f, 1.0f);
|
||
glEnable(GL_DEPTH_TEST);
|
||
// 桌面 GL(GL 3.2+)下,顶点着色器输出的 gl_PointSize 需显式开启才生效;
|
||
// GLES2 总是使用 shader 点大小,无需此开关。
|
||
if (QOpenGLContext::currentContext() &&
|
||
!QOpenGLContext::currentContext()->isOpenGLES()) {
|
||
glEnable(GL_PROGRAM_POINT_SIZE);
|
||
}
|
||
if (!m_dynamicVbo.isCreated()) {
|
||
m_dynamicVbo.create();
|
||
}
|
||
m_shaderReady = false;
|
||
ensureShader();
|
||
}
|
||
|
||
bool PointCloudGLWidget::ensureShader()
|
||
{
|
||
if (m_shaderReady) {
|
||
return m_shader != nullptr;
|
||
}
|
||
|
||
if (!m_shader) {
|
||
auto* program = new QOpenGLShaderProgram();
|
||
program->bindAttributeLocation("aPosition", 0);
|
||
program->bindAttributeLocation("aColor", 1);
|
||
|
||
if (!program->addShaderFromSourceCode(
|
||
QOpenGLShader::Vertex,
|
||
// GLSL ES 1.00 的 vertex shader 有默认 float 精度(highp),
|
||
// 但显式声明更稳妥,兼容性更好。
|
||
"precision highp float;\n"
|
||
"attribute vec3 aPosition;\n"
|
||
"attribute vec3 aColor;\n"
|
||
"uniform mat4 uMvp;\n"
|
||
"uniform float uPointSize;\n"
|
||
"uniform float uAlpha;\n"
|
||
"uniform float uUseVertexColor;\n"
|
||
"uniform vec3 uColor;\n"
|
||
"varying vec3 vColor;\n"
|
||
"varying float vAlpha;\n"
|
||
"void main() {\n"
|
||
" gl_Position = uMvp * vec4(aPosition, 1.0);\n"
|
||
" gl_PointSize = uPointSize;\n"
|
||
" vColor = (uUseVertexColor > 0.5) ? aColor : uColor;\n"
|
||
" vAlpha = uAlpha;\n"
|
||
"}\n") ||
|
||
!program->addShaderFromSourceCode(
|
||
QOpenGLShader::Fragment,
|
||
// GLSL ES 1.00 的 fragment shader 没有默认 float 精度,
|
||
// 必须显式声明,否则 Mali 等 GLES 驱动编译失败(S0032)。
|
||
"precision highp float;\n"
|
||
"varying vec3 vColor;\n"
|
||
"varying float vAlpha;\n"
|
||
"void main() {\n"
|
||
" gl_FragColor = vec4(vColor, vAlpha);\n"
|
||
"}\n")) {
|
||
delete program;
|
||
m_shaderReady = false;
|
||
return false;
|
||
}
|
||
|
||
if (!program->link()) {
|
||
delete program;
|
||
m_shaderReady = false;
|
||
return false;
|
||
}
|
||
|
||
m_shader = program;
|
||
m_positionLocation = m_shader->attributeLocation("aPosition");
|
||
m_colorLocation = m_shader->attributeLocation("aColor");
|
||
m_mvpLocation = m_shader->uniformLocation("uMvp");
|
||
m_pointSizeLocation = m_shader->uniformLocation("uPointSize");
|
||
m_alphaLocation = m_shader->uniformLocation("uAlpha");
|
||
m_useVertexColorLocation = m_shader->uniformLocation("uUseVertexColor");
|
||
m_uniformColorLocation = m_shader->uniformLocation("uColor");
|
||
}
|
||
m_shaderReady = m_shader != nullptr;
|
||
return m_shaderReady;
|
||
}
|
||
|
||
QVector3D PointCloudGLWidget::colorForIndex(int colorIndex) const
|
||
{
|
||
static const QVector3D colorTable[COLOR_COUNT] = {
|
||
QVector3D(0.75f, 0.75f, 0.75f), // 浅灰
|
||
QVector3D(0.3f, 1.0f, 0.3f), // 浅绿
|
||
QVector3D(0.3f, 0.3f, 1.0f), // 浅蓝
|
||
QVector3D(1.0f, 1.0f, 0.3f), // 黄
|
||
QVector3D(0.3f, 1.0f, 1.0f), // 青
|
||
QVector3D(1.0f, 0.3f, 1.0f), // 品红
|
||
QVector3D(1.0f, 1.0f, 1.0f) // 白
|
||
};
|
||
return colorTable[colorIndex % COLOR_COUNT];
|
||
}
|
||
|
||
void PointCloudGLWidget::drawCloud(PointCloudData& data, float pointSize)
|
||
{
|
||
if (data.vertices.empty() || !m_shader) {
|
||
return;
|
||
}
|
||
m_shader->bind();
|
||
glUniformMatrix4fv(m_mvpLocation, 1, GL_FALSE, m_mvp.constData());
|
||
glUniform1f(m_pointSizeLocation, pointSize);
|
||
glUniform1f(m_alphaLocation, 1.0f);
|
||
// 部分驱动忽略 shader 里的 gl_PointSize,固定管线点大小作为兜底。
|
||
glPointSize(pointSize);
|
||
|
||
data.vertexBuffer.bind();
|
||
glEnableVertexAttribArray(m_positionLocation);
|
||
glVertexAttribPointer(m_positionLocation, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
|
||
|
||
const bool useVertexColor = (data.hasColor || data.pseudoColor) &&
|
||
data.colorBuffer.isCreated() && !data.colors.empty();
|
||
if (useVertexColor) {
|
||
data.colorBuffer.bind();
|
||
glEnableVertexAttribArray(m_colorLocation);
|
||
glVertexAttribPointer(m_colorLocation, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
|
||
glUniform1f(m_useVertexColorLocation, 1.0f);
|
||
} else {
|
||
glDisableVertexAttribArray(m_colorLocation);
|
||
const QVector3D color = colorForIndex(data.colorIndex);
|
||
glUniform3f(m_uniformColorLocation, color.x(), color.y(), color.z());
|
||
glUniform1f(m_useVertexColorLocation, 0.0f);
|
||
}
|
||
|
||
glDrawArrays(GL_POINTS, 0, static_cast<GLsizei>(data.vertices.size() / 3));
|
||
|
||
glDisableVertexAttribArray(m_positionLocation);
|
||
if (useVertexColor) {
|
||
glDisableVertexAttribArray(m_colorLocation);
|
||
}
|
||
data.vertexBuffer.release();
|
||
data.colorBuffer.release();
|
||
}
|
||
|
||
void PointCloudGLWidget::drawPrimitives(const QMatrix4x4& mvp,
|
||
const float* positions,
|
||
int vertexCount,
|
||
GLenum mode,
|
||
const float* colors,
|
||
const QVector3D& uniformColor,
|
||
float pointSize,
|
||
float alpha,
|
||
bool depthTest)
|
||
{
|
||
if (!positions || vertexCount <= 0 || !m_shader) {
|
||
return;
|
||
}
|
||
m_shader->bind();
|
||
glUniformMatrix4fv(m_mvpLocation, 1, GL_FALSE, mvp.constData());
|
||
glUniform1f(m_pointSizeLocation, pointSize);
|
||
glUniform1f(m_alphaLocation, alpha);
|
||
if (!m_dynamicVbo.isCreated())
|
||
{
|
||
m_dynamicVbo.create();
|
||
}
|
||
m_dynamicVbo.setUsagePattern(QOpenGLBuffer::DynamicDraw);
|
||
if (depthTest) {
|
||
glEnable(GL_DEPTH_TEST);
|
||
} else {
|
||
glDisable(GL_DEPTH_TEST);
|
||
}
|
||
|
||
const GLsizeiptr positionBytes =
|
||
static_cast<GLsizeiptr>(vertexCount) * 3 * sizeof(float);
|
||
const GLsizeiptr colorBytes =
|
||
colors ? static_cast<GLsizeiptr>(vertexCount) * 3 * sizeof(float) : 0;
|
||
|
||
m_dynamicVbo.bind();
|
||
m_dynamicVbo.allocate(positionBytes + colorBytes);
|
||
m_dynamicVbo.write(0, positions, positionBytes);
|
||
if (colors) {
|
||
m_dynamicVbo.write(positionBytes, colors, colorBytes);
|
||
}
|
||
|
||
glEnableVertexAttribArray(m_positionLocation);
|
||
glVertexAttribPointer(m_positionLocation, 3, GL_FLOAT, GL_FALSE, 0, nullptr);
|
||
|
||
if (colors) {
|
||
glEnableVertexAttribArray(m_colorLocation);
|
||
glVertexAttribPointer(m_colorLocation, 3, GL_FLOAT, GL_FALSE, 0,
|
||
reinterpret_cast<void*>(positionBytes));
|
||
glUniform1f(m_useVertexColorLocation, 1.0f);
|
||
} else {
|
||
glDisableVertexAttribArray(m_colorLocation);
|
||
glUniform3f(m_uniformColorLocation,
|
||
uniformColor.x(), uniformColor.y(), uniformColor.z());
|
||
glUniform1f(m_useVertexColorLocation, 0.0f);
|
||
}
|
||
|
||
glDrawArrays(mode, 0, vertexCount);
|
||
|
||
m_dynamicVbo.release();
|
||
glDisableVertexAttribArray(m_positionLocation);
|
||
if (colors) {
|
||
glDisableVertexAttribArray(m_colorLocation);
|
||
}
|
||
glEnable(GL_DEPTH_TEST);
|
||
}
|
||
|
||
void PointCloudGLWidget::resetRenderState()
|
||
{
|
||
glDisableVertexAttribArray(m_positionLocation);
|
||
glDisableVertexAttribArray(m_colorLocation);
|
||
m_dynamicVbo.release();
|
||
if (m_shader) {
|
||
m_shader->release();
|
||
}
|
||
glEnable(GL_DEPTH_TEST);
|
||
}
|
||
|
||
void PointCloudGLWidget::resizeGL(int w, int h)
|
||
{
|
||
glViewport(0, 0, w, h);
|
||
|
||
m_projection.setToIdentity();
|
||
float aspect = static_cast<float>(w) / static_cast<float>(h > 0 ? h : 1);
|
||
// 放大裁剪范围以容纳激光雷达点云(最大可能跨越数十万 mm)
|
||
// near 取 1.0 保留 depth buffer 精度,far 放到 1e7 避免远点被裁掉
|
||
m_projection.perspective(45.0f, aspect, 1.0f, 1.0e7f);
|
||
}
|
||
|
||
void PointCloudGLWidget::resizeEvent(QResizeEvent* event)
|
||
{
|
||
QOpenGLWidget::resizeEvent(event);
|
||
// 覆盖控件依赖视口尺寸,父布局变化后必须重新计算位置。
|
||
repositionOverlayControls();
|
||
updateControlButtonGeometry();
|
||
}
|
||
|
||
void PointCloudGLWidget::paintGL()
|
||
{
|
||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||
if (!ensureShader()) {
|
||
return;
|
||
}
|
||
|
||
m_view.setToIdentity();
|
||
// 平移在相机空间中进行,使鼠标拖动方向与屏幕方向一致
|
||
m_view.translate(-m_pan.x(), -m_pan.y(), -m_distance);
|
||
|
||
// 使用四元数旋转(物体坐标系旋转)
|
||
m_view.rotate(m_rotation);
|
||
|
||
m_view.translate(-m_center);
|
||
|
||
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];
|
||
if (cloudData.vertices.empty()) {
|
||
continue;
|
||
}
|
||
|
||
// 确保 VBO 已创建(延迟上传,因为 addPointCloud 可能在 GL 上下文外调用)
|
||
if (!cloudData.vboCreated) {
|
||
uploadToVBO(cloudData);
|
||
}
|
||
|
||
drawCloud(cloudData, pointSize);
|
||
|
||
// 绘制自定义大小的点(RGBA 中 A > 1 的点)
|
||
if (cloudData.hasCustomPointSizes) {
|
||
glDepthFunc(GL_LEQUAL); // 允许在相同深度覆盖绘制
|
||
const bool cloudHasColor = (cloudData.hasColor || cloudData.pseudoColor) &&
|
||
cloudData.colorBuffer.isCreated() &&
|
||
!cloudData.colors.empty();
|
||
for (const auto& group : cloudData.customPointSizeGroups) {
|
||
QVector<float> positions;
|
||
positions.reserve(static_cast<int>(group.indices.size()) * 3);
|
||
QVector<float> colors;
|
||
if (cloudHasColor) {
|
||
colors.reserve(static_cast<int>(group.indices.size()) * 3);
|
||
}
|
||
for (size_t idx : group.indices) {
|
||
const size_t vi = idx * 3;
|
||
if (vi + 2 < cloudData.vertices.size()) {
|
||
positions.append(cloudData.vertices[vi]);
|
||
positions.append(cloudData.vertices[vi + 1]);
|
||
positions.append(cloudData.vertices[vi + 2]);
|
||
if (cloudHasColor && vi + 2 < cloudData.colors.size()) {
|
||
colors.append(cloudData.colors[vi]);
|
||
colors.append(cloudData.colors[vi + 1]);
|
||
colors.append(cloudData.colors[vi + 2]);
|
||
}
|
||
}
|
||
}
|
||
if (!positions.isEmpty()) {
|
||
drawPrimitives(m_mvp,
|
||
positions.constData(),
|
||
positions.size() / 3,
|
||
GL_POINTS,
|
||
cloudHasColor ? colors.constData() : nullptr,
|
||
colorForIndex(cloudData.colorIndex),
|
||
group.pointSize,
|
||
1.0f,
|
||
true);
|
||
}
|
||
}
|
||
glDepthFunc(GL_LESS); // 恢复默认深度测试
|
||
}
|
||
}
|
||
|
||
drawSelectedPoints();
|
||
drawMeasurementLine();
|
||
drawSelectedLine();
|
||
drawLineSegments();
|
||
drawBasicShapeSurfaces();
|
||
drawBasicShapeSegments();
|
||
drawBasicShapePoints();
|
||
drawPosePoints();
|
||
|
||
// 最后绘制坐标系指示器(覆盖在所有内容之上)
|
||
drawAxis();
|
||
|
||
// 清理 GL 状态,避免影响 QPainter 标注
|
||
resetRenderState();
|
||
|
||
// 使用 QPainter 绘制坐标轴 XYZ 标注
|
||
drawAxisLabels();
|
||
drawSelectionInfoOverlay();
|
||
}
|
||
|
||
void PointCloudGLWidget::addPointCloud(const PointCloudXYZ& cloud, const QString& name, bool resetViewAfterAdd)
|
||
{
|
||
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;
|
||
}
|
||
|
||
PointCloudData data;
|
||
data.name = name;
|
||
data.hasColor = false;
|
||
data.hasLineInfo = !cloud.lineIndices.empty();
|
||
data.colorIndex = m_colorIndex; // 分配颜色索引
|
||
m_colorIndex = (m_colorIndex + 1) % COLOR_COUNT; // 轮换到下一个颜色
|
||
data.vertices.reserve(cloud.size() * 3);
|
||
data.totalLines = 0;
|
||
data.pointsPerLine = 0;
|
||
|
||
const float EPSILON = 1e-6f;
|
||
int prevLineIdx = -1;
|
||
int ptInLineCounter = 0;
|
||
int maxPointCount = 0;
|
||
for (size_t i = 0; i < cloud.points.size(); ++i) {
|
||
// 跟踪线内索引(对所有点递增,包括被过滤的零点)
|
||
int lineIdx = -1;
|
||
if (data.hasLineInfo && i < cloud.lineIndices.size()) {
|
||
lineIdx = cloud.lineIndices[i];
|
||
}
|
||
if (lineIdx != prevLineIdx) {
|
||
ptInLineCounter = 0;
|
||
prevLineIdx = lineIdx;
|
||
}
|
||
|
||
int pointInLineIndex = (i < cloud.pointInLineIndices.size())
|
||
? cloud.pointInLineIndices[i] : -1;
|
||
if (pointInLineIndex < 0) {
|
||
pointInLineIndex = ptInLineCounter;
|
||
}
|
||
maxPointCount = qMax(maxPointCount, pointInLineIndex + 1);
|
||
|
||
const auto& pt = cloud.points[i];
|
||
if (!std::isfinite(pt.x) || !std::isfinite(pt.y) || !std::isfinite(pt.z)) {
|
||
ptInLineCounter++;
|
||
continue;
|
||
}
|
||
// 显示时过滤 (0,0,0) 点
|
||
if (std::fabs(pt.x) < EPSILON && std::fabs(pt.y) < EPSILON && std::fabs(pt.z) < EPSILON) {
|
||
ptInLineCounter++;
|
||
continue;
|
||
}
|
||
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<int>(i));
|
||
|
||
// 保存线内索引(点在所属线中的位置)
|
||
data.pointInLineIndices.push_back(pointInLineIndex);
|
||
|
||
// 线索引数组必须和顶点数组保持一一对应;未知线号用 -1 占位。
|
||
if (data.hasLineInfo) {
|
||
data.lineIndices.push_back(lineIdx);
|
||
if (lineIdx >= 0 && lineIdx + 1 > data.totalLines) {
|
||
data.totalLines = lineIdx + 1;
|
||
}
|
||
}
|
||
|
||
ptInLineCounter++;
|
||
}
|
||
|
||
// 计算每线点数(假设网格化点云)
|
||
if (data.totalLines > 0) {
|
||
data.pointsPerLine = maxPointCount;
|
||
}
|
||
|
||
// 新建纯 XYZ 点云时直接生成颜色,避免随后再次上传同一份 VBO。
|
||
rebuildPseudoColors(data, 0);
|
||
|
||
m_pointClouds.push_back(std::move(data));
|
||
// VBO 推迟到 paintGL,避免高频 add 时反复 makeCurrent。
|
||
|
||
if (!refreshDisplay)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (resetViewAfterAdd) {
|
||
computeBoundingBox();
|
||
resetView();
|
||
} else {
|
||
computeBoundingBox();
|
||
const float fittedDistance = fittedCameraDistance();
|
||
if (std::isfinite(fittedDistance)) {
|
||
m_distance = qMax(m_distance, fittedDistance);
|
||
}
|
||
}
|
||
update();
|
||
}
|
||
|
||
void PointCloudGLWidget::setControlButtonsVisible(bool visible)
|
||
{
|
||
m_showControlButtons = visible;
|
||
if (m_controlPanel) {
|
||
m_controlPanel->setVisible(visible);
|
||
}
|
||
if (m_viewToolbar) {
|
||
m_viewToolbar->setVisible(visible);
|
||
}
|
||
if (m_viewAngleButton) {
|
||
m_viewAngleButton->setVisible(visible);
|
||
}
|
||
if (m_zoomInButton) {
|
||
m_zoomInButton->setVisible(visible);
|
||
}
|
||
if (m_zoomOutButton) {
|
||
m_zoomOutButton->setVisible(visible);
|
||
}
|
||
if (visible)
|
||
{
|
||
repositionOverlayControls();
|
||
updateControlButtonGeometry();
|
||
}
|
||
}
|
||
|
||
void PointCloudGLWidget::createControlButtons()
|
||
{
|
||
m_controlPanel = new QWidget(this);
|
||
m_controlPanel->setObjectName("cloudView3DControlPanel");
|
||
m_controlPanel->setStyleSheet(
|
||
"QWidget#cloudView3DControlPanel {"
|
||
" background-color: rgba(40, 40, 40, 185);"
|
||
" border: 1px solid rgba(130, 130, 130, 150);"
|
||
" border-radius: 4px;"
|
||
"}"
|
||
"QToolButton {"
|
||
" background-color: rgba(70, 70, 70, 210);"
|
||
" color: white;"
|
||
" border: 1px solid rgba(150, 150, 150, 120);"
|
||
" border-radius: 3px;"
|
||
" font-size: 10px;"
|
||
"}"
|
||
"QToolButton:hover { background-color: rgba(100, 100, 100, 230); }"
|
||
"QToolButton:pressed { background-color: rgba(25, 25, 25, 240); }");
|
||
|
||
QVBoxLayout* panelLayout = new QVBoxLayout(m_controlPanel);
|
||
panelLayout->setContentsMargins(4, 4, 4, 4);
|
||
panelLayout->setSpacing(3);
|
||
|
||
auto makeRow = [this, panelLayout]() {
|
||
QHBoxLayout* row = new QHBoxLayout();
|
||
row->setContentsMargins(0, 0, 0, 0);
|
||
row->setSpacing(2);
|
||
panelLayout->addLayout(row);
|
||
return row;
|
||
};
|
||
|
||
auto addButton = [this](QHBoxLayout* row, const QString& text, const QString& tooltip) {
|
||
QToolButton* button = new QToolButton(m_controlPanel);
|
||
button->setText(text);
|
||
button->setToolTip(tooltip);
|
||
button->setFixedSize(26, 24);
|
||
button->setAutoRaise(false);
|
||
row->addWidget(button);
|
||
return button;
|
||
};
|
||
|
||
struct ViewPreset {
|
||
const char* text;
|
||
const char* tooltip;
|
||
float rotX;
|
||
float rotY;
|
||
float rotZ;
|
||
};
|
||
const ViewPreset presets[] = {
|
||
{"F", "正视", 180.0f, 0.0f, 0.0f},
|
||
{"B", "后视", 180.0f, 180.0f, 0.0f},
|
||
{"L", "左视", 180.0f, 90.0f, 0.0f},
|
||
{"R", "右视", 180.0f, -90.0f, 0.0f},
|
||
{"T", "俯视", 90.0f, 0.0f, 0.0f},
|
||
{"D", "仰视", -90.0f, 0.0f, 0.0f},
|
||
{"A", "机械臂视角", -90.0f, 90.0f, 0.0f}
|
||
};
|
||
|
||
QHBoxLayout* viewRow = makeRow();
|
||
for (const ViewPreset& preset : presets) {
|
||
QToolButton* button = addButton(viewRow, preset.text, preset.tooltip);
|
||
connect(button, &QToolButton::clicked, this, [this, preset]() {
|
||
setViewAngles(preset.rotX, preset.rotY, preset.rotZ);
|
||
});
|
||
}
|
||
|
||
QHBoxLayout* rotateRow = makeRow();
|
||
auto addRotationButton = [this, &addButton, rotateRow](const QString& text,
|
||
const QString& tooltip,
|
||
float deltaX,
|
||
float deltaY,
|
||
float deltaZ) {
|
||
QToolButton* button = addButton(rotateRow, text, tooltip);
|
||
connect(button, &QToolButton::clicked, this, [this, deltaX, deltaY, deltaZ]() {
|
||
setViewAngles(m_rotationX + deltaX,
|
||
m_rotationY + deltaY,
|
||
m_rotationZ + deltaZ);
|
||
});
|
||
};
|
||
addRotationButton("X-", "绕 X 轴反向旋转 5 度", -5.0f, 0.0f, 0.0f);
|
||
addRotationButton("X+", "绕 X 轴正向旋转 5 度", 5.0f, 0.0f, 0.0f);
|
||
addRotationButton("Y-", "绕 Y 轴反向旋转 5 度", 0.0f, -5.0f, 0.0f);
|
||
addRotationButton("Y+", "绕 Y 轴正向旋转 5 度", 0.0f, 5.0f, 0.0f);
|
||
addRotationButton("Z-", "绕 Z 轴反向旋转 5 度", 0.0f, 0.0f, -5.0f);
|
||
addRotationButton("Z+", "绕 Z 轴正向旋转 5 度", 0.0f, 0.0f, 5.0f);
|
||
|
||
QToolButton* resetButton = addButton(rotateRow, "0", "重置视图");
|
||
connect(resetButton, &QToolButton::clicked, this, &PointCloudGLWidget::resetView);
|
||
|
||
QHBoxLayout* zoomRow = makeRow();
|
||
m_zoomInButton = addButton(zoomRow, "+", "放大");
|
||
m_zoomOutButton = addButton(zoomRow, "-", "缩小");
|
||
m_zoomInButton->setAutoRepeat(true);
|
||
m_zoomOutButton->setAutoRepeat(true);
|
||
connect(m_zoomInButton, &QToolButton::clicked, this, &PointCloudGLWidget::zoomIn);
|
||
connect(m_zoomOutButton, &QToolButton::clicked, this, &PointCloudGLWidget::zoomOut);
|
||
zoomRow->addStretch();
|
||
|
||
m_controlPanel->adjustSize();
|
||
m_controlPanel->setVisible(m_showControlButtons);
|
||
updateControlButtonGeometry();
|
||
}
|
||
|
||
void PointCloudGLWidget::updateControlButtonGeometry()
|
||
{
|
||
if (!m_controlPanel) {
|
||
return;
|
||
}
|
||
m_controlPanel->move(8, 8);
|
||
m_controlPanel->raise();
|
||
}
|
||
|
||
void PointCloudGLWidget::addPointCloud(const PointCloudXYZRGB& cloud, const QString& name, bool resetViewAfterAdd)
|
||
{
|
||
if (cloud.empty()) {
|
||
return;
|
||
}
|
||
|
||
PointCloudData data;
|
||
data.name = name;
|
||
data.hasColor = true;
|
||
data.hasLineInfo = !cloud.lineIndices.empty();
|
||
data.colorIndex = m_colorIndex; // 即使有颜色也分配索引(备用)
|
||
m_colorIndex = (m_colorIndex + 1) % COLOR_COUNT;
|
||
data.vertices.reserve(cloud.size() * 3);
|
||
data.colors.reserve(cloud.size() * 3);
|
||
data.totalLines = 0;
|
||
data.pointsPerLine = 0;
|
||
data.hasCustomPointSizes = false;
|
||
|
||
// 用于按点大小分组的临时 map
|
||
std::map<float, std::vector<size_t>> sizeGroupMap;
|
||
|
||
const float EPSILON = 1e-6f;
|
||
int prevLineIdx = -1;
|
||
int ptInLineCounter = 0;
|
||
int maxPointCount = 0;
|
||
|
||
for (size_t i = 0; i < cloud.points.size(); ++i) {
|
||
const auto& pt = cloud.points[i];
|
||
|
||
// 跟踪线内索引(对所有点递增,包括被过滤的零点)
|
||
int lineIdx = -1;
|
||
if (data.hasLineInfo && i < cloud.lineIndices.size()) {
|
||
lineIdx = cloud.lineIndices[i];
|
||
}
|
||
if (lineIdx != prevLineIdx) {
|
||
ptInLineCounter = 0;
|
||
prevLineIdx = lineIdx;
|
||
}
|
||
|
||
int pointInLineIndex = (i < cloud.pointInLineIndices.size())
|
||
? cloud.pointInLineIndices[i] : -1;
|
||
if (pointInLineIndex < 0) {
|
||
pointInLineIndex = ptInLineCounter;
|
||
}
|
||
maxPointCount = qMax(maxPointCount, pointInLineIndex + 1);
|
||
|
||
if (!std::isfinite(pt.x) || !std::isfinite(pt.y) || !std::isfinite(pt.z)) {
|
||
ptInLineCounter++;
|
||
continue;
|
||
}
|
||
|
||
// 显示时过滤 (0,0,0) 点
|
||
if (std::fabs(pt.x) < EPSILON && std::fabs(pt.y) < EPSILON && std::fabs(pt.z) < EPSILON) {
|
||
ptInLineCounter++;
|
||
continue;
|
||
}
|
||
|
||
size_t pointIndex = data.vertices.size() / 3;
|
||
|
||
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);
|
||
data.colors.push_back(pt.b / 255.0f);
|
||
|
||
// 保存原始索引
|
||
data.originalIndices.push_back(static_cast<int>(i));
|
||
|
||
// 保存线内索引(点在所属线中的位置)
|
||
data.pointInLineIndices.push_back(pointInLineIndex);
|
||
|
||
// 线索引数组必须和顶点数组保持一一对应;未知线号用 -1 占位。
|
||
if (data.hasLineInfo) {
|
||
data.lineIndices.push_back(lineIdx);
|
||
if (lineIdx >= 0 && lineIdx + 1 > data.totalLines) {
|
||
data.totalLines = lineIdx + 1;
|
||
}
|
||
}
|
||
|
||
// 收集自定义点大小(A > 1)
|
||
if (pt.pointSize > 1.0f) {
|
||
sizeGroupMap[pt.pointSize].push_back(pointIndex);
|
||
}
|
||
|
||
ptInLineCounter++;
|
||
}
|
||
|
||
// 构建自定义点大小分组
|
||
if (!sizeGroupMap.empty()) {
|
||
data.hasCustomPointSizes = true;
|
||
for (auto& pair : sizeGroupMap) {
|
||
PointCloudData::PointSizeGroup group;
|
||
group.pointSize = pair.first;
|
||
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());
|
||
}
|
||
|
||
// 计算每线点数
|
||
if (data.totalLines > 0) {
|
||
data.pointsPerLine = maxPointCount;
|
||
}
|
||
|
||
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();
|
||
}
|
||
|
||
void PointCloudGLWidget::appendPointCloud(const PointCloudXYZRGB& cloud,
|
||
const QString& name)
|
||
{
|
||
if (cloud.empty()) {
|
||
return;
|
||
}
|
||
if (m_pointClouds.empty() || !m_pointClouds.back().hasColor) {
|
||
addPointCloud(cloud, name);
|
||
return;
|
||
}
|
||
|
||
PointCloudData& data = m_pointClouds.back();
|
||
if (!name.isEmpty()) {
|
||
data.name = name;
|
||
}
|
||
|
||
const bool incomingHasLineInfo = !cloud.lineIndices.empty();
|
||
if (incomingHasLineInfo && !data.hasLineInfo) {
|
||
// 后续数据首次带线信息时,为已有顶点补齐未知线号,避免索引数组错位。
|
||
data.lineIndices.resize(data.vertices.size() / 3, -1);
|
||
data.hasLineInfo = true;
|
||
} else {
|
||
data.hasLineInfo = data.hasLineInfo || incomingHasLineInfo;
|
||
}
|
||
const size_t originalIndexOffset = data.vertices.size() / 3;
|
||
const float epsilon = 1e-6f;
|
||
int previousLineIndex = -1;
|
||
int pointInLineIndex = 0;
|
||
int maxPointCount = data.pointsPerLine;
|
||
|
||
for (size_t index = 0; index < cloud.points.size(); ++index) {
|
||
const Point3DRGB& point = cloud.points[index];
|
||
int lineIndex = -1;
|
||
if (incomingHasLineInfo && index < cloud.lineIndices.size()) {
|
||
lineIndex = cloud.lineIndices[index];
|
||
}
|
||
if (lineIndex != previousLineIndex) {
|
||
pointInLineIndex = 0;
|
||
previousLineIndex = lineIndex;
|
||
}
|
||
|
||
int sourcePointIndex = (index < cloud.pointInLineIndices.size())
|
||
? cloud.pointInLineIndices[index] : -1;
|
||
if (sourcePointIndex < 0) {
|
||
sourcePointIndex = pointInLineIndex;
|
||
}
|
||
maxPointCount = qMax(maxPointCount, sourcePointIndex + 1);
|
||
|
||
if (!std::isfinite(point.x) || !std::isfinite(point.y) ||
|
||
!std::isfinite(point.z) ||
|
||
(std::fabs(point.x) < epsilon &&
|
||
std::fabs(point.y) < epsilon &&
|
||
std::fabs(point.z) < epsilon)) {
|
||
++pointInLineIndex;
|
||
continue;
|
||
}
|
||
|
||
const size_t pointIndex = data.vertices.size() / 3;
|
||
data.vertices.push_back(point.x);
|
||
data.vertices.push_back(point.y);
|
||
data.vertices.push_back(point.z);
|
||
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);
|
||
data.originalIndices.push_back(
|
||
static_cast<int>(originalIndexOffset + index));
|
||
data.pointInLineIndices.push_back(sourcePointIndex);
|
||
|
||
if (data.hasLineInfo) {
|
||
data.lineIndices.push_back(lineIndex);
|
||
if (lineIndex >= 0) {
|
||
data.totalLines = qMax(data.totalLines, lineIndex + 1);
|
||
}
|
||
}
|
||
|
||
if (point.pointSize > 1.0f) {
|
||
auto sizeGroup = std::find_if(
|
||
data.customPointSizeGroups.begin(),
|
||
data.customPointSizeGroups.end(),
|
||
[&point](const PointCloudData::PointSizeGroup& group) {
|
||
return group.pointSize == point.pointSize;
|
||
});
|
||
if (sizeGroup == data.customPointSizeGroups.end()) {
|
||
PointCloudData::PointSizeGroup group;
|
||
group.pointSize = point.pointSize;
|
||
data.customPointSizeGroups.push_back(std::move(group));
|
||
sizeGroup = data.customPointSizeGroups.end() - 1;
|
||
}
|
||
sizeGroup->indices.push_back(pointIndex);
|
||
data.hasCustomPointSizes = true;
|
||
}
|
||
++pointInLineIndex;
|
||
}
|
||
|
||
if (data.totalLines > 0) {
|
||
data.pointsPerLine = maxPointCount;
|
||
}
|
||
|
||
makeCurrent();
|
||
uploadToVBO(data);
|
||
doneCurrent();
|
||
|
||
computeBoundingBox();
|
||
const float fittedDistance = fittedCameraDistance();
|
||
if (std::isfinite(fittedDistance)) {
|
||
m_distance = qMax(m_distance, fittedDistance);
|
||
}
|
||
update();
|
||
}
|
||
|
||
|
||
void PointCloudGLWidget::appendPointCloud(const PointCloudXYZ& cloud, const QString& name)
|
||
{
|
||
if (cloud.empty()) {
|
||
return;
|
||
}
|
||
if (m_pointClouds.empty() || m_pointClouds.back().hasColor) {
|
||
addPointCloud(cloud, name, false);
|
||
return;
|
||
}
|
||
|
||
PointCloudData& data = m_pointClouds.back();
|
||
if (!name.isEmpty()) {
|
||
data.name = name;
|
||
}
|
||
|
||
const bool incomingHasLineInfo = !cloud.lineIndices.empty();
|
||
if (incomingHasLineInfo && !data.hasLineInfo) {
|
||
// 后续数据首次带线信息时,为已有顶点补齐未知线号,避免索引数组错位。
|
||
data.lineIndices.resize(data.vertices.size() / 3, -1);
|
||
data.hasLineInfo = true;
|
||
} else {
|
||
data.hasLineInfo = data.hasLineInfo || incomingHasLineInfo;
|
||
}
|
||
const size_t originalIndexOffset = data.vertices.size() / 3;
|
||
const size_t firstNewVertex = originalIndexOffset;
|
||
const float epsilon = 1e-6f;
|
||
int previousLineIndex = -1;
|
||
int pointInLineIndex = 0;
|
||
int maxPointCount = data.pointsPerLine;
|
||
|
||
for (size_t index = 0; index < cloud.points.size(); ++index) {
|
||
const Point3D& point = cloud.points[index];
|
||
int lineIndex = -1;
|
||
if (incomingHasLineInfo && index < cloud.lineIndices.size()) {
|
||
lineIndex = cloud.lineIndices[index];
|
||
}
|
||
if (lineIndex != previousLineIndex) {
|
||
pointInLineIndex = 0;
|
||
previousLineIndex = lineIndex;
|
||
}
|
||
|
||
int sourcePointIndex = (index < cloud.pointInLineIndices.size())
|
||
? cloud.pointInLineIndices[index] : -1;
|
||
if (sourcePointIndex < 0) {
|
||
sourcePointIndex = pointInLineIndex;
|
||
}
|
||
maxPointCount = qMax(maxPointCount, sourcePointIndex + 1);
|
||
|
||
if (!std::isfinite(point.x) || !std::isfinite(point.y) ||
|
||
!std::isfinite(point.z) ||
|
||
(std::fabs(point.x) < epsilon &&
|
||
std::fabs(point.y) < epsilon &&
|
||
std::fabs(point.z) < epsilon)) {
|
||
++pointInLineIndex;
|
||
continue;
|
||
}
|
||
|
||
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<int>(originalIndexOffset + index));
|
||
data.pointInLineIndices.push_back(sourcePointIndex);
|
||
if (data.hasLineInfo) {
|
||
data.lineIndices.push_back(lineIndex);
|
||
if (lineIndex >= 0) {
|
||
data.totalLines = qMax(data.totalLines, lineIndex + 1);
|
||
}
|
||
}
|
||
++pointInLineIndex;
|
||
}
|
||
|
||
if (data.totalLines > 0) {
|
||
// 过滤后各扫描线可能不等长,使用原始线内索引的最大值保留网格位置。
|
||
data.pointsPerLine = maxPointCount;
|
||
}
|
||
|
||
// 流式追加时只为新增顶点生成伪彩,避免重复扫描历史点云。
|
||
rebuildPseudoColors(data, firstNewVertex);
|
||
|
||
makeCurrent();
|
||
uploadToVBO(data);
|
||
doneCurrent();
|
||
computeBoundingBox();
|
||
m_distance = qMax(m_distance, fittedCameraDistance());
|
||
update();
|
||
}
|
||
|
||
void PointCloudGLWidget::setNewestPointCloudLineLayout(int totalLines, int pointsPerLine)
|
||
{
|
||
if (m_pointClouds.empty())
|
||
{
|
||
return;
|
||
}
|
||
|
||
PointCloudData& data = m_pointClouds.back();
|
||
if (totalLines > 0)
|
||
{
|
||
data.hasLineInfo = true;
|
||
data.totalLines = qMax(data.totalLines, totalLines);
|
||
}
|
||
if (pointsPerLine > 0)
|
||
{
|
||
data.pointsPerLine = qMax(data.pointsPerLine, pointsPerLine);
|
||
}
|
||
}
|
||
|
||
void PointCloudGLWidget::clearPointClouds()
|
||
{
|
||
clearPointCloudBatches();
|
||
m_streamDisplayedLineCount = 0;
|
||
|
||
// 释放所有 VBO
|
||
makeCurrent();
|
||
for (auto& cloudData : m_pointClouds) {
|
||
releaseVBO(cloudData);
|
||
}
|
||
doneCurrent();
|
||
|
||
m_pointClouds.clear();
|
||
m_selectedPoints.clear();
|
||
m_selectedLine = SelectedLineInfo();
|
||
m_lineSegments.clear();
|
||
m_basicShapeSegments.clear();
|
||
m_basicShapeSurfaces.clear();
|
||
m_basicShapePoints.clear();
|
||
m_posePoints.clear();
|
||
m_minBound = QVector3D(-50, -50, -50);
|
||
m_maxBound = QVector3D(50, 50, 50);
|
||
m_center = QVector3D(0, 0, 0);
|
||
m_colorIndex = 0; // 重置颜色索引
|
||
resetView();
|
||
update();
|
||
}
|
||
|
||
bool PointCloudGLWidget::removeOldestPointCloud(bool refreshDisplay)
|
||
{
|
||
if (m_pointClouds.empty()) {
|
||
return false;
|
||
}
|
||
|
||
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();
|
||
// 伪彩按绝对 Z 周期着色,删块不必重算其余点云颜色/VBO。
|
||
if (refreshDisplay) {
|
||
computeBoundingBox();
|
||
update();
|
||
}
|
||
return true;
|
||
}
|
||
|
||
bool PointCloudGLWidget::removeNewestPointCloud(bool refreshDisplay)
|
||
{
|
||
if (m_pointClouds.empty()) {
|
||
return false;
|
||
}
|
||
|
||
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();
|
||
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<double>(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<float> vertices;
|
||
std::vector<float> colors;
|
||
std::vector<int> lineIndices;
|
||
std::vector<int> originalIndices;
|
||
std::vector<int> pointInLineIndices;
|
||
std::vector<int> 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<int>(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<PointCloudData::PointSizeGroup> 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<size_t>(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;
|
||
update();
|
||
}
|
||
|
||
void PointCloudGLWidget::setPointSize(float size)
|
||
{
|
||
m_pointSize = qBound(1.0f, size, 10.0f);
|
||
update();
|
||
}
|
||
|
||
float PointCloudGLWidget::effectivePointSize() const
|
||
{
|
||
const float userSize = qMax(m_pointSize, 1.0f);
|
||
const float pixelRatio = static_cast<float>(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))
|
||
{
|
||
return QVector3D();
|
||
}
|
||
|
||
if (!std::isfinite(m_pseudoColorPeriod) || m_pseudoColorPeriod <= 1e-6f)
|
||
{
|
||
return m_pseudoColorDefaultPalette
|
||
? defaultHuePseudoColor(0.0f)
|
||
: m_pseudoColorStartColor;
|
||
}
|
||
|
||
const double period = static_cast<double>(m_pseudoColorPeriod);
|
||
const double cycleValue = std::floor(static_cast<double>(z) / period);
|
||
double phase = std::fmod(static_cast<double>(z), period);
|
||
if (phase < 0.0)
|
||
{
|
||
phase += period;
|
||
}
|
||
|
||
float t = static_cast<float>(phase / period);
|
||
if (m_pseudoColorDefaultPalette)
|
||
{
|
||
// 默认方案:每个周期走完整色相条,首尾同为红色,周期边界自然衔接。
|
||
return defaultHuePseudoColor(t);
|
||
}
|
||
|
||
// 自定义起止色:固定周期内连续梯度,奇数周期反向避免边界突变。
|
||
if (std::fmod(std::fabs(cycleValue), 2.0) >= 1.0)
|
||
{
|
||
t = 1.0f - t;
|
||
}
|
||
return m_pseudoColorStartColor * (1.0f - t) + m_pseudoColorEndColor * t;
|
||
}
|
||
|
||
void PointCloudGLWidget::rebuildPseudoColors(PointCloudData& data, size_t firstVertex)
|
||
{
|
||
if (data.hasColor)
|
||
{
|
||
return;
|
||
}
|
||
|
||
const size_t vertexCount = data.vertices.size() / 3;
|
||
if (!m_pseudoColorEnabled)
|
||
{
|
||
data.colors.clear();
|
||
data.pseudoColor = false;
|
||
return;
|
||
}
|
||
|
||
if (firstVertex > vertexCount)
|
||
{
|
||
firstVertex = 0;
|
||
}
|
||
|
||
data.colors.resize(vertexCount * 3);
|
||
for (size_t vertexIndex = firstVertex; vertexIndex < vertexCount; ++vertexIndex)
|
||
{
|
||
const size_t offset = vertexIndex * 3;
|
||
const QVector3D color = pseudoColorForZ(data.vertices[offset + 2]);
|
||
data.colors[offset] = color.x();
|
||
data.colors[offset + 1] = color.y();
|
||
data.colors[offset + 2] = color.z();
|
||
}
|
||
data.pseudoColor = true;
|
||
}
|
||
|
||
void PointCloudGLWidget::updatePseudoColors()
|
||
{
|
||
bool hasUncoloredCloud = false;
|
||
for (auto& data : m_pointClouds)
|
||
{
|
||
if (!data.hasColor)
|
||
{
|
||
hasUncoloredCloud = true;
|
||
rebuildPseudoColors(data, 0);
|
||
}
|
||
}
|
||
|
||
if (!hasUncoloredCloud)
|
||
{
|
||
return;
|
||
}
|
||
|
||
makeCurrent();
|
||
for (auto& data : m_pointClouds)
|
||
{
|
||
if (!data.hasColor)
|
||
{
|
||
uploadToVBO(data);
|
||
}
|
||
}
|
||
doneCurrent();
|
||
update();
|
||
}
|
||
|
||
void PointCloudGLWidget::setPseudoColorEnabled(bool enabled)
|
||
{
|
||
setPseudoColorSettings(enabled,
|
||
m_pseudoColorPeriod,
|
||
m_pseudoColorStartColor,
|
||
m_pseudoColorEndColor,
|
||
m_pseudoColorDefaultPalette);
|
||
}
|
||
|
||
void PointCloudGLWidget::setPseudoColorSettings(bool enabled, float period, const QVector3D& startColor, const QVector3D& endColor, bool defaultPalette)
|
||
{
|
||
const float safePeriod = std::isfinite(period) && period > 0.0f
|
||
? qMax(1.0f, period)
|
||
: defaultPseudoColorPeriod();
|
||
const QVector3D clampedStart = clampPseudoColor(startColor);
|
||
const QVector3D clampedEnd = clampPseudoColor(endColor);
|
||
const bool changed = m_pseudoColorEnabled != enabled ||
|
||
!qFuzzyCompare(m_pseudoColorPeriod, safePeriod) ||
|
||
m_pseudoColorStartColor != clampedStart ||
|
||
m_pseudoColorEndColor != clampedEnd ||
|
||
m_pseudoColorDefaultPalette != defaultPalette;
|
||
if (!changed)
|
||
{
|
||
return;
|
||
}
|
||
|
||
m_pseudoColorEnabled = enabled;
|
||
m_pseudoColorPeriod = safePeriod;
|
||
m_pseudoColorStartColor = clampedStart;
|
||
m_pseudoColorEndColor = clampedEnd;
|
||
m_pseudoColorDefaultPalette = defaultPalette;
|
||
updatePseudoColors();
|
||
}
|
||
|
||
void PointCloudGLWidget::setPseudoColorPeriod(float period)
|
||
{
|
||
setPseudoColorSettings(m_pseudoColorEnabled,
|
||
period,
|
||
m_pseudoColorStartColor,
|
||
m_pseudoColorEndColor,
|
||
m_pseudoColorDefaultPalette);
|
||
}
|
||
|
||
void PointCloudGLWidget::setPseudoColorColorRange(const QVector3D& startColor,
|
||
const QVector3D& endColor)
|
||
{
|
||
// 直接指定起止色表示切换到自定义梯度;默认蓝→红可通过重置恢复。
|
||
setPseudoColorSettings(m_pseudoColorEnabled,
|
||
m_pseudoColorPeriod,
|
||
startColor,
|
||
endColor,
|
||
false);
|
||
}
|
||
|
||
void PointCloudGLWidget::setPseudoColorDefaultPalette(bool enabled)
|
||
{
|
||
setPseudoColorSettings(m_pseudoColorEnabled,
|
||
m_pseudoColorPeriod,
|
||
m_pseudoColorStartColor,
|
||
m_pseudoColorEndColor,
|
||
enabled);
|
||
}
|
||
|
||
void PointCloudGLWidget::resetPseudoColorDefaults()
|
||
{
|
||
setPseudoColorSettings(true,
|
||
defaultPseudoColorPeriod(),
|
||
defaultPseudoColorStartColor(),
|
||
defaultPseudoColorEndColor(),
|
||
true);
|
||
}
|
||
|
||
float PointCloudGLWidget::defaultPseudoColorPeriod()
|
||
{
|
||
return kDefaultPseudoColorPeriod;
|
||
}
|
||
|
||
QVector3D PointCloudGLWidget::defaultPseudoColorStartColor()
|
||
{
|
||
return QVector3D(0.0f, 0.0f, 1.0f);
|
||
}
|
||
|
||
QVector3D PointCloudGLWidget::defaultPseudoColorEndColor()
|
||
{
|
||
return QVector3D(1.0f, 0.0f, 0.0f);
|
||
}
|
||
|
||
void PointCloudGLWidget::resetView()
|
||
{
|
||
QVector3D size = m_maxBound - m_minBound;
|
||
float maxSize = qMax(qMax(size.x(), size.y()), size.z());
|
||
|
||
// 默认视角:面向 XY 平面,X 向右,Y 向下
|
||
// 绕 X 轴旋转 180°:Y 轴从向上翻转为向下
|
||
m_rotationX = 180.0f;
|
||
m_rotationY = 0.0f;
|
||
m_rotationZ = 0.0f;
|
||
m_rotation = QQuaternion::fromEulerAngles(m_rotationX, m_rotationY, m_rotationZ);
|
||
m_pan = QVector3D(0, 0, 0);
|
||
m_distance = fittedCameraDistance();
|
||
|
||
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);
|
||
update();
|
||
}
|
||
|
||
void PointCloudGLWidget::fitToContent()
|
||
{
|
||
computeBoundingBox();
|
||
updatePseudoColors();
|
||
resetView();
|
||
}
|
||
|
||
void PointCloudGLWidget::updateProjection(const QMatrix4x4& modelView)
|
||
{
|
||
const QVector3D corners[] = {
|
||
{m_minBound.x(), m_minBound.y(), m_minBound.z()},
|
||
{m_minBound.x(), m_minBound.y(), m_maxBound.z()},
|
||
{m_minBound.x(), m_maxBound.y(), m_minBound.z()},
|
||
{m_minBound.x(), m_maxBound.y(), m_maxBound.z()},
|
||
{m_maxBound.x(), m_minBound.y(), m_minBound.z()},
|
||
{m_maxBound.x(), m_minBound.y(), m_maxBound.z()},
|
||
{m_maxBound.x(), m_maxBound.y(), m_minBound.z()},
|
||
{m_maxBound.x(), m_maxBound.y(), m_maxBound.z()}
|
||
};
|
||
|
||
float minimumDepth = FLT_MAX;
|
||
float maximumDepth = -FLT_MAX;
|
||
for (const QVector3D& corner : corners) {
|
||
const float depth = -modelView.map(corner).z();
|
||
minimumDepth = qMin(minimumDepth, depth);
|
||
maximumDepth = qMax(maximumDepth, depth);
|
||
}
|
||
|
||
constexpr float kMinimumNearPlane = 0.01f;
|
||
float nearPlane = kMinimumNearPlane;
|
||
float farPlane = 1.0f;
|
||
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;
|
||
}
|
||
|
||
const float aspect = static_cast<float>(width()) /
|
||
static_cast<float>(height() > 0 ? height() : 1);
|
||
m_projection.setToIdentity();
|
||
m_projection.perspective(45.0f, aspect, nearPlane, farPlane);
|
||
}
|
||
|
||
float PointCloudGLWidget::fittedCameraDistance(float padding) const
|
||
{
|
||
const QVector3D corners[] = {
|
||
{m_minBound.x(), m_minBound.y(), m_minBound.z()},
|
||
{m_minBound.x(), m_minBound.y(), m_maxBound.z()},
|
||
{m_minBound.x(), m_maxBound.y(), m_minBound.z()},
|
||
{m_minBound.x(), m_maxBound.y(), m_maxBound.z()},
|
||
{m_maxBound.x(), m_minBound.y(), m_minBound.z()},
|
||
{m_maxBound.x(), m_minBound.y(), m_maxBound.z()},
|
||
{m_maxBound.x(), m_maxBound.y(), m_minBound.z()},
|
||
{m_maxBound.x(), m_maxBound.y(), m_maxBound.z()}};
|
||
|
||
const float aspect = qMax(0.01f,
|
||
static_cast<float>(width()) /
|
||
static_cast<float>(height() > 0 ? height() : 1));
|
||
const float tangent = std::tan(qDegreesToRadians(45.0f * 0.5f));
|
||
const float safePadding = qMax(1.0f, padding);
|
||
float distance = minimumCameraDistance();
|
||
for (const QVector3D& corner : corners) {
|
||
const QVector3D relative =
|
||
m_rotation.rotatedVector(corner - m_center);
|
||
const float horizontalDistance =
|
||
relative.z() + std::fabs(relative.x()) * safePadding / (tangent * aspect);
|
||
const float verticalDistance =
|
||
relative.z() + std::fabs(relative.y()) * safePadding / tangent;
|
||
distance = qMax(distance, qMax(horizontalDistance, verticalDistance));
|
||
}
|
||
return qBound(10.0f, distance, 1.0e7f);
|
||
}
|
||
|
||
float PointCloudGLWidget::minimumCameraDistance() const
|
||
{
|
||
const QVector3D corners[] = {
|
||
{m_minBound.x(), m_minBound.y(), m_minBound.z()},
|
||
{m_minBound.x(), m_minBound.y(), m_maxBound.z()},
|
||
{m_minBound.x(), m_maxBound.y(), m_minBound.z()},
|
||
{m_minBound.x(), m_maxBound.y(), m_maxBound.z()},
|
||
{m_maxBound.x(), m_minBound.y(), m_minBound.z()},
|
||
{m_maxBound.x(), m_minBound.y(), m_maxBound.z()},
|
||
{m_maxBound.x(), m_maxBound.y(), m_minBound.z()},
|
||
{m_maxBound.x(), m_maxBound.y(), m_maxBound.z()}
|
||
};
|
||
|
||
float frontExtent = 0.0f;
|
||
for (const QVector3D& corner : corners) {
|
||
const QVector3D relativePosition = corner - m_center;
|
||
frontExtent = qMax(frontExtent,
|
||
m_rotation.rotatedVector(relativePosition).z());
|
||
}
|
||
|
||
const float margin = qMax(0.1f, (m_maxBound - m_minBound).length() * 0.002f);
|
||
return qBound(0.1f, frontExtent + margin, 1.0e7f);
|
||
}
|
||
|
||
void PointCloudGLWidget::zoomIn()
|
||
{
|
||
m_distance *= 0.9f;
|
||
m_distance = qBound(minimumCameraDistance(), m_distance, 1.0e7f);
|
||
update();
|
||
}
|
||
|
||
void PointCloudGLWidget::zoomOut()
|
||
{
|
||
m_distance *= 1.1f;
|
||
m_distance = qBound(minimumCameraDistance(), m_distance, 1.0e7f);
|
||
update();
|
||
}
|
||
|
||
void PointCloudGLWidget::setViewAngles(float rotX, float rotY, float rotZ)
|
||
{
|
||
// 归一化到 ±180°,避免往同一方向旋转时角度无限累加
|
||
m_rotationX = NormalizeAngleDegrees(rotX);
|
||
m_rotationY = NormalizeAngleDegrees(rotY);
|
||
m_rotationZ = NormalizeAngleDegrees(rotZ);
|
||
m_pan = QVector3D(0, 0, 0);
|
||
|
||
// 从欧拉角转换为四元数(ZYX顺序)
|
||
m_rotation = QQuaternion::fromEulerAngles(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);
|
||
|
||
emit viewAnglesChanged(m_rotationX, m_rotationY, m_rotationZ);
|
||
update();
|
||
}
|
||
|
||
void PointCloudGLWidget::clearSelectedPoints()
|
||
{
|
||
m_selectedPoints.clear();
|
||
update();
|
||
}
|
||
|
||
void PointCloudGLWidget::clearSelectedLine()
|
||
{
|
||
m_selectedLine = SelectedLineInfo();
|
||
update();
|
||
}
|
||
|
||
bool PointCloudGLWidget::selectLineByIndex(int lineIndex)
|
||
{
|
||
if (m_pointClouds.empty()) {
|
||
return false;
|
||
}
|
||
|
||
const auto& cloudData = m_pointClouds[0];
|
||
if (!cloudData.hasLineInfo) {
|
||
return false;
|
||
}
|
||
|
||
// 检查线索引是否有效
|
||
if (lineIndex < 0 || lineIndex >= cloudData.totalLines) {
|
||
return false;
|
||
}
|
||
|
||
// 计算该线上的点数
|
||
int pointCount = 0;
|
||
for (int idx : cloudData.lineIndices) {
|
||
if (idx == lineIndex) {
|
||
pointCount++;
|
||
}
|
||
}
|
||
|
||
if (pointCount == 0) {
|
||
return false;
|
||
}
|
||
|
||
// 设置选中的线
|
||
m_selectedLine.valid = true;
|
||
m_selectedLine.cloudIndex = 0;
|
||
m_selectedLine.lineIndex = lineIndex;
|
||
m_selectedLine.pointIndex = -1;
|
||
m_selectedLine.pointCount = pointCount;
|
||
m_selectedLine.mode = LineSelectMode::Vertical;
|
||
|
||
emit lineSelected(m_selectedLine);
|
||
update();
|
||
return true;
|
||
}
|
||
|
||
bool PointCloudGLWidget::selectHorizontalLineByIndex(int pointIndex)
|
||
{
|
||
if (m_pointClouds.empty()) {
|
||
return false;
|
||
}
|
||
|
||
const auto& cloudData = m_pointClouds[0];
|
||
if (!cloudData.hasLineInfo || cloudData.totalLines <= 0 || cloudData.pointsPerLine <= 0) {
|
||
return false;
|
||
}
|
||
|
||
// 检查点索引是否有效(使用原始点云的每线点数)
|
||
if (pointIndex < 0 || pointIndex >= cloudData.pointsPerLine) {
|
||
return false;
|
||
}
|
||
|
||
bool hasPointAtIndex = false;
|
||
for (const int sourceIndex : cloudData.pointInLineIndices) {
|
||
if (sourceIndex == pointIndex) {
|
||
hasPointAtIndex = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!hasPointAtIndex) {
|
||
return false;
|
||
}
|
||
|
||
// 设置选中的横向线
|
||
m_selectedLine.valid = true;
|
||
m_selectedLine.cloudIndex = 0;
|
||
m_selectedLine.lineIndex = -1;
|
||
m_selectedLine.pointIndex = pointIndex;
|
||
m_selectedLine.pointCount = cloudData.totalLines; // 横向线的点数等于总线数
|
||
m_selectedLine.mode = LineSelectMode::Horizontal;
|
||
|
||
emit lineSelected(m_selectedLine);
|
||
update();
|
||
return true;
|
||
}
|
||
|
||
float PointCloudGLWidget::calculateDistance(const SelectedPointInfo& p1, const SelectedPointInfo& p2)
|
||
{
|
||
if (!p1.valid || !p2.valid) {
|
||
return 0.0f;
|
||
}
|
||
|
||
float dx = p2.x - p1.x;
|
||
float dy = p2.y - p1.y;
|
||
float dz = p2.z - p1.z;
|
||
|
||
return std::sqrt(dx * dx + dy * dy + dz * dz);
|
||
}
|
||
|
||
void PointCloudGLWidget::updateSelectedPointCoord(int index, float x, float y, float z)
|
||
{
|
||
if (index < 0 || index >= m_selectedPoints.size()) {
|
||
return;
|
||
}
|
||
|
||
if (!m_selectedPoints[index].valid) {
|
||
return;
|
||
}
|
||
|
||
// 更新选中点的坐标
|
||
m_selectedPoints[index].x = x;
|
||
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);
|
||
|
||
// 刷新显示
|
||
update();
|
||
}
|
||
|
||
void PointCloudGLWidget::setSelectedPointCoord(int index, float x, float y, float z)
|
||
{
|
||
if (index < 0 || index >= MAX_SELECTED_POINTS) {
|
||
return;
|
||
}
|
||
|
||
// 如果索引位置不存在,扩展列表
|
||
while (m_selectedPoints.size() <= index) {
|
||
m_selectedPoints.append(SelectedPointInfo());
|
||
}
|
||
|
||
m_selectedPoints[index].valid = true;
|
||
m_selectedPoints[index].x = x;
|
||
m_selectedPoints[index].y = y;
|
||
m_selectedPoints[index].z = z;
|
||
m_selectedPoints[index].cloudIndex = -1;
|
||
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);
|
||
|
||
// 刷新显示
|
||
update();
|
||
}
|
||
|
||
QVector<QVector3D> PointCloudGLWidget::getSelectedLinePoints() const
|
||
{
|
||
QVector<QVector3D> points;
|
||
|
||
if (!m_selectedLine.valid || m_selectedLine.cloudIndex < 0) {
|
||
return points;
|
||
}
|
||
|
||
if (m_selectedLine.cloudIndex >= static_cast<int>(m_pointClouds.size())) {
|
||
return points;
|
||
}
|
||
|
||
const auto& cloudData = m_pointClouds[m_selectedLine.cloudIndex];
|
||
if (!cloudData.hasLineInfo) {
|
||
return points;
|
||
}
|
||
|
||
if (m_selectedLine.mode == LineSelectMode::Vertical) {
|
||
// 纵向选线:获取同一条扫描线上的所有点
|
||
for (size_t i = 0; i < cloudData.lineIndices.size(); ++i) {
|
||
if (cloudData.lineIndices[i] == m_selectedLine.lineIndex) {
|
||
size_t vertIdx = i * 3;
|
||
if (vertIdx + 2 < cloudData.vertices.size()) {
|
||
points.append(QVector3D(
|
||
cloudData.vertices[vertIdx],
|
||
cloudData.vertices[vertIdx + 1],
|
||
cloudData.vertices[vertIdx + 2]));
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// 横向选线:获取所有线的相同索引点
|
||
if (cloudData.pointsPerLine > 0 && m_selectedLine.pointIndex >= 0) {
|
||
for (size_t i = 0; i < cloudData.pointInLineIndices.size(); ++i) {
|
||
if (cloudData.pointInLineIndices[i] == m_selectedLine.pointIndex) {
|
||
size_t vertIdx = i * 3;
|
||
if (vertIdx + 2 < cloudData.vertices.size()) {
|
||
points.append(QVector3D(
|
||
cloudData.vertices[vertIdx],
|
||
cloudData.vertices[vertIdx + 1],
|
||
cloudData.vertices[vertIdx + 2]));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return points;
|
||
}
|
||
|
||
void PointCloudGLWidget::setListHighlightPoint(const QVector3D& point)
|
||
{
|
||
m_hasListHighlightPoint = true;
|
||
m_listHighlightPoint = point;
|
||
update();
|
||
}
|
||
|
||
void PointCloudGLWidget::clearListHighlightPoint()
|
||
{
|
||
m_hasListHighlightPoint = false;
|
||
update();
|
||
}
|
||
|
||
void PointCloudGLWidget::transformAllClouds(const QMatrix4x4& matrix)
|
||
{
|
||
for (auto& cloudData : m_pointClouds) {
|
||
for (size_t i = 0; i + 2 < cloudData.vertices.size(); i += 3) {
|
||
QVector3D pt(cloudData.vertices[i], cloudData.vertices[i + 1], cloudData.vertices[i + 2]);
|
||
QVector3D transformed = matrix.map(pt);
|
||
cloudData.vertices[i] = transformed.x();
|
||
cloudData.vertices[i + 1] = transformed.y();
|
||
cloudData.vertices[i + 2] = transformed.z();
|
||
}
|
||
recomputeCloudBounds(cloudData);
|
||
}
|
||
|
||
// 重新上传 VBO(顶点数据已变更)
|
||
makeCurrent();
|
||
for (auto& cloudData : m_pointClouds) {
|
||
uploadToVBO(cloudData);
|
||
}
|
||
doneCurrent();
|
||
|
||
computeBoundingBox();
|
||
updatePseudoColors();
|
||
resetView();
|
||
update();
|
||
}
|
||
|
||
size_t PointCloudGLWidget::rotateCloudsByZThreshold(const QMatrix4x4& rotMatrix, float zThreshold)
|
||
{
|
||
size_t rotatedCount = 0;
|
||
|
||
for (auto& cloudData : m_pointClouds) {
|
||
for (size_t i = 0; i + 2 < cloudData.vertices.size(); i += 3) {
|
||
if (cloudData.vertices[i + 2] < zThreshold) {
|
||
QVector3D pt(cloudData.vertices[i], cloudData.vertices[i + 1], cloudData.vertices[i + 2]);
|
||
QVector3D transformed = rotMatrix.map(pt);
|
||
cloudData.vertices[i] = transformed.x();
|
||
cloudData.vertices[i + 1] = transformed.y();
|
||
cloudData.vertices[i + 2] = transformed.z();
|
||
++rotatedCount;
|
||
}
|
||
}
|
||
recomputeCloudBounds(cloudData);
|
||
}
|
||
|
||
if (rotatedCount > 0) {
|
||
makeCurrent();
|
||
for (auto& cloudData : m_pointClouds) {
|
||
uploadToVBO(cloudData);
|
||
}
|
||
doneCurrent();
|
||
computeBoundingBox();
|
||
updatePseudoColors();
|
||
resetView();
|
||
update();
|
||
}
|
||
|
||
return rotatedCount;
|
||
}
|
||
|
||
size_t PointCloudGLWidget::getAllCloudsByLines(std::vector<std::vector<SVzNL3DPosition>>& scanLines) const
|
||
{
|
||
scanLines.clear();
|
||
size_t totalPoints = 0;
|
||
|
||
for (const auto& cloudData : m_pointClouds) {
|
||
const size_t displayCount = cloudData.vertices.size() / 3;
|
||
if (displayCount == 0) continue;
|
||
|
||
if (cloudData.hasLineInfo && cloudData.totalLines > 0 && cloudData.pointsPerLine > 0) {
|
||
// 有线信息:用 pointsPerLine 还原每条线的完整点数,过滤掉的 (0,0,0) 点补零
|
||
const int lines = cloudData.totalLines;
|
||
const int ptsPerLine = cloudData.pointsPerLine;
|
||
|
||
// 初始化所有线,每条线 pointsPerLine 个零点
|
||
const size_t baseIdx = scanLines.size();
|
||
for (int i = 0; i < lines; ++i) {
|
||
std::vector<SVzNL3DPosition> line(ptsPerLine);
|
||
memset(line.data(), 0, sizeof(SVzNL3DPosition) * ptsPerLine);
|
||
for (int j = 0; j < ptsPerLine; ++j) {
|
||
line[j].nPointIdx = j;
|
||
}
|
||
scanLines.push_back(std::move(line));
|
||
}
|
||
|
||
// 用实际显示的点填充对应位置
|
||
for (size_t i = 0; i < displayCount; ++i) {
|
||
const int lineIdx = (i < cloudData.lineIndices.size()) ? cloudData.lineIndices[i] : 0;
|
||
const int ptInLine = (i < cloudData.pointInLineIndices.size()) ? cloudData.pointInLineIndices[i] : static_cast<int>(i);
|
||
|
||
if (lineIdx >= 0 && lineIdx < lines && ptInLine >= 0 && ptInLine < ptsPerLine) {
|
||
auto& pos = scanLines[baseIdx + lineIdx][ptInLine];
|
||
pos.pt3D.x = cloudData.vertices[i * 3];
|
||
pos.pt3D.y = cloudData.vertices[i * 3 + 1];
|
||
pos.pt3D.z = cloudData.vertices[i * 3 + 2];
|
||
pos.nPointIdx = ptInLine;
|
||
}
|
||
}
|
||
|
||
totalPoints += static_cast<size_t>(lines) * ptsPerLine;
|
||
} else {
|
||
// 无线信息,每个点云作为一条线
|
||
std::vector<SVzNL3DPosition> line;
|
||
line.reserve(displayCount);
|
||
for (size_t i = 0; i < displayCount; ++i) {
|
||
SVzNL3DPosition pos;
|
||
memset(&pos, 0, sizeof(pos));
|
||
pos.nPointIdx = static_cast<int>(i);
|
||
pos.pt3D.x = cloudData.vertices[i * 3];
|
||
pos.pt3D.y = cloudData.vertices[i * 3 + 1];
|
||
pos.pt3D.z = cloudData.vertices[i * 3 + 2];
|
||
line.push_back(pos);
|
||
}
|
||
totalPoints += displayCount;
|
||
scanLines.push_back(std::move(line));
|
||
}
|
||
}
|
||
|
||
return totalPoints;
|
||
}
|
||
|
||
void PointCloudGLWidget::computeBoundingBox()
|
||
{
|
||
float minX = FLT_MAX, minY = FLT_MAX, minZ = FLT_MAX;
|
||
float maxX = -FLT_MAX, maxY = -FLT_MAX, maxZ = -FLT_MAX;
|
||
bool hasContent = false;
|
||
|
||
auto includePoint = [&](float x, float y, float z) {
|
||
minX = qMin(minX, x);
|
||
minY = qMin(minY, y);
|
||
minZ = qMin(minZ, z);
|
||
maxX = qMax(maxX, x);
|
||
maxY = qMax(maxY, y);
|
||
maxZ = qMax(maxZ, z);
|
||
hasContent = true;
|
||
};
|
||
|
||
auto includeSegment = [&](const LineSegment& segment) {
|
||
includePoint(segment.x1, segment.y1, segment.z1);
|
||
includePoint(segment.x2, segment.y2, segment.z2);
|
||
};
|
||
|
||
for (const auto& cloudData : m_pointClouds) {
|
||
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) {
|
||
includeSegment(segment);
|
||
}
|
||
|
||
for (const LineSegment& segment : m_basicShapeSegments) {
|
||
includeSegment(segment);
|
||
}
|
||
|
||
for (const SurfaceQuad& surface : m_basicShapeSurfaces) {
|
||
includePoint(surface.p1.x(), surface.p1.y(), surface.p1.z());
|
||
includePoint(surface.p2.x(), surface.p2.y(), surface.p2.z());
|
||
includePoint(surface.p3.x(), surface.p3.y(), surface.p3.z());
|
||
includePoint(surface.p4.x(), surface.p4.y(), surface.p4.z());
|
||
}
|
||
|
||
for (const BasicShapePoint& point : m_basicShapePoints) {
|
||
includePoint(point.x, point.y, point.z);
|
||
}
|
||
|
||
for (const PosePoint& pose : m_posePoints) {
|
||
const float scale = qMax(0.0f, pose.scale);
|
||
includePoint(pose.x - scale, pose.y - scale, pose.z - scale);
|
||
includePoint(pose.x + scale, pose.y + scale, pose.z + scale);
|
||
}
|
||
|
||
if (!hasContent) {
|
||
m_minBound = QVector3D(-50, -50, -50);
|
||
m_maxBound = QVector3D(50, 50, 50);
|
||
m_center = QVector3D(0, 0, 0);
|
||
return;
|
||
}
|
||
|
||
m_minBound = QVector3D(minX, minY, minZ);
|
||
m_maxBound = QVector3D(maxX, maxY, maxZ);
|
||
m_center = (m_minBound + m_maxBound) / 2.0f;
|
||
}
|
||
|
||
SelectedPointInfo PointCloudGLWidget::pickPoint(int screenX, int screenY)
|
||
{
|
||
SelectedPointInfo result;
|
||
|
||
if (m_pointClouds.empty()) {
|
||
return result;
|
||
}
|
||
|
||
// 视口(Qt 窗口即视口);GLES2 无 GL_MODELVIEW_MATRIX / gluProject,用与渲染一致的矩阵手动投影
|
||
const GLint viewport[4] = {0, 0, static_cast<GLint>(width()), static_cast<GLint>(height())};
|
||
const QMatrix4x4 modelView = m_view * m_model;
|
||
const QMatrix4x4 mvp = m_projection * modelView;
|
||
|
||
// 转换屏幕 Y 坐标(OpenGL Y 轴向上)
|
||
const int glScreenY = viewport[3] - screenY;
|
||
|
||
float minScreenDist = FLT_MAX;
|
||
size_t bestIndex = 0;
|
||
int bestCloudIndex = -1;
|
||
int bestLineIndex = -1;
|
||
float bestX = 0, bestY = 0, bestZ = 0;
|
||
|
||
// 遍历所有点,计算屏幕投影距离
|
||
for (size_t cloudIdx = 0; cloudIdx < m_pointClouds.size(); ++cloudIdx) {
|
||
const auto& cloudData = m_pointClouds[cloudIdx];
|
||
for (size_t i = 0; i < cloudData.vertices.size(); i += 3) {
|
||
const float wx = cloudData.vertices[i];
|
||
const float wy = cloudData.vertices[i + 1];
|
||
const float wz = cloudData.vertices[i + 2];
|
||
|
||
// 将世界坐标投影到屏幕(等价于 gluProject)
|
||
const QVector4D clip = mvp * QVector4D(wx, wy, wz, 1.0f);
|
||
if (std::fabs(clip.w()) < 1e-9f) {
|
||
continue;
|
||
}
|
||
const float ndcX = clip.x() / clip.w();
|
||
const float ndcY = clip.y() / clip.w();
|
||
const float sx = viewport[0] + (ndcX + 1.0f) * 0.5f * viewport[2];
|
||
const float sy = viewport[1] + (ndcY + 1.0f) * 0.5f * viewport[3];
|
||
|
||
// 计算屏幕距离
|
||
const float dx = sx - screenX;
|
||
const float dy = sy - glScreenY;
|
||
const float screenDist = dx * dx + dy * dy;
|
||
|
||
if (screenDist < minScreenDist) {
|
||
minScreenDist = screenDist;
|
||
bestIndex = i / 3;
|
||
bestCloudIndex = static_cast<int>(cloudIdx);
|
||
bestX = wx;
|
||
bestY = wy;
|
||
bestZ = wz;
|
||
|
||
// 获取线索引
|
||
if (cloudData.hasLineInfo && bestIndex < cloudData.lineIndices.size()) {
|
||
bestLineIndex = cloudData.lineIndices[bestIndex];
|
||
} else {
|
||
bestLineIndex = -1;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 屏幕距离阈值(像素),20像素内认为选中
|
||
float threshold = 20.0f * 20.0f;
|
||
if (minScreenDist < threshold) {
|
||
result.valid = true;
|
||
result.index = bestIndex;
|
||
result.cloudIndex = bestCloudIndex;
|
||
result.lineIndex = bestLineIndex;
|
||
result.x = bestX;
|
||
result.y = bestY;
|
||
result.z = bestZ;
|
||
|
||
// 计算点在线中的原始索引
|
||
if (bestLineIndex >= 0 && bestCloudIndex >= 0) {
|
||
const auto& cloudData = m_pointClouds[bestCloudIndex];
|
||
// 使用预计算的线内索引(支持不等长线)
|
||
if (bestIndex < cloudData.pointInLineIndices.size()) {
|
||
result.pointIndexInLine = cloudData.pointInLineIndices[bestIndex];
|
||
}
|
||
}
|
||
|
||
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));
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
void PointCloudGLWidget::drawSelectedPoints()
|
||
{
|
||
QVector<float> positions;
|
||
QVector<float> colors;
|
||
|
||
// 绘制选中的点(橙色)
|
||
for (const auto& pt : m_selectedPoints) {
|
||
if (pt.valid) {
|
||
positions.append(pt.x);
|
||
positions.append(pt.y);
|
||
positions.append(pt.z);
|
||
colors.append(1.0f);
|
||
colors.append(0.5f);
|
||
colors.append(0.0f);
|
||
}
|
||
}
|
||
|
||
// 绘制列表高亮点(蓝色,与选点区分)
|
||
if (m_hasListHighlightPoint) {
|
||
positions.append(m_listHighlightPoint.x());
|
||
positions.append(m_listHighlightPoint.y());
|
||
positions.append(m_listHighlightPoint.z());
|
||
colors.append(0.0f);
|
||
colors.append(0.5f);
|
||
colors.append(1.0f);
|
||
}
|
||
|
||
if (positions.isEmpty()) {
|
||
return;
|
||
}
|
||
drawPrimitives(m_mvp,
|
||
positions.constData(),
|
||
positions.size() / 3,
|
||
GL_POINTS,
|
||
colors.constData(),
|
||
QVector3D(1.0f, 1.0f, 1.0f),
|
||
10.0f,
|
||
1.0f,
|
||
false);
|
||
}
|
||
|
||
void PointCloudGLWidget::drawMeasurementLine()
|
||
{
|
||
if (m_selectedPoints.size() < 2) {
|
||
return;
|
||
}
|
||
|
||
const auto& p1 = m_selectedPoints[0];
|
||
const auto& p2 = m_selectedPoints[1];
|
||
|
||
if (!p1.valid || !p2.valid) {
|
||
return;
|
||
}
|
||
|
||
const float positions[] = {
|
||
p1.x, p1.y, p1.z,
|
||
p2.x, p2.y, p2.z};
|
||
drawPrimitives(m_mvp,
|
||
positions,
|
||
2,
|
||
GL_LINES,
|
||
nullptr,
|
||
QVector3D(0.0f, 1.0f, 0.0f),
|
||
1.0f,
|
||
1.0f,
|
||
false);
|
||
}
|
||
|
||
void PointCloudGLWidget::drawSelectedLine()
|
||
{
|
||
if (!m_selectedLine.valid || m_selectedLine.cloudIndex < 0) {
|
||
return;
|
||
}
|
||
|
||
if (m_selectedLine.cloudIndex >= static_cast<int>(m_pointClouds.size())) {
|
||
return;
|
||
}
|
||
|
||
const auto& cloudData = m_pointClouds[m_selectedLine.cloudIndex];
|
||
if (!cloudData.hasLineInfo) {
|
||
return;
|
||
}
|
||
|
||
// 高亮显示选中线上的所有点(黄色)
|
||
QVector<float> positions;
|
||
|
||
if (m_selectedLine.mode == LineSelectMode::Vertical) {
|
||
// 纵向选线:显示同一条扫描线上的所有点
|
||
for (size_t i = 0; i < cloudData.lineIndices.size(); ++i) {
|
||
if (cloudData.lineIndices[i] == m_selectedLine.lineIndex) {
|
||
const size_t vertIdx = i * 3;
|
||
if (vertIdx + 2 < cloudData.vertices.size()) {
|
||
positions.append(cloudData.vertices[vertIdx]);
|
||
positions.append(cloudData.vertices[vertIdx + 1]);
|
||
positions.append(cloudData.vertices[vertIdx + 2]);
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
// 横向选线:显示所有线的相同原始索引点
|
||
if (cloudData.pointsPerLine > 0 && m_selectedLine.pointIndex >= 0) {
|
||
for (size_t i = 0; i < cloudData.pointInLineIndices.size(); ++i) {
|
||
if (cloudData.pointInLineIndices[i] == m_selectedLine.pointIndex) {
|
||
const size_t vertIdx = i * 3;
|
||
if (vertIdx + 2 < cloudData.vertices.size()) {
|
||
positions.append(cloudData.vertices[vertIdx]);
|
||
positions.append(cloudData.vertices[vertIdx + 1]);
|
||
positions.append(cloudData.vertices[vertIdx + 2]);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (positions.isEmpty()) {
|
||
return;
|
||
}
|
||
drawPrimitives(m_mvp,
|
||
positions.constData(),
|
||
positions.size() / 3,
|
||
GL_POINTS,
|
||
nullptr,
|
||
QVector3D(1.0f, 1.0f, 0.0f),
|
||
3.0f,
|
||
1.0f,
|
||
false);
|
||
}
|
||
|
||
void PointCloudGLWidget::drawAxis()
|
||
{
|
||
// 在右下角绘制坐标系指示器
|
||
// 坐标系定义:X向右,Y向下,Z朝后
|
||
// X轴:红色,指向右
|
||
// Y轴:绿色,指向下
|
||
// Z轴:蓝色,指向后(远离观察者)
|
||
GLint viewport[4];
|
||
glGetIntegerv(GL_VIEWPORT, viewport);
|
||
|
||
int axisSize = 60; // 坐标系区域大小(像素)
|
||
int margin = 10; // 距离边缘的边距
|
||
int axisX = viewport[2] - axisSize - margin; // 右下角 X
|
||
int axisY = margin; // 右下角 Y(OpenGL Y 轴向上)
|
||
|
||
// 正交投影 + 视图变换(等价于原 glOrtho + glTranslatef + glMultMatrixf)
|
||
QMatrix4x4 projection;
|
||
projection.setToIdentity();
|
||
projection.ortho(0, viewport[2], 0, viewport[3], -100, 100);
|
||
|
||
QMatrix4x4 modelView;
|
||
modelView.setToIdentity();
|
||
modelView.translate(axisX + axisSize / 2.0f, axisY + axisSize / 2.0f, 0.0f);
|
||
QMatrix4x4 rotation;
|
||
rotation.rotate(m_rotation);
|
||
modelView *= rotation;
|
||
|
||
const QMatrix4x4 mvp = projection * modelView;
|
||
|
||
const float axisLength = axisSize * 0.4f; // 坐标轴长度
|
||
const float positions[] = {
|
||
0.0f, 0.0f, 0.0f, axisLength, 0.0f, 0.0f, // X 轴 - 红色(向右)
|
||
0.0f, 0.0f, 0.0f, 0.0f, axisLength, 0.0f, // Y 轴 - 绿色(向下)
|
||
0.0f, 0.0f, 0.0f, 0.0f, 0.0f, axisLength // Z 轴 - 蓝色(向后)
|
||
};
|
||
const float colors[] = {
|
||
1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f,
|
||
0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f,
|
||
0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f
|
||
};
|
||
|
||
// 关闭深度测试,确保坐标系始终可见
|
||
drawPrimitives(mvp, positions, 6, GL_LINES, colors,
|
||
QVector3D(1.0f, 1.0f, 1.0f), 1.0f, 1.0f, false);
|
||
}
|
||
|
||
void PointCloudGLWidget::drawAxisLabels()
|
||
{
|
||
GLint viewport[4];
|
||
glGetIntegerv(GL_VIEWPORT, viewport);
|
||
|
||
int axisSize = 60;
|
||
int margin = 10;
|
||
float axisLength = axisSize * 0.4f;
|
||
|
||
// 坐标轴中心在 OpenGL 坐标系中的位置
|
||
float centerGlX = viewport[2] - axisSize / 2.0f - margin;
|
||
float centerGlY = axisSize / 2.0f + margin;
|
||
|
||
// 对各轴端点应用当前视图旋转
|
||
QMatrix4x4 rotMatrix;
|
||
rotMatrix.rotate(m_rotation);
|
||
|
||
QVector3D xEnd = rotMatrix.map(QVector3D(axisLength, 0, 0));
|
||
QVector3D yEnd = rotMatrix.map(QVector3D(0, axisLength, 0));
|
||
QVector3D zEnd = rotMatrix.map(QVector3D(0, 0, axisLength));
|
||
|
||
// 转换为 Qt widget 坐标(Y 轴翻转:OpenGL Y 向上 → Qt Y 向下)
|
||
auto toWidgetPos = [&](const QVector3D& v) -> QPointF {
|
||
float glX = centerGlX + v.x();
|
||
float glY = centerGlY + v.y();
|
||
return QPointF(glX, viewport[3] - glY);
|
||
};
|
||
|
||
QPointF xPos = toWidgetPos(xEnd);
|
||
QPointF yPos = toWidgetPos(yEnd);
|
||
QPointF zPos = toWidgetPos(zEnd);
|
||
|
||
// 使用 QPainter 绘制文字标注
|
||
QPainter painter(this);
|
||
painter.setRenderHint(QPainter::TextAntialiasing);
|
||
QFont font = painter.font();
|
||
font.setBold(true);
|
||
font.setPointSize(14);
|
||
painter.setFont(font);
|
||
|
||
// 文字偏移:向轴方向外侧偏移,避免与轴线重叠
|
||
int offset = 4;
|
||
|
||
painter.setPen(QColor(255, 80, 80)); // 红色
|
||
painter.drawText(xPos.x() + offset, xPos.y() + offset, "X");
|
||
|
||
painter.setPen(QColor(80, 255, 80)); // 绿色
|
||
painter.drawText(yPos.x() + offset, yPos.y() + offset, "Y");
|
||
|
||
painter.setPen(QColor(80, 128, 255)); // 蓝色
|
||
painter.drawText(zPos.x() + offset, zPos.y() + offset, "Z");
|
||
|
||
painter.end();
|
||
}
|
||
|
||
void PointCloudGLWidget::drawSelectionInfoOverlay()
|
||
{
|
||
QVector<SelectedPointInfo> 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) {
|
||
// 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());
|
||
doneCurrent();
|
||
|
||
if (point.valid && point.lineIndex >= 0) {
|
||
if (m_lineSelectMode == LineSelectMode::Vertical) {
|
||
// 纵向选线:选中该点所在的线
|
||
m_selectedLine.valid = true;
|
||
m_selectedLine.cloudIndex = point.cloudIndex;
|
||
m_selectedLine.lineIndex = point.lineIndex;
|
||
m_selectedLine.pointIndex = -1;
|
||
m_selectedLine.mode = LineSelectMode::Vertical;
|
||
|
||
// 计算该线上的点数
|
||
int pointCount = 0;
|
||
if (point.cloudIndex >= 0 && point.cloudIndex < static_cast<int>(m_pointClouds.size())) {
|
||
const auto& cloudData = m_pointClouds[point.cloudIndex];
|
||
for (int idx : cloudData.lineIndices) {
|
||
if (idx == point.lineIndex) {
|
||
pointCount++;
|
||
}
|
||
}
|
||
}
|
||
m_selectedLine.pointCount = pointCount;
|
||
} else {
|
||
// 横向选线:选中所有线的相同索引点
|
||
m_selectedLine.valid = true;
|
||
m_selectedLine.cloudIndex = point.cloudIndex;
|
||
m_selectedLine.lineIndex = -1;
|
||
m_selectedLine.pointIndex = point.pointIndexInLine;
|
||
m_selectedLine.mode = LineSelectMode::Horizontal;
|
||
|
||
// 横向线的点数等于总线数
|
||
if (point.cloudIndex >= 0 && point.cloudIndex < static_cast<int>(m_pointClouds.size())) {
|
||
m_selectedLine.pointCount = m_pointClouds[point.cloudIndex].totalLines;
|
||
}
|
||
}
|
||
|
||
emit lineSelected(m_selectedLine);
|
||
update();
|
||
}
|
||
} else {
|
||
m_leftButtonPressed = true;
|
||
}
|
||
} else if (event->button() == Qt::RightButton) {
|
||
m_rightButtonPressed = true;
|
||
} else if (event->button() == Qt::MiddleButton) {
|
||
m_middleButtonPressed = true;
|
||
}
|
||
}
|
||
|
||
void PointCloudGLWidget::mouseDoubleClickEvent(QMouseEvent* event)
|
||
{
|
||
if (event->button() == Qt::LeftButton) {
|
||
// 双击左键:选点(与 Ctrl+左键 相同逻辑)
|
||
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();
|
||
}
|
||
}
|
||
}
|
||
|
||
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_distance = qMax(m_distance, minimumCameraDistance());
|
||
emit viewAnglesChanged(m_rotationX, m_rotationY, m_rotationZ);
|
||
update();
|
||
} else if (m_middleButtonPressed) {
|
||
float factor = m_distance * 0.002f;
|
||
m_pan.setX(m_pan.x() - delta.x() * factor);
|
||
m_pan.setY(m_pan.y() + delta.y() * factor);
|
||
update();
|
||
} else if (m_rightButtonPressed) {
|
||
// 右键拖动:绕视线方向旋转(滚转)(修正方向)
|
||
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);
|
||
m_distance = qMax(m_distance, minimumCameraDistance());
|
||
emit viewAnglesChanged(m_rotationX, m_rotationY, m_rotationZ);
|
||
update();
|
||
}
|
||
}
|
||
|
||
void PointCloudGLWidget::mouseReleaseEvent(QMouseEvent* event)
|
||
{
|
||
if (event->button() == Qt::LeftButton) {
|
||
m_leftButtonPressed = false;
|
||
} else if (event->button() == Qt::RightButton) {
|
||
m_rightButtonPressed = false;
|
||
} else if (event->button() == Qt::MiddleButton) {
|
||
m_middleButtonPressed = false;
|
||
}
|
||
}
|
||
|
||
void PointCloudGLWidget::wheelEvent(QWheelEvent* event)
|
||
{
|
||
float delta = event->angleDelta().y() / 120.0f;
|
||
m_distance *= (1.0f - delta * 0.1f);
|
||
m_distance = qBound(minimumCameraDistance(), m_distance, 1.0e7f);
|
||
update();
|
||
}
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
bool PointCloudGLWidget::getFirstCloudData(PointCloudXYZ& cloud) const
|
||
{
|
||
if (m_pointClouds.empty()) {
|
||
return false;
|
||
}
|
||
|
||
const auto& cloudData = m_pointClouds[0];
|
||
cloud.clear();
|
||
cloud.reserve(cloudData.vertices.size() / 3);
|
||
|
||
for (size_t i = 0; i < cloudData.vertices.size(); i += 3) {
|
||
Point3D pt;
|
||
pt.x = cloudData.vertices[i];
|
||
pt.y = cloudData.vertices[i + 1];
|
||
pt.z = cloudData.vertices[i + 2];
|
||
|
||
int lineIdx = 0;
|
||
if (cloudData.hasLineInfo && (i / 3) < cloudData.lineIndices.size()) {
|
||
lineIdx = cloudData.lineIndices[i / 3];
|
||
}
|
||
const int pointIdx = ((i / 3) < cloudData.pointInLineIndices.size())
|
||
? cloudData.pointInLineIndices[i / 3] : static_cast<int>(i / 3);
|
||
cloud.push_back(pt, lineIdx, pointIdx);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
void PointCloudGLWidget::replaceFirstCloud(const PointCloudXYZ& cloud, const QString& name)
|
||
{
|
||
if (m_pointClouds.empty()) {
|
||
addPointCloud(cloud, name);
|
||
return;
|
||
}
|
||
|
||
// 保留第一个点云的颜色索引
|
||
int colorIndex = m_pointClouds[0].colorIndex;
|
||
|
||
// 重新构建数据
|
||
PointCloudData data;
|
||
data.name = name;
|
||
data.hasColor = false;
|
||
data.hasLineInfo = !cloud.lineIndices.empty();
|
||
data.colorIndex = colorIndex;
|
||
data.vertices.reserve(cloud.size() * 3);
|
||
data.totalLines = 0;
|
||
data.pointsPerLine = 0;
|
||
|
||
const float EPSILON = 1e-6f;
|
||
int previousLineIndex = -1;
|
||
int fallbackPointIndex = 0;
|
||
int maxPointCount = 0;
|
||
for (size_t i = 0; i < cloud.points.size(); ++i) {
|
||
const auto& pt = cloud.points[i];
|
||
const int lineIdx = (i < cloud.lineIndices.size()) ? cloud.lineIndices[i] : -1;
|
||
if (lineIdx != previousLineIndex) {
|
||
fallbackPointIndex = 0;
|
||
previousLineIndex = lineIdx;
|
||
}
|
||
int pointIdx = (i < cloud.pointInLineIndices.size())
|
||
? cloud.pointInLineIndices[i] : -1;
|
||
if (pointIdx < 0) {
|
||
pointIdx = fallbackPointIndex;
|
||
}
|
||
maxPointCount = qMax(maxPointCount, pointIdx + 1);
|
||
if (!std::isfinite(pt.x) || !std::isfinite(pt.y) || !std::isfinite(pt.z)) {
|
||
++fallbackPointIndex;
|
||
continue;
|
||
}
|
||
// 显示时过滤 (0,0,0) 点
|
||
if (std::fabs(pt.x) < EPSILON && std::fabs(pt.y) < EPSILON && std::fabs(pt.z) < EPSILON) {
|
||
++fallbackPointIndex;
|
||
continue;
|
||
}
|
||
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<int>(i));
|
||
data.pointInLineIndices.push_back(pointIdx);
|
||
|
||
if (data.hasLineInfo) {
|
||
// 与顶点保持一一对应;缺失的来源线号以 -1 表示。
|
||
data.lineIndices.push_back(lineIdx);
|
||
if (lineIdx >= 0 && lineIdx + 1 > data.totalLines) {
|
||
data.totalLines = lineIdx + 1;
|
||
}
|
||
}
|
||
++fallbackPointIndex;
|
||
}
|
||
|
||
// 计算每线点数
|
||
if (data.totalLines > 0) {
|
||
data.pointsPerLine = maxPointCount;
|
||
}
|
||
|
||
// 释放旧的 VBO
|
||
makeCurrent();
|
||
releaseVBO(m_pointClouds[0]);
|
||
|
||
m_pointClouds[0] = std::move(data);
|
||
|
||
// 上传新的 VBO
|
||
uploadToVBO(m_pointClouds[0]);
|
||
doneCurrent();
|
||
|
||
computeBoundingBox();
|
||
updatePseudoColors();
|
||
resetView();
|
||
update();
|
||
}
|
||
|
||
void PointCloudGLWidget::addLineSegments(const QVector<LineSegment>& segments)
|
||
{
|
||
m_lineSegments.append(segments);
|
||
update();
|
||
}
|
||
|
||
void PointCloudGLWidget::clearLineSegments()
|
||
{
|
||
m_lineSegments.clear();
|
||
update();
|
||
}
|
||
|
||
void PointCloudGLWidget::setBasicShapeSegments(const QVector<LineSegment>& segments)
|
||
{
|
||
m_basicShapeSegments = segments;
|
||
update();
|
||
}
|
||
|
||
void PointCloudGLWidget::appendBasicShapeSegments(const QVector<LineSegment>& segments)
|
||
{
|
||
if (segments.isEmpty()) {
|
||
return;
|
||
}
|
||
|
||
for (const LineSegment& segment : segments) {
|
||
m_basicShapeSegments.append(segment);
|
||
}
|
||
update();
|
||
}
|
||
|
||
void PointCloudGLWidget::appendBasicShapeSurfaces(const QVector<SurfaceQuad>& surfaces)
|
||
{
|
||
if (surfaces.isEmpty()) {
|
||
return;
|
||
}
|
||
|
||
for (const SurfaceQuad& surface : surfaces) {
|
||
m_basicShapeSurfaces.append(surface);
|
||
}
|
||
update();
|
||
}
|
||
|
||
void PointCloudGLWidget::appendBasicShapePoints(const QVector<BasicShapePoint>& points)
|
||
{
|
||
if (points.isEmpty()) {
|
||
return;
|
||
}
|
||
|
||
for (const BasicShapePoint& point : points) {
|
||
m_basicShapePoints.append(point);
|
||
}
|
||
update();
|
||
}
|
||
|
||
void PointCloudGLWidget::clearBasicShapeSegments()
|
||
{
|
||
m_basicShapeSegments.clear();
|
||
m_basicShapeSurfaces.clear();
|
||
m_basicShapePoints.clear();
|
||
update();
|
||
}
|
||
|
||
void PointCloudGLWidget::addPosePoints(const QVector<PosePoint>& poses)
|
||
{
|
||
m_posePoints.append(poses);
|
||
computeBoundingBox();
|
||
update();
|
||
}
|
||
|
||
void PointCloudGLWidget::clearPosePoints()
|
||
{
|
||
m_posePoints.clear();
|
||
update();
|
||
}
|
||
|
||
void PointCloudGLWidget::drawLineSegments()
|
||
{
|
||
drawLineSegmentList(m_lineSegments);
|
||
}
|
||
|
||
void PointCloudGLWidget::drawBasicShapeSurfaces()
|
||
{
|
||
if (m_basicShapeSurfaces.isEmpty()) {
|
||
return;
|
||
}
|
||
|
||
glEnable(GL_BLEND);
|
||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||
glDepthMask(GL_FALSE);
|
||
|
||
for (const SurfaceQuad& surface : m_basicShapeSurfaces) {
|
||
// GLES2 无 GL_QUADS,四边形拆成两个三角形 (p1,p2,p3) 和 (p1,p3,p4)
|
||
const float positions[] = {
|
||
surface.p1.x(), surface.p1.y(), surface.p1.z(),
|
||
surface.p2.x(), surface.p2.y(), surface.p2.z(),
|
||
surface.p3.x(), surface.p3.y(), surface.p3.z(),
|
||
surface.p1.x(), surface.p1.y(), surface.p1.z(),
|
||
surface.p3.x(), surface.p3.y(), surface.p3.z(),
|
||
surface.p4.x(), surface.p4.y(), surface.p4.z(),
|
||
};
|
||
drawPrimitives(m_mvp, positions, 6, GL_TRIANGLES, nullptr,
|
||
QVector3D(surface.r, surface.g, surface.b),
|
||
1.0f, surface.a, true);
|
||
}
|
||
|
||
glDepthMask(GL_TRUE);
|
||
glDisable(GL_BLEND);
|
||
}
|
||
|
||
void PointCloudGLWidget::drawBasicShapeSegments()
|
||
{
|
||
drawLineSegmentList(m_basicShapeSegments);
|
||
}
|
||
|
||
void PointCloudGLWidget::drawBasicShapePoints()
|
||
{
|
||
if (m_basicShapePoints.isEmpty()) {
|
||
return;
|
||
}
|
||
|
||
for (const BasicShapePoint& point : m_basicShapePoints) {
|
||
const float positions[] = {point.x, point.y, point.z};
|
||
drawPrimitives(m_mvp, positions, 1, GL_POINTS, nullptr,
|
||
QVector3D(point.r, point.g, point.b),
|
||
point.pointSize, 1.0f, false);
|
||
}
|
||
}
|
||
|
||
void PointCloudGLWidget::drawLineSegmentList(const QVector<LineSegment>& segments)
|
||
{
|
||
if (segments.isEmpty()) {
|
||
return;
|
||
}
|
||
|
||
// 按线宽分组绘制
|
||
// 收集所有不同的线宽值(0 表示默认 2.0f)
|
||
std::map<float, QVector<int>> widthGroups;
|
||
for (int i = 0; i < segments.size(); ++i) {
|
||
const float w = segments[i].lineWidth > 0 ? segments[i].lineWidth : 2.0f;
|
||
widthGroups[w].append(i);
|
||
}
|
||
|
||
for (const auto& group : widthGroups) {
|
||
QVector<float> positions;
|
||
QVector<float> colors;
|
||
positions.reserve(group.second.size() * 6);
|
||
colors.reserve(group.second.size() * 6);
|
||
for (int idx : group.second) {
|
||
const LineSegment& seg = segments[idx];
|
||
positions.append(seg.x1); positions.append(seg.y1); positions.append(seg.z1);
|
||
positions.append(seg.x2); positions.append(seg.y2); positions.append(seg.z2);
|
||
colors.append(seg.r); colors.append(seg.g); colors.append(seg.b);
|
||
colors.append(seg.r); colors.append(seg.g); colors.append(seg.b);
|
||
}
|
||
if (positions.isEmpty()) {
|
||
continue;
|
||
}
|
||
// GLES2 下 glLineWidth 有效宽度常受限(部分 Mali 仅支持 1.0),
|
||
// 仍按原线宽设置,驱动不支持时自动退化为最接近的可用宽度。
|
||
glLineWidth(group.first);
|
||
drawPrimitives(m_mvp, positions.constData(), positions.size() / 3,
|
||
GL_LINES, colors.constData(),
|
||
QVector3D(1.0f, 1.0f, 1.0f), 1.0f, 1.0f, true);
|
||
}
|
||
|
||
glLineWidth(1.0f);
|
||
}
|
||
|
||
void PointCloudGLWidget::drawPosePoints()
|
||
{
|
||
if (m_posePoints.isEmpty())
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 从视图矩阵取相机右/上方向,把轴线拉成面向屏幕的窄带,避免 GLES2 线宽被钳成 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:
|
||
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:
|
||
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:
|
||
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:
|
||
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:
|
||
poseLocal.rotate(pose.rx, 1, 0, 0);
|
||
poseLocal.rotate(pose.rz, 0, 0, 1);
|
||
poseLocal.rotate(pose.ry, 0, 1, 0);
|
||
break;
|
||
}
|
||
};
|
||
|
||
QVector<float> positions;
|
||
QVector<float> colors;
|
||
positions.reserve(m_posePoints.size() * 18);
|
||
colors.reserve(m_posePoints.size() * 18);
|
||
|
||
const auto appendAxis = [&](const QVector3D& start,
|
||
const QVector3D& end,
|
||
const QVector3D& color)
|
||
{
|
||
positions.append(start.x());
|
||
positions.append(start.y());
|
||
positions.append(start.z());
|
||
positions.append(end.x());
|
||
positions.append(end.y());
|
||
positions.append(end.z());
|
||
for (int i = 0; i < 2; ++i)
|
||
{
|
||
colors.append(color.x());
|
||
colors.append(color.y());
|
||
colors.append(color.z());
|
||
}
|
||
};
|
||
|
||
for (const PosePoint& 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));
|
||
appendAxis(origin, xEnd, QVector3D(1.0f, 0.0f, 0.0f));
|
||
appendAxis(origin, yEnd, QVector3D(0.0f, 1.0f, 0.0f));
|
||
appendAxis(origin, zEnd, QVector3D(0.0f, 0.0f, 1.0f));
|
||
|
||
}
|
||
drawPrimitives(m_mvp,
|
||
positions.constData(),
|
||
positions.size() / 3,
|
||
GL_LINES,
|
||
colors.constData(),
|
||
QVector3D(1.0f, 1.0f, 1.0f),
|
||
1.0f,
|
||
1.0f,
|
||
false);
|
||
}
|