refactor: 重构项目构建系统与点云查看器功能

1.  新增CloudUtils静态库与相关CMake配置
2.  重构VrUtils构建脚本,修正编译选项与依赖
3.  为Windows平台添加utf-8编译选项
4.  扩展PointCloudGLWidget功能:新增点云移除、选择信息叠加、增量相机适配
5.  优化点云添加接口,支持不重置相机视图
6.  修正激光数据保存时的DataType输出
7.  为Point3D结构体添加单位注释
8.  调整鼠标交互逻辑,统一按键操作行为
This commit is contained in:
杰仔 2026-08-18 12:26:28 +08:00
parent 44180b7f85
commit 9271a37a81
8 changed files with 427 additions and 185 deletions

View File

@ -1,16 +1,23 @@
cmake_minimum_required(VERSION 3.14)
project(WoodCamUtils LANGUAGES CXX)
project(WoodCamUtils LANGUAGES C CXX)
option(WOODCAM_BUILD_CLOUDVIEW3D "Build the Qt/OpenGL CloudView3D utility" ON)
option(WOODCAM_BUILD_CLOUDUTILS "Build the CloudUtils static library" ON)
# VrCommon is a header-oriented compatibility module. Keep it as an interface
# target so consumers get the canonical VZNL_Types.h include path.
add_library(VrCommon INTERFACE)
target_include_directories(VrCommon INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/VrCommon/Inc
)
if(NOT TARGET VrCommon)
add_library(VrCommon INTERFACE)
target_include_directories(VrCommon INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/VrCommon/Inc
)
endif()
if(WOODCAM_BUILD_CLOUDVIEW3D)
add_subdirectory(CloudView3D)
endif()
if(WOODCAM_BUILD_CLOUDUTILS)
add_subdirectory(CloudUtils)
endif()

46
CloudUtils/CMakeLists.txt Normal file
View File

@ -0,0 +1,46 @@
cmake_minimum_required(VERSION 3.14)
find_package(Qt5 REQUIRED COMPONENTS Core Gui)
if(NOT TARGET VrCommon)
add_library(VrCommon INTERFACE)
target_include_directories(VrCommon INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/../VrCommon/Inc
)
endif()
if(NOT TARGET VrUtils)
add_subdirectory(
"${CMAKE_CURRENT_SOURCE_DIR}/../VrUtils"
"${CMAKE_CURRENT_BINARY_DIR}/VrUtils"
)
endif()
add_library(CloudUtils STATIC
Src/LaserDataLoader.cpp
Src/PointCloudImageUtils.cpp
Inc/LaserDataLoader.h
Inc/PointCloudImageUtils.h
)
set_target_properties(CloudUtils PROPERTIES
CXX_STANDARD 11
CXX_STANDARD_REQUIRED ON
)
target_include_directories(CloudUtils PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/Inc
)
target_link_libraries(CloudUtils
PUBLIC
VrCommon
Qt5::Core
Qt5::Gui
PRIVATE
VrUtils
)
if(MSVC)
target_compile_options(CloudUtils PRIVATE /utf-8)
endif()

View File

@ -546,7 +546,7 @@ int LaserDataLoader::SaveLaserScanData(const std::string& fileName,
// 写入文件头
sw << "LineNum:" << lineNum << '\n';
sw << "DataType:" << static_cast<int>(fileDataType) << '\n';
sw << "DataType:" << 0 << '\n';
sw << "ScanSpeed:" << scanSpeed << '\n';
sw << "PointAdjust: 1" << '\n';
sw << "MaxTimeStamp:" << maxTimeStamp << "_" << clockPerSecond << '\n';

View File

@ -36,6 +36,7 @@ target_link_libraries(CloudView3D PUBLIC
)
if(WIN32)
target_compile_options(CloudView3D PRIVATE /utf-8)
target_link_libraries(CloudView3D PUBLIC opengl32 glu32)
elseif(UNIX AND NOT APPLE)
target_link_libraries(CloudView3D PUBLIC GL GLU)

View File

@ -7,14 +7,14 @@
struct Point3D
{
float x, y, z;
float x, y, z; // millimetres
Point3D() : x(0), y(0), z(0) {}
Point3D(float _x, float _y, float _z) : x(_x), y(_y), z(_z) {}
};
struct Point3DRGB
{
float x, y, z;
float x, y, z; // millimetres
uint8_t r, g, b;
float pointSize;

View File

@ -9,8 +9,9 @@
#include <QVector3D>
#include <QQuaternion>
#include <QMouseEvent>
#include <QWheelEvent>
#include <vector>
#include <QWheelEvent>
#include <vector>
#include <deque>
#include "CloudView3DTypes.h"
#include "VZNL_Types.h"
@ -174,10 +175,12 @@ public:
explicit PointCloudGLWidget(QWidget* parent = nullptr);
~PointCloudGLWidget() override;
void addPointCloud(const PointCloudXYZ& cloud, const QString& name = "");
void addPointCloud(const PointCloudXYZRGB& cloud, const QString& name = "");
void appendPointCloud(const PointCloudXYZRGB& cloud, const QString& name = "");
void clearPointClouds();
void addPointCloud(const PointCloudXYZ& cloud, const QString& name = "");
void addPointCloud(const PointCloudXYZRGB& cloud, const QString& name = "",
bool resetCamera = true);
void appendPointCloud(const PointCloudXYZRGB& cloud, const QString& name = "");
void removeOldestPointCloud();
void clearPointClouds();
void setPointCloudColor(PointCloudColor color);
void setPointSize(float size);
void resetView();
@ -370,8 +373,8 @@ protected:
void keyPressEvent(QKeyEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
private:
struct PointCloudData
private:
struct PointCloudData
{
std::vector<float> vertices;
std::vector<float> colors;
@ -460,10 +463,10 @@ private:
// 禁止拷贝
PointCloudData(const PointCloudData&) = delete;
PointCloudData& operator=(const PointCloudData&) = delete;
};
void computeBoundingBox();
PointCloudData& operator=(const PointCloudData&) = delete;
};
void computeBoundingBox();
void updateProjection(const QMatrix4x4& modelView);
float fittedCameraDistance(float padding = 1.05f) const;
float minimumCameraDistance() const;
@ -478,8 +481,9 @@ private:
void drawBasicShapeSegments(); // 绘制基础图形
void drawBasicShapePoints();
void drawLineSegmentList(const QVector<LineSegment>& segments);
void drawPosePoints(); // 绘制姿态点
void drawAxisLabels(); // 绘制坐标轴 XYZ 标注
void drawPosePoints(); // 绘制姿态点
void drawAxisLabels(); // 绘制坐标轴 XYZ 标注
void drawSelectionInfoOverlay(); // 右上角显示选点和测量结果
void uploadToVBO(PointCloudData& data); // 上传数据到 VBO
void releaseVBO(PointCloudData& data); // 释放 VBO 资源
@ -504,7 +508,9 @@ private:
void updateViewAngleDisplay();
bool handlePinchGesture(QPinchGesture* gesture);
std::vector<PointCloudData> m_pointClouds;
// Each entry owns QOpenGLBuffer objects. deque avoids relocating existing
// entries when new scan lines are appended or the oldest one is removed.
std::deque<PointCloudData> m_pointClouds;
QMatrix4x4 m_projection;
QMatrix4x4 m_view;

View File

@ -1,5 +1,5 @@
#include "PointCloudGLWidget.h"
#include <QDebug>
#include <QDebug>
#include <QtMath>
#include <QDialog>
#include <QDialogButtonBox>
@ -7,22 +7,24 @@
#include <QFormLayout>
#include <QGestureEvent>
#include <QHBoxLayout>
#include <QFontMetrics>
#include <QOpenGLContext>
#include <QPainter>
#include <QPinchGesture>
#include <QPushButton>
#include <QResizeEvent>
#include <QStringList>
#include <QToolButton>
#include <QVBoxLayout>
#include <algorithm>
#include <cfloat>
#include <cmath>
#include <map>
#define CLOUD_LOG_INFO(...) \
qInfo().noquote() << QString::asprintf(__VA_ARGS__).trimmed()
#define CLOUD_LOG_WARN(...) \
qWarning().noquote() << QString::asprintf(__VA_ARGS__).trimmed()
#define CLOUD_LOG_INFO(...) \
qInfo().noquote() << QString::asprintf(__VA_ARGS__).trimmed()
#define CLOUD_LOG_WARN(...) \
qWarning().noquote() << QString::asprintf(__VA_ARGS__).trimmed()
// OpenGL/GLU 头文件
#ifdef _WIN32
@ -410,7 +412,7 @@ void PointCloudGLWidget::initializeGL()
{
initializeOpenGLFunctions();
CLOUD_LOG_INFO("[CloudView] OpenGL initialized, version: %s\n", reinterpret_cast<const char*>(glGetString(GL_VERSION)));
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);
@ -471,19 +473,11 @@ bool PointCloudGLWidget::ensureShader()
"void main() {\n"
" gl_FragColor = vec4(vColor, vAlpha);\n"
"}\n")) {
LOG_ERROR("[CloudView] Shader compile failed: %s\n",
program->log().isEmpty()
? "unknown error"
: qPrintable(program->log()));
delete program;
return false;
}
if (!program->link()) {
LOG_ERROR("[CloudView] Shader link failed: %s\n",
program->log().isEmpty()
? "unknown error"
: qPrintable(program->log()));
delete program;
return false;
}
@ -652,7 +646,7 @@ void PointCloudGLWidget::paintGL()
if (++frameCount % 100 == 0) {
// 从四元数转换为欧拉角用于显示
QVector3D euler = m_rotation.toEulerAngles();
CLOUD_LOG_INFO("[CloudView] Current rotation: pitch=%.1f, yaw=%.1f, roll=%.1f\n",
CLOUD_LOG_INFO("[CloudView] Current rotation: pitch=%.1f, yaw=%.1f, roll=%.1f\n",
euler.x(), euler.y(), euler.z());
}
@ -731,14 +725,15 @@ void PointCloudGLWidget::paintGL()
// 使用 QPainter 绘制坐标轴 XYZ 标注
drawAxisLabels();
drawSelectionInfoOverlay();
}
void PointCloudGLWidget::addPointCloud(const PointCloudXYZ& cloud, const QString& name)
{
CLOUD_LOG_INFO("[CloudView] addPointCloud called, cloud size: %zu\n", cloud.size());
CLOUD_LOG_INFO("[CloudView] addPointCloud called, cloud size: %zu\n", cloud.size());
if (cloud.empty()) {
CLOUD_LOG_WARN("[CloudView] Cloud is empty, returning\n");
CLOUD_LOG_WARN("[CloudView] Cloud is empty, returning\n");
return;
}
@ -802,10 +797,10 @@ void PointCloudGLWidget::addPointCloud(const PointCloudXYZ& cloud, const QString
data.pointsPerLine = static_cast<int>(cloud.size()) / data.totalLines;
}
CLOUD_LOG_INFO("[CloudView] Valid vertices count: %zu, colorIndex: %d, totalLines: %d, pointsPerLine: %d\n",
CLOUD_LOG_INFO("[CloudView] Valid vertices count: %zu, colorIndex: %d, totalLines: %d, pointsPerLine: %d\n",
data.vertices.size() / 3, data.colorIndex, data.totalLines, data.pointsPerLine);
m_pointClouds.push_back(std::move(data));
m_pointClouds.push_back(std::move(data));
// 上传 VBO(如果当前在 GL 上下文中)
makeCurrent();
@ -813,17 +808,18 @@ void PointCloudGLWidget::addPointCloud(const PointCloudXYZ& cloud, const QString
doneCurrent();
computeBoundingBox();
CLOUD_LOG_INFO("[CloudView] BoundingBox min: (%.3f, %.3f, %.3f) max: (%.3f, %.3f, %.3f)\n",
CLOUD_LOG_INFO("[CloudView] BoundingBox min: (%.3f, %.3f, %.3f) max: (%.3f, %.3f, %.3f)\n",
m_minBound.x(), m_minBound.y(), m_minBound.z(),
m_maxBound.x(), m_maxBound.y(), m_maxBound.z());
CLOUD_LOG_INFO("[CloudView] Center: (%.3f, %.3f, %.3f) Distance: %.3f\n",
CLOUD_LOG_INFO("[CloudView] Center: (%.3f, %.3f, %.3f) Distance: %.3f\n",
m_center.x(), m_center.y(), m_center.z(), m_distance);
resetView();
update();
}
void PointCloudGLWidget::addPointCloud(const PointCloudXYZRGB& cloud, const QString& name)
void PointCloudGLWidget::addPointCloud(const PointCloudXYZRGB& cloud, const QString& name,
bool resetCamera)
{
if (cloud.empty()) {
return;
@ -913,7 +909,7 @@ void PointCloudGLWidget::addPointCloud(const PointCloudXYZRGB& cloud, const QStr
group.indices = std::move(pair.second);
data.customPointSizeGroups.push_back(std::move(group));
}
CLOUD_LOG_INFO("[CloudView] Found %zu custom point size groups\n", data.customPointSizeGroups.size());
CLOUD_LOG_INFO("[CloudView] Found %zu custom point size groups\n", data.customPointSizeGroups.size());
}
// 计算每线点数
@ -921,17 +917,27 @@ void PointCloudGLWidget::addPointCloud(const PointCloudXYZRGB& cloud, const QStr
data.pointsPerLine = static_cast<int>(cloud.size()) / data.totalLines;
}
m_pointClouds.push_back(std::move(data));
m_pointClouds.push_back(std::move(data));
// 上传 VBO
makeCurrent();
uploadToVBO(m_pointClouds.back());
doneCurrent();
computeBoundingBox();
resetView();
update();
}
doneCurrent();
computeBoundingBox();
if (resetCamera) {
resetView();
} else {
// A line-scan cloud grows along X over time. Keep the user's view
// orientation/pan, but expand the camera distance when the retained
// cloud no longer fits in the frustum.
const float fittedDistance = fittedCameraDistance();
if (std::isfinite(fittedDistance)) {
m_distance = qMax(m_distance, fittedDistance);
}
}
update();
}
void PointCloudGLWidget::appendPointCloud(const PointCloudXYZRGB& cloud,
const QString& name)
@ -1016,16 +1022,19 @@ void PointCloudGLWidget::appendPointCloud(const PointCloudXYZRGB& cloud,
static_cast<int>(data.vertices.size() / 3) / data.totalLines;
}
makeCurrent();
uploadToVBO(data);
doneCurrent();
makeCurrent();
uploadToVBO(data);
doneCurrent();
computeBoundingBox();
const float fittedDistance = fittedCameraDistance();
if (std::isfinite(fittedDistance)) {
m_distance = qMax(m_distance, fittedDistance);
}
update();
}
computeBoundingBox();
m_distance = qMax(m_distance, fittedCameraDistance());
update();
}
void PointCloudGLWidget::clearPointClouds()
void PointCloudGLWidget::clearPointClouds()
{
// 释放所有 VBO
makeCurrent();
@ -1047,8 +1056,25 @@ void PointCloudGLWidget::clearPointClouds()
m_center = QVector3D(0, 0, 0);
m_colorIndex = 0; // 重置颜色索引
resetView();
update();
}
update();
}
void PointCloudGLWidget::removeOldestPointCloud()
{
if (m_pointClouds.empty()) {
return;
}
// deque::pop_front does not relocate the remaining PointCloudData objects.
makeCurrent();
releaseVBO(m_pointClouds.front());
doneCurrent();
m_pointClouds.pop_front();
m_selectedLine = SelectedLineInfo();
computeBoundingBox();
update();
}
void PointCloudGLWidget::setPointCloudColor(PointCloudColor color)
{
@ -1076,7 +1102,7 @@ void PointCloudGLWidget::resetView()
m_pan = QVector3D(0, 0, 0);
m_distance = fittedCameraDistance();
CLOUD_LOG_INFO("[CloudView] resetView: size=(%.3f, %.3f, %.3f) maxSize=%.3f distance=%.3f\n",
CLOUD_LOG_INFO("[CloudView] resetView: size=(%.3f, %.3f, %.3f) maxSize=%.3f distance=%.3f\n",
size.x(), size.y(), size.z(), maxSize, m_distance);
emit viewAnglesChanged(m_rotationX, m_rotationY, m_rotationZ);
@ -1110,18 +1136,36 @@ void PointCloudGLWidget::updateProjection(const QMatrix4x4& modelView)
maximumDepth = qMax(maximumDepth, depth);
}
constexpr float kMinimumNearPlane = 0.01f;
float nearPlane = kMinimumNearPlane;
float farPlane = 1.0f;
if (maximumDepth > kMinimumNearPlane) {
constexpr float kMinimumNearPlane = 0.01f;
float nearPlane = kMinimumNearPlane;
float farPlane = 1.0f;
const float boundsDiagonal = (m_maxBound - m_minBound).length();
const float safeDiagonal = std::isfinite(boundsDiagonal)
? qMax(boundsDiagonal, 1.0f)
: 1.0f;
const float safeCameraDistance = std::isfinite(m_distance)
? qMax(m_distance, 1.0f)
: 1.0f;
if (std::isfinite(minimumDepth) && std::isfinite(maximumDepth) &&
maximumDepth > kMinimumNearPlane) {
// 点云贴近或跨过相机时保持很小的近裁剪距离,避免前侧点被裁掉。
if (minimumDepth > kMinimumNearPlane) {
nearPlane = qMax(kMinimumNearPlane, minimumDepth * 0.5f);
}
// 远裁剪面仅覆盖当前内容,并限制 near/far 比值以保证深度缓冲精度。
farPlane = qMax(nearPlane + 1.0f, maximumDepth * 1.5f);
nearPlane = qMax(nearPlane, farPlane / 100000.0f);
}
farPlane = qMax(nearPlane + 1.0f, maximumDepth * 1.5f);
nearPlane = qMax(nearPlane, farPlane / 100000.0f);
} else {
// The scan range can cross the current camera plane while the view
// is being updated. Keep a conservative frustum instead of letting
// all points fall outside a near/far clip interval.
nearPlane = kMinimumNearPlane;
farPlane = safeCameraDistance + safeDiagonal * 2.0f;
}
if (!std::isfinite(farPlane) || farPlane <= nearPlane) {
farPlane = nearPlane + safeCameraDistance + safeDiagonal * 2.0f;
}
const float aspect = static_cast<float>(width()) /
static_cast<float>(height() > 0 ? height() : 1);
@ -1209,7 +1253,7 @@ void PointCloudGLWidget::setViewAngles(float rotX, float rotY, float rotZ)
m_rotation = QQuaternion::fromEulerAngles(m_rotationX, m_rotationY, m_rotationZ);
m_distance = qMax(m_distance, minimumCameraDistance());
CLOUD_LOG_INFO("[CloudView] setViewAngles: rotX=%.1f, rotY=%.1f, rotZ=%.1f\n", rotX, rotY, rotZ);
CLOUD_LOG_INFO("[CloudView] setViewAngles: rotX=%.1f, rotY=%.1f, rotZ=%.1f\n", rotX, rotY, rotZ);
emit viewAnglesChanged(m_rotationX, m_rotationY, m_rotationZ);
update();
@ -1325,7 +1369,7 @@ void PointCloudGLWidget::updateSelectedPointCoord(int index, float x, float y, f
m_selectedPoints[index].y = y;
m_selectedPoints[index].z = z;
CLOUD_LOG_INFO("[CloudView] Updated selected point %d to (%.3f, %.3f, %.3f)\n", index, x, y, z);
CLOUD_LOG_INFO("[CloudView] Updated selected point %d to (%.3f, %.3f, %.3f)\n", index, x, y, z);
// 刷新显示
update();
@ -1350,7 +1394,7 @@ void PointCloudGLWidget::setSelectedPointCoord(int index, float x, float y, floa
m_selectedPoints[index].lineIndex = -1;
m_selectedPoints[index].pointIndexInLine = -1;
CLOUD_LOG_INFO("[CloudView] Set selected point %d to (%.3f, %.3f, %.3f)\n", index, x, y, z);
CLOUD_LOG_INFO("[CloudView] Set selected point %d to (%.3f, %.3f, %.3f)\n", index, x, y, z);
// 刷新显示
update();
@ -1683,10 +1727,10 @@ SelectedPointInfo PointCloudGLWidget::pickPoint(int screenX, int screenY)
}
}
CLOUD_LOG_INFO("[CloudView] Point selected: (%.3f, %.3f, %.3f) lineIndex=%d pointIndexInLine=%d screenDist=%.1f\n",
CLOUD_LOG_INFO("[CloudView] Point selected: (%.3f, %.3f, %.3f) lineIndex=%d pointIndexInLine=%d screenDist=%.1f\n",
bestX, bestY, bestZ, bestLineIndex, result.pointIndexInLine, std::sqrt(minScreenDist));
} else {
CLOUD_LOG_INFO("[CloudView] No point selected, minScreenDist=%.1f\n", std::sqrt(minScreenDist));
CLOUD_LOG_INFO("[CloudView] No point selected, minScreenDist=%.1f\n", std::sqrt(minScreenDist));
}
return result;
@ -1923,45 +1967,110 @@ void PointCloudGLWidget::drawAxisLabels()
painter.end();
}
void PointCloudGLWidget::drawSelectionInfoOverlay()
{
QVector<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) {
// Ctrl+左键:选点
if (event->modifiers() & Qt::ControlModifier) {
makeCurrent();
SelectedPointInfo point = pickPoint(event->pos().x(), event->pos().y());
doneCurrent();
if (point.valid) {
if (m_measureDistanceEnabled) {
// 启用测距:最多保留两个点
if (m_selectedPoints.size() >= MAX_SELECTED_POINTS) {
m_selectedPoints.clear();
}
m_selectedPoints.append(point);
emit pointSelected(point);
if (m_selectedPoints.size() == 2) {
const float distance = calculateDistance(m_selectedPoints[0], m_selectedPoints[1]);
const float dx = m_selectedPoints[1].x - m_selectedPoints[0].x;
const float dy = m_selectedPoints[1].y - m_selectedPoints[0].y;
const float dz = m_selectedPoints[1].z - m_selectedPoints[0].z;
emit twoPointsSelected(m_selectedPoints[0], m_selectedPoints[1], distance);
emit measurementFinished(m_selectedPoints[0], m_selectedPoints[1], distance, dx, dy, dz);
}
} else {
// 未启用测距:只保留一个点
m_selectedPoints.clear();
m_selectedPoints.append(point);
emit pointSelected(point);
}
update();
}
} else if (event->modifiers() & Qt::ShiftModifier) {
m_lastMousePos = event->pos();
if (event->button() == Qt::LeftButton) {
// Alt+左键:依次选择两个点并测距
if (event->modifiers() & Qt::AltModifier) {
makeCurrent();
SelectedPointInfo point = pickPoint(event->pos().x(), event->pos().y());
doneCurrent();
if (point.valid) {
if (m_selectedPoints.size() >= MAX_SELECTED_POINTS) {
m_selectedPoints.clear();
}
m_selectedPoints.append(point);
emit pointSelected(point);
if (m_selectedPoints.size() == 2) {
const float distance = calculateDistance(m_selectedPoints[0], m_selectedPoints[1]);
const float dx = m_selectedPoints[1].x - m_selectedPoints[0].x;
const float dy = m_selectedPoints[1].y - m_selectedPoints[0].y;
const float dz = m_selectedPoints[1].z - m_selectedPoints[0].z;
emit twoPointsSelected(m_selectedPoints[0], m_selectedPoints[1], distance);
emit measurementFinished(m_selectedPoints[0], m_selectedPoints[1], distance, dx, dy, dz);
}
update();
}
} else if (event->modifiers() & Qt::ControlModifier) {
// Ctrl+左键:选择一个点
makeCurrent();
SelectedPointInfo point = pickPoint(event->pos().x(), event->pos().y());
doneCurrent();
if (point.valid) {
m_selectedPoints.clear();
m_selectedPoints.append(point);
emit pointSelected(point);
update();
}
} else if (event->modifiers() & Qt::ShiftModifier) {
// Shift+左键:选线
makeCurrent();
SelectedPointInfo point = pickPoint(event->pos().x(), event->pos().y());
@ -2055,38 +2164,29 @@ void PointCloudGLWidget::mouseDoubleClickEvent(QMouseEvent* event)
void PointCloudGLWidget::mouseMoveEvent(QMouseEvent* event)
{
QPoint delta = event->pos() - m_lastMousePos;
m_lastMousePos = event->pos();
if (m_leftButtonPressed) {
// Alt+左键拖动:绕视线方向旋转(滚转)
if (event->modifiers() & Qt::AltModifier) {
// 绕Z轴(视线方向)旋转(修正方向)
float angle = -delta.x() * 0.5f; // 取反修正方向
QQuaternion deltaRotation = QQuaternion::fromAxisAndAngle(QVector3D(0, 0, 1), angle);
m_rotation = deltaRotation * m_rotation;
m_rotationZ = NormalizeAngleDegrees(m_rotationZ + angle);
} else {
// 普通左键拖动:基于当前物体坐标系的旋转
// 鼠标水平移动 -> 绕当前Y轴旋转
// 鼠标垂直移动 -> 绕当前X轴旋转
// 计算旋转角度
float angleX = delta.y() * 0.5f; // 垂直移动
float angleY = delta.x() * 0.5f; // 水平移动
// 创建增量旋转四元数(在相机空间中)
// 先绕X轴旋转(俯仰),再绕Y轴旋转(偏航)
QQuaternion deltaRotationX = QQuaternion::fromAxisAndAngle(QVector3D(1, 0, 0), angleX);
QQuaternion deltaRotationY = QQuaternion::fromAxisAndAngle(QVector3D(0, 1, 0), angleY);
QQuaternion deltaRotation = deltaRotationY * deltaRotationX;
// 应用增量旋转(左乘,相机空间旋转)
m_rotation = deltaRotation * m_rotation;
// 更新欧拉角(用于显示),归一化到 ±180°
m_rotationX = NormalizeAngleDegrees(m_rotationX + angleX);
m_rotationY = NormalizeAngleDegrees(m_rotationY + angleY);
}
m_lastMousePos = event->pos();
if (m_leftButtonPressed) {
// 左键拖动:基于当前物体坐标系的旋转
// 鼠标水平移动 -> 绕当前Y轴旋转
// 鼠标垂直移动 -> 绕当前X轴旋转
// 计算旋转角度
float angleX = delta.y() * 0.5f; // 垂直移动
float angleY = delta.x() * 0.5f; // 水平移动
// 创建增量旋转四元数(在相机空间中)
// 先绕X轴旋转(俯仰),再绕Y轴旋转(偏航)
QQuaternion deltaRotationX = QQuaternion::fromAxisAndAngle(QVector3D(1, 0, 0), angleX);
QQuaternion deltaRotationY = QQuaternion::fromAxisAndAngle(QVector3D(0, 1, 0), angleY);
QQuaternion deltaRotation = deltaRotationY * deltaRotationX;
// 应用增量旋转(左乘,相机空间旋转)
m_rotation = deltaRotation * m_rotation;
// 更新欧拉角(用于显示),归一化到 ±180°
m_rotationX = NormalizeAngleDegrees(m_rotationX + angleX);
m_rotationY = NormalizeAngleDegrees(m_rotationY + angleY);
// 发送角度变化信号
m_distance = qMax(m_distance, minimumCameraDistance());
@ -2128,10 +2228,14 @@ void PointCloudGLWidget::wheelEvent(QWheelEvent* event)
update();
}
void PointCloudGLWidget::keyPressEvent(QKeyEvent* event)
{
if (event->key() == Qt::Key_Space) {
resetView();
void PointCloudGLWidget::keyPressEvent(QKeyEvent* event)
{
if (event->key() == Qt::Key_Escape) {
m_selectedPoints.clear();
update();
event->accept();
} else if (event->key() == Qt::Key_Space) {
resetView();
} else {
QOpenGLWidget::keyPressEvent(event);
}

View File

@ -1,34 +1,112 @@
ADD_DEFINITIONS(-D__OPENVMS__) #log4cpp 编译选项
if(NOT WIN32)
ADD_DEFINITIONS(-DLOG4CPP_HAVE_GETTIMEOFDAY) #ARM/Linux 上直接启用 gettimeofday,确保毫秒正确
endif()
ADD_DEFINITIONS(-DVR_UTILS_EXPORTS) #VrUtils 导出宏定义
# ADD_COMPILE_OPTIONS(-Wno-deprecated-declarations) #auto_ptr警告消除
# 设置CMake策略,允许AUTOMOC和AUTOUIC处理.hh文件
# cmake_policy(SET CMP0100 NEW)
INCLUDE_DIRECTORIES(
./_Inc
./Inc
./MD5
./tinyxml2
./log4cpp/include
)
AUX_SOURCE_DIRECTORY(./Src SrcS)
AUX_SOURCE_DIRECTORY(./MD5 MD5SrcS)
AUX_SOURCE_DIRECTORY(./tinyxml2 TinyxmlSrcS)
AUX_SOURCE_DIRECTORY(./log4cpp/src LogSrcS)
# 对特定的.hh文件设置SKIP_AUTOGEN属性
set_property(SOURCE log4cpp/src/StringUtil.hh PROPERTY SKIP_AUTOGEN ON)
set_property(SOURCE log4cpp/src/PortabilityImpl.hh PROPERTY SKIP_AUTOGEN ON)
set_property(SOURCE log4cpp/src/Properties.hh PROPERTY SKIP_AUTOGEN ON)
set_property(SOURCE log4cpp/src/Localtime.hh PROPERTY SKIP_AUTOGEN ON)
set_property(SOURCE log4cpp/src/PropertyConfiguratorImpl.hh PROPERTY SKIP_AUTOGEN ON)
ADD_LIBRARY(VrUtils STATIC ${SrcS} ${MD5SrcS}
${TinyxmlSrcS}
${LogSrcS}
)
cmake_minimum_required(VERSION 3.14)
# Keep this source list aligned with VrUtils.pro. Do not glob Src/: it contains
# legacy standalone sources such as getopt.c that are not part of VrUtils.
set(VRUTILS_SOURCES
Src/VrDateUtils.cpp
Src/VrFileUtils.cpp
Src/VrLog.cpp
Src/VrNetUtils.cpp
Src/VrStringUtils.cpp
Src/VrTimeUtils.cpp
ini/ConvertUTF.c
log4cpp/src/AbortAppender.cpp
log4cpp/src/Appender.cpp
log4cpp/src/AppenderSkeleton.cpp
log4cpp/src/AppendersFactory.cpp
log4cpp/src/BasicConfigurator.cpp
log4cpp/src/BasicLayout.cpp
log4cpp/src/BufferingAppender.cpp
log4cpp/src/Category.cpp
log4cpp/src/CategoryStream.cpp
log4cpp/src/Configurator.cpp
log4cpp/src/DailyRollingFileAppender.cpp
log4cpp/src/DllMain.cpp
log4cpp/src/DummyThreads.cpp
log4cpp/src/FactoryParams.cpp
log4cpp/src/FileAppender.cpp
log4cpp/src/Filter.cpp
log4cpp/src/FixedContextCategory.cpp
log4cpp/src/HierarchyMaintainer.cpp
log4cpp/src/IdsaAppender.cpp
log4cpp/src/LayoutAppender.cpp
log4cpp/src/LayoutsFactory.cpp
log4cpp/src/LevelEvaluator.cpp
log4cpp/src/Localtime.cpp
log4cpp/src/LoggingEvent.cpp
log4cpp/src/MSThreads.cpp
log4cpp/src/Manipulator.cpp
log4cpp/src/NDC.cpp
log4cpp/src/NTEventLogAppender.cpp
log4cpp/src/OmniThreads.cpp
log4cpp/src/OstreamAppender.cpp
log4cpp/src/PThreads.cpp
log4cpp/src/PassThroughLayout.cpp
log4cpp/src/PatternLayout.cpp
log4cpp/src/PortabilityImpl.cpp
log4cpp/src/Priority.cpp
log4cpp/src/Properties.cpp
log4cpp/src/PropertyConfigurator.cpp
log4cpp/src/PropertyConfiguratorImpl.cpp
log4cpp/src/RemoteSyslogAppender.cpp
log4cpp/src/RollingFileAppender.cpp
log4cpp/src/SimpleConfigurator.cpp
log4cpp/src/SimpleLayout.cpp
log4cpp/src/SmtpAppender.cpp
log4cpp/src/StringQueueAppender.cpp
log4cpp/src/StringUtil.cpp
log4cpp/src/SyslogAppender.cpp
log4cpp/src/TimeStamp.cpp
log4cpp/src/TriggeringEventEvaluatorFactory.cpp
log4cpp/src/Win32DebugAppender.cpp
log4cpp/src/snprintf.c
tinyxml2/tinyxml2.cpp
jsoncpp/json_reader.cpp
jsoncpp/json_value.cpp
jsoncpp/json_writer.cpp
)
add_library(VrUtils STATIC ${VRUTILS_SOURCES})
set_target_properties(VrUtils PROPERTIES
CXX_STANDARD 11
CXX_STANDARD_REQUIRED ON
)
target_include_directories(VrUtils
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/Inc
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/ini
${CMAKE_CURRENT_SOURCE_DIR}/tinyxml2
${CMAKE_CURRENT_SOURCE_DIR}/jsoncpp
${CMAKE_CURRENT_SOURCE_DIR}/log4cpp/include
${CMAKE_CURRENT_SOURCE_DIR}/log4cpp/src
)
target_compile_definitions(VrUtils PRIVATE
TINYXML2_EXPORT
HAVE_SNPRINTF
LOG4CPP_HAVE_SSTREAM=1
VR_UTILS_EXPORTS
)
if(UNIX AND NOT APPLE)
target_compile_definitions(VrUtils PRIVATE LOG4CPP_HAVE_UNISTD_H)
set_target_properties(VrUtils PROPERTIES POSITION_INDEPENDENT_CODE ON)
endif()
find_package(Threads REQUIRED)
target_link_libraries(VrUtils PUBLIC Threads::Threads)
if(WIN32)
# qmake's Windows mkspec supplies these definitions. log4cpp uses the
# generic Win32 API names and therefore must see the same character mode.
target_compile_definitions(VrUtils PRIVATE UNICODE _UNICODE)
target_link_libraries(VrUtils PUBLIC ws2_32 iphlpapi advapi32 shell32)
endif()
if(MSVC)
target_compile_definitions(VrUtils PRIVATE _HAS_AUTO_PTR_ETC=1)
target_compile_options(VrUtils PRIVATE /utf-8)
endif()