LineDrawer.h

#pragma once
#include <QOpenGLFunctions_4_5_Core>
#include <QOpenGLShaderProgram>
#include <QVector3D>
#include <vector>
#include <qdebug.h>

// 定义全局无效点常量(三个分量均为正无穷大)
const QVector3D INVALID_POINT(
std::numeric_limits<float>::infinity(), // x分量:正无穷大
std::numeric_limits<float>::infinity(), // y分量:正无穷大
std::numeric_limits<float>::infinity() // z分量:正无穷大
);

class LineDrawer : protected QOpenGLFunctions_4_5_Core
{
public:
LineDrawer() {
m_isDrawing = false;
m_selectedIndex = -1; // 初始无选中线段
m_selectedColor = QVector4D(1.0f, 0.0f, 0.0f, 1.0f); // 选中颜色(红色)
}
~LineDrawer() { cleanup(); }

// 初始化VAO/VBO(在initializeGL中调用)
void init()
{
initializeOpenGLFunctions();
// 创建VAO和VBO
glGenVertexArrays(1, &m_lineVAO);
glGenBuffers(1, &m_lineVBO);
// 初始顶点数据(起点+终点,默认原点)
m_vertices = { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f };
}

// 获取所有端点的接口
std::vector<QVector3D> getAllEndpoints() const {
std::vector<QVector3D> points;
for (const auto& line : m_savedLines) {
points.push_back(line.first);
points.push_back(line.second);
}
return points;
}

// 获取所有线段(用于点击检测)
const std::vector<std::pair<QVector3D, QVector3D>>& getAllLines() const {
return m_savedLines;
}

// 设置直线颜色(支持当前绘制线、已保存线和选中线)
void setCurrentLineColor(const QVector4D& color) { m_currentLineColor = color; }
void setSavedLineColor(const QVector4D& color) { m_savedLineColor = color; }
void setSelectedColor(const QVector4D& color) { m_selectedColor = color; }

// 删除所有直线
void clearAllLines() {
m_isDrawing = false;
m_savedLines.clear();
m_selectedIndex = -1; // 清空选中状态
m_vertices = { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f };
updateVertexData();
}

// 撤销最近绘制的线段
void undoLastLine() {
if (!m_savedLines.empty()) {
m_savedLines.pop_back();
// 若撤销的是选中线段,取消选中状态
if (m_selectedIndex == m_savedLines.size()) {
m_selectedIndex = -1;
}
}
}

//删除选中的直线段
void deleteSelectedLine()
{
if (!m_savedLines.empty() && (m_selectedIndex < m_savedLines.size()))
{
m_savedLines.erase(m_savedLines.begin() + m_selectedIndex);
m_delRecordStartPos = m_savedLines[m_selectedIndex].first;
m_selectedIndex = -1;
}
}

// 开始绘制
void startDraw(const QVector3D& startPos)
{
m_isDrawing = true;
m_startPos = startPos;
m_endPos = startPos;
updateVertexData();
}

// 更新绘制
void updateDraw(const QVector3D& currentPos)
{
if (!m_isDrawing) return;
m_endPos = currentPos;
updateVertexData();
}

//保存已经绘制的直线
void saveDrawnLine()
{
m_isDrawing = false;
m_savedLines.push_back({ m_startPos, m_endPos });
}

// 结束绘制
void finishDraw()
{
m_isDrawing = false;
m_selectedIndex = -1;//恢复未选中状态
if (m_delRecordStartPos != m_startPos)
{
m_savedLines.push_back({ m_startPos, m_endPos });
}

m_delRecordStartPos = INVALID_POINT;
}

void draw(QOpenGLShaderProgram& shader)
{
if (!m_isDrawing && m_savedLines.empty()) return;

shader.bind();
glBindVertexArray(m_lineVAO);
glLineWidth(2.0f); // 线宽(可调整)

// 1. 绘制当前正在绘制的直线(保持原逻辑)
if (m_isDrawing)
{
shader.setUniformValue("lineColor", m_currentLineColor);
glDrawArrays(GL_LINES, 0, 2);
}

// 2. 绘制已保存的线段(关键修改:逐线段绘制,区分选中状态)
if (!m_savedLines.empty())
{
// 临时存储单条线段的顶点数据(2个点,每个点3个坐标)
std::vector<float> singleLineVertices(6);

for (size_t i = 0; i < m_savedLines.size(); ++i)
{
const auto& line = m_savedLines[i];
// 填充当前线段的顶点数据
singleLineVertices[0] = line.first.x(); // 起点X
singleLineVertices[1] = line.first.y(); // 起点Y
singleLineVertices[2] = line.first.z(); // 起点Z
singleLineVertices[3] = line.second.x(); // 终点X
singleLineVertices[4] = line.second.y(); // 终点Y
singleLineVertices[5] = line.second.z(); // 终点Z

// 3. 设置当前线段的颜色(选中则红色,否则默认色)
if (i == m_selectedIndex)
{
// 选中颜色:红色(RGBA)
shader.setUniformValue("lineColor", m_selectedColor);
}
else
{
// 未选中:默认已保存线颜色
shader.setUniformValue("lineColor", m_savedLineColor);
}

// 4. 更新VBO并绘制当前线段
glBindBuffer(GL_ARRAY_BUFFER, m_lineVBO);
glBufferData(GL_ARRAY_BUFFER, singleLineVertices.size() * sizeof(float),
singleLineVertices.data(), GL_DYNAMIC_DRAW); // 动态更新单条线段
// 设置顶点属性(位置)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 绘制单条线段(GL_LINES 模式,2个顶点)
glDrawArrays(GL_LINES, 0, 2);
}
}

glBindVertexArray(0);
shader.release();
}

// 清理资源
void cleanup()
{
glDeleteVertexArrays(1, &m_lineVAO);
glDeleteBuffers(1, &m_lineVBO);
m_vertices.clear();
m_savedLines.clear();
m_selectedIndex = -1;
}

// 选中状态管理
void setSelectedIndex(int index) {
// 索引有效性检查
if (index < -1 || static_cast<size_t>(index) >= m_savedLines.size()) {
m_selectedIndex = -1;
}
else {
m_selectedIndex = index;
}
}

int getSelectedIndex() const { return m_selectedIndex; }

QVector3D getCurrentLineStart() const {
return m_startPos;
}

bool isDrawing() const { return m_isDrawing; }

private:
// 更新顶点数据到VBO
void updateVertexData()
{
m_vertices[0] = m_startPos.x();
m_vertices[1] = m_startPos.y();
m_vertices[2] = m_startPos.z();
m_vertices[3] = m_endPos.x();
m_vertices[4] = m_endPos.y();
m_vertices[5] = m_endPos.z();

glBindVertexArray(m_lineVAO);
glBindBuffer(GL_ARRAY_BUFFER, m_lineVBO);
glBufferData(GL_ARRAY_BUFFER, m_vertices.size() * sizeof(float), m_vertices.data(), GL_DYNAMIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);
}

private:
unsigned int m_lineVAO = 0, m_lineVBO = 0;
std::vector<float> m_vertices; // 顶点数据:[x1,y1,z1, x2,y2,z2]
QVector3D m_startPos, m_endPos;
QVector3D m_delRecordStartPos = INVALID_POINT;//记录已经删除的直线的起点
bool m_isDrawing;//正在绘制(鼠标左键按下置为true, 鼠标移动时都为true, 左键松开置为false)
std::vector<std::pair<QVector3D, QVector3D>> m_savedLines; // 保存已完成线段
int m_selectedIndex; // 当前选中线段索引(-1表示无选中)

// 颜色属性
float m_fixedDepth = -5.0f;
QVector4D m_currentLineColor = { 1.0f, 0.0f, 0.0f, 1.0f }; // 当前绘制线(红色)
QVector4D m_savedLineColor = { 0.0f, 1.0f, 0.0f, 1.0f }; // 已保存线(绿色)
QVector4D m_selectedColor = { 1.0f, 1.0f, 0.0f, 1.0f }; // 点击选中的线(绿色)
};

workPlane.h

 #pragma once
#include <QOpenGLFunctions_3_3_Core>
#include <QVector3D>
#include <vector>

// 颜色常量定义
const float PLANE_COLOR_R = 0.839f;
const float PLANE_COLOR_G = 0.859f;
const float PLANE_COLOR_B = 0.859f;
const float GRID_COLOR_R = 0.102f;
const float GRID_COLOR_G = 0.984f;
const float GRID_COLOR_B = 0.984f;
const float GRID_OFFSET = 0.001f; // 统一网格线偏移量
const float GRID_Y_OFFSET = 0.01f; // 新增:网格线y方向位移量

struct PlanePara
{
QVector3D origin;
QVector3D normal;
QVector3D localX;
QVector3D localY;
float halfLength;
unsigned int gridCntPerEdge;//网格线数量
float offset;
};

// 继承 QOpenGLFunctions_3_3_Core 以绑定 OpenGL 函数
class WorkPlane : public QOpenGLFunctions_3_3_Core
{
public:
WorkPlane()
: m_planeVAO(0), m_planeVBO(0), m_planeEBO(0)
, m_planeIndexCount(0)
, m_gridVAO(0), m_gridVBO(0)
, m_gridVertexCount(0)
{
}

~WorkPlane()
{
// 释放 OpenGL 资源
glDeleteVertexArrays(1, &m_planeVAO);
glDeleteBuffers(1, &m_planeVBO);
glDeleteBuffers(1, &m_planeEBO);
glDeleteVertexArrays(1, &m_gridVAO);
glDeleteBuffers(1, &m_gridVBO);
}

void initPlane(const PlanePara& para)
{
// 初始化 OpenGL 函数(必须调用,绑定上下文)
initializeOpenGLFunctions();

// 复制传入参数到成员变量
m_para = para;

// 生成局部坐标系
getLocalXY(m_para.normal, m_para.localX, m_para.localY);

// 清空之前的网格点数据(避免重复初始化时数据叠加)
m_gridPoints.clear();
// 绘制平面和网格线
generatePlaneBase(m_para);
generateGridLines(m_para);
}

void drawPlane()
{
// 绘制平面基底
glBindVertexArray(m_planeVAO);
glDrawElements(GL_TRIANGLES, m_planeIndexCount, GL_UNSIGNED_INT, 0);

// 绘制网格线
glBindVertexArray(m_gridVAO);
glDrawArrays(GL_LINES, 0, m_gridVertexCount);

// 解绑 VAO
glBindVertexArray(0);
}

// 计算射线与工作平面的交点(返回是否相交)
bool getRayPlaneIntersection(
const QVector3D& rayOrigin, // 射线起点(摄像机位置)
const QVector3D& rayDir, // 射线方向
const PlanePara& plane, // 工作平面参数
QVector3D& outIntersection // 输出交点
) {
// 平面方程:(P - origin) · normal = 0
float denominator = QVector3D::dotProduct(rayDir, plane.normal);
if (qAbs(denominator) < 1e-6) return false; // 射线与平面平行,无交点

float t = QVector3D::dotProduct(plane.origin - rayOrigin, plane.normal) / denominator;
if (t < 0) return false; // 交点在射线后方(摄像机背后)

outIntersection = rayOrigin + rayDir * t;
return true;
}

// 获取网格交叉点列表
const std::vector<QVector3D>& gridPoints() const { return m_gridPoints; }
// 获取平面参数(origin、normal等)
const PlanePara& para() const { return m_para; }

private:
void getLocalXY(const QVector3D& normal, QVector3D& localX, QVector3D& localY)
{
QVector3D ref = (qAbs(normal.y()) < 0.999f) ? QVector3D(0, 1, 0) : QVector3D(1, 0, 0);
localX = QVector3D::crossProduct(normal, ref).normalized();
localY = QVector3D::crossProduct(normal, localX).normalized();
}

void generatePlaneBase(const PlanePara& para)
{
//计算出平面四个顶点
QVector3D pts[4];
pts[0] = para.origin + (para.halfLength * -para.localX) + (para.halfLength * -para.localY) + (para.offset * para.normal);
pts[1] = para.origin + (para.halfLength * para.localX) + (para.halfLength * -para.localY) + (para.offset * para.normal);
pts[2] = para.origin + (para.halfLength * para.localX) + (para.halfLength * para.localY) + (para.offset * para.normal);
pts[3] = para.origin + (para.halfLength * -para.localX) + (para.halfLength * para.localY) + (para.offset * para.normal);

// 平面顶点数据(使用常量颜色)
std::vector<float> vertices = {
pts[0].x(), pts[0].y(), pts[0].z(), PLANE_COLOR_R, PLANE_COLOR_G, PLANE_COLOR_B,
pts[1].x(), pts[1].y(), pts[1].z(), PLANE_COLOR_R, PLANE_COLOR_G, PLANE_COLOR_B,
pts[2].x(), pts[2].y(), pts[2].z(), PLANE_COLOR_R, PLANE_COLOR_G, PLANE_COLOR_B,
pts[3].x(), pts[3].y(), pts[3].z(), PLANE_COLOR_R, PLANE_COLOR_G, PLANE_COLOR_B
};

unsigned int indices[] = { 0, 1, 2, 0, 2, 3 };
m_planeIndexCount = sizeof(indices) / sizeof(unsigned int);

// 生成并配置 VAO/VBO/EBO
glGenVertexArrays(1, &m_planeVAO);
glGenBuffers(1, &m_planeVBO);
glGenBuffers(1, &m_planeEBO);

glBindVertexArray(m_planeVAO);
glBindBuffer(GL_ARRAY_BUFFER, m_planeVBO);
glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_STATIC_DRAW);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_planeEBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

// 配置顶点位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 配置顶点颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

glBindVertexArray(0);
}

void generateGridLines(const PlanePara& para)
{
std::vector<float> gridVertices;
float step = 2.0f * para.halfLength / para.gridCntPerEdge;

// 计算网格交叉点(同样添加y方向位移)
{
std::vector<float> xCoords, yCoords;
xCoords.push_back(-para.halfLength);
yCoords.push_back(-para.halfLength);
for (int i = 1; i < para.gridCntPerEdge; ++i) {
xCoords.push_back(-para.halfLength + i * step);
yCoords.push_back(-para.halfLength + i * step);
}
xCoords.push_back(para.halfLength);
yCoords.push_back(para.halfLength);

for (float x : xCoords) {
for (float y : yCoords) {
QVector3D point = para.origin
+ x * para.localX
+ y * para.localY
+ para.offset * para.normal;
point.setY(point.y() + GRID_Y_OFFSET); // 网格交叉点y方向位移
m_gridPoints.push_back(point);
}
}
}

// X方向网格线(平行于localX)
for (int i = 1; i < para.gridCntPerEdge; ++i)
{
float yPos = -para.halfLength + i * step;

QVector3D start = para.origin - para.halfLength * para.localX + yPos * para.localY;
QVector3D end = para.origin + para.halfLength * para.localX + yPos * para.localY;

start += para.offset * para.normal;
end += para.offset * para.normal;

// 关键修改1:X方向网格线y位移
start.setY(start.y() + GRID_Y_OFFSET);
end.setY(end.y() + GRID_Y_OFFSET);

gridVertices.insert(gridVertices.end(),
{
start.x(), start.y(), start.z(), GRID_COLOR_R, GRID_COLOR_G, GRID_COLOR_B,
end.x(), end.y(), end.z(), GRID_COLOR_R, GRID_COLOR_G, GRID_COLOR_B
});
}

// Y方向网格线(平行于localY)
for (int i = 1; i < para.gridCntPerEdge; ++i)
{
float xPos = -para.halfLength + i * step;

QVector3D start = para.origin - para.halfLength * para.localY + xPos * para.localX;
QVector3D end = para.origin + para.halfLength * para.localY + xPos * para.localX;

start += para.offset * para.normal;
end += para.offset * para.normal;

// 关键修改2:Y方向网格线y位移
start.setY(start.y() + GRID_Y_OFFSET);
end.setY(end.y() + GRID_Y_OFFSET);

gridVertices.insert(gridVertices.end(),
{
start.x(), start.y(), start.z(), GRID_COLOR_R, GRID_COLOR_G, GRID_COLOR_B,
end.x(), end.y(), end.z(), GRID_COLOR_R, GRID_COLOR_G, GRID_COLOR_B
});
}

m_gridVertexCount = gridVertices.size() / 6;

// 生成并配置 VAO/VBO
glGenVertexArrays(1, &m_gridVAO);
glGenBuffers(1, &m_gridVBO);

glBindVertexArray(m_gridVAO);
glBindBuffer(GL_ARRAY_BUFFER, m_gridVBO);
glBufferData(GL_ARRAY_BUFFER, gridVertices.size() * sizeof(float), gridVertices.data(), GL_STATIC_DRAW);

// 配置顶点位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 配置顶点颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);

glBindVertexArray(0);
}

private:
PlanePara m_para;//平面参数
unsigned int m_planeVAO, m_planeVBO, m_planeEBO;
int m_planeIndexCount;
unsigned int m_gridVAO, m_gridVBO;
int m_gridVertexCount;
std::vector<QVector3D> m_gridPoints; // 存储所有网格交叉点
};

glview.h

#pragma once
#include <qopenglwidget.h>

#include <qopenglshaderprogram.h>
#include <QMouseEvent>
#include <qmenu.h>

#include "model.h"
#include "camera.h"
#include "workPlane.h"
#include "viewCube.h"
#include "LineDrawer.h"

class GLView : public QOpenGLWidget, QOpenGLFunctions_4_5_Core
{
Q_OBJECT
public:
explicit GLView(QWidget* parent = nullptr);
~GLView();

void beginDrawLines();
void clearAllLines();
void undoLastLine();
void selectLine();


protected:
virtual void initializeGL();
virtual void resizeGL(int w,int h);
virtual void paintGL();
virtual bool event(QEvent* e);

void initWorkPlane();

QVector3D screenToWorld_3D_PointSnap(const QPoint& pos);
QVector3D screenToWorldRawRay(const QPoint& pos);
float rayLineDistance(const QPoint& mousePos, const QVector3D& rayOrigin, const QVector3D& rayDir, const QVector3D& lineStart, const QVector3D& lineEnd);
bool checkLineHit(const QPoint& mousePos);
bool getRayPlaneIntersection(const QVector3D& rayOrigin, const QVector3D& rayDir, const QVector3D& planeOrigin, const QVector3D& planeNormal, QVector3D& outIntersection);

private:
void initShader(QOpenGLShaderProgram& shader, const QString& vertexFile, const QString& fragFile);

private:
Model* m_model{ nullptr };
Camera m_camera = Camera(this, QVector3D(0.0f, 0.0f, 5.0f));

//mvp矩阵
QMatrix4x4 m_modelMat;//自动为单位阵
QMatrix4x4 m_viewMat;
QMatrix4x4 m_projectionMat;

//着色器程序
QOpenGLShaderProgram m_lightShader;
QOpenGLShaderProgram m_viewCubeShader;
QOpenGLShaderProgram m_drawLineShader;

//工作平面
WorkPlane* m_workPlane{ nullptr };

//视图立方体
ViewCube* m_viewCube{ nullptr };

QMatrix4x4 m_modelCube;
QMatrix4x4 m_viewMatNoTrans;
QMatrix4x4 m_projection4ViewCube;
QMatrix4x4 m_finalProjection;

//直线绘制
LineDrawer m_lineDrawer;

bool b_drawLineState = false;

// 新增:绘制平面参数(垂直工作平面且朝向视角)
QVector3D m_drawPlaneOrigin; // 平面原点(过直线起点)
QVector3D m_drawPlaneNormal; // 平面法向量(垂直工作平面,朝向视角)
bool m_enableDrawPlane = true; // 是否启用平面约束

bool b_selectLineState = false;

QMenu* m_onSelectMenu = nullptr;
QAction* m_action_deleteSelecedLine = nullptr;

signals:

};

glview.cpp

#include "glview.h"
#include <QMouseEvent>
#include <qdebug.h>

GLView::GLView(QWidget* parent)
: QOpenGLWidget{ parent }, m_workPlane(nullptr), m_model(nullptr), m_viewCube(nullptr)
, b_drawLineState(false), b_selectLineState(false), m_enableDrawPlane(true)
{
m_action_deleteSelecedLine = new QAction("delete");
m_onSelectMenu = new QMenu(this);
m_onSelectMenu->setStyleSheet(R"(
QMenu {
background-color: rgba(240, 240, 240, 0.9);
border: 0.5px solid white;
border-radius: 4px;
padding: 4px 0;
}

QMenu::item {
padding: 6px 20px;
color: #333333;
background-color: transparent;
}

QMenu::item:selected {
background-color: rgba(220, 220, 220, 0.8);
color: #000000;
}

QMenu::item:disabled {
color: #aaaaaa;
}
)");
m_onSelectMenu->addAction(m_action_deleteSelecedLine);

connect(m_action_deleteSelecedLine, &QAction::triggered, this, [=]()
{
m_lineDrawer.deleteSelectedLine();
update();
m_onSelectMenu->hide();
});
}

GLView::~GLView()
{
makeCurrent();
delete m_model;
delete m_workPlane;
delete m_viewCube;
doneCurrent();
}

void GLView::beginDrawLines()
{
b_drawLineState = true;
m_lineDrawer.setCurrentLineColor(QVector4D(0.0f, 0.0f, 1.0f, 1.0f));
m_lineDrawer.setSavedLineColor(QVector4D(1.0f, 1.0f, 0.0f, 1.0f));
}

void GLView::clearAllLines()
{
m_lineDrawer.clearAllLines();
update();
}

void GLView::undoLastLine()
{
m_lineDrawer.undoLastLine();
update();
}

void GLView::selectLine()
{
b_selectLineState = true;
}

QVector3D GLView::screenToWorld_3D_PointSnap(const QPoint& pos)
{
int x = pos.x();
int y = height() - pos.y(); // 翻转y轴

// 1. 生成鼠标射线(起点:相机位置;方向:视线方向)
QVector3D camPos = m_viewMat.inverted().column(3).toVector3D(); // 射线起点
QMatrix4x4 invMVP = (m_projectionMat * m_viewMat).inverted();

// 屏幕坐标 → NDC坐标
float ndcX = (2.0f * x) / width() - 1.0f;
float ndcY = (2.0f * y) / height() - 1.0f;

// 计算射线方向(通过近/远平面)
QVector4D nearNDC(ndcX, ndcY, -1.0f, 1.0f);
QVector4D farNDC(ndcX, ndcY, 1.0f, 1.0f);
QVector3D nearWorld = (invMVP * nearNDC).toVector3D() / (invMVP * nearNDC).w();
QVector3D farWorld = (invMVP * farNDC).toVector3D() / (invMVP * farNDC).w();
QVector3D rayDir = (farWorld - nearWorld).normalized(); // 射线方向(单位向量)

// 2. 优先吸附到直线端点
const float endpointSnapThreshold = 0.1f; // 端点吸附阈值
auto endpoints = m_lineDrawer.getAllEndpoints();
QVector3D closestEndpoint;
float minEndpointDistance = FLT_MAX;

for (const auto& endpoint : endpoints) {
QVector3D camToEndpoint = endpoint - camPos;
float t = QVector3D::dotProduct(camToEndpoint, rayDir); // 端点在射线上的投影参数
QVector3D projection = camPos + rayDir * t; // 端点在射线上的投影点
float distance = (endpoint - projection).length();

// 确保端点在射线前方且距离达标
if (t > 0 && distance < minEndpointDistance && distance < endpointSnapThreshold) {
minEndpointDistance = distance;
closestEndpoint = endpoint;
}
}
// 若找到端点,直接返回(端点吸附优先级高于网格)
if (minEndpointDistance != FLT_MAX) {
return closestEndpoint;
}

// 3. 吸附到工作平面网格交叉点
const float gridSnapThreshold = 0.1f; // 网格吸附阈值
QVector3D planeIntersection; // 射线与工作平面的交点

// 3.1 计算射线与工作平面的交点
if (m_workPlane->getRayPlaneIntersection(camPos, rayDir, m_workPlane->para(), planeIntersection)) {
// 3.2 遍历所有网格交叉点,找最近的点
QVector3D closestGridPoint;
float minGridDistance = FLT_MAX;

for (const QVector3D& gridPoint : m_workPlane->gridPoints()) {
float distance = (planeIntersection - gridPoint).length();
if (distance < minGridDistance && distance < gridSnapThreshold) {
minGridDistance = distance;
closestGridPoint = gridPoint;
}
}

// 3.3 若找到符合条件的网格点,返回该点
if (minGridDistance != FLT_MAX) {
return closestGridPoint;
}
}

if (m_workPlane->getRayPlaneIntersection(camPos, rayDir, m_workPlane->para(), planeIntersection))
{
return planeIntersection;
}

// 4. 未找到任何吸附点,返回射线默认点
float defaultDistance = 5.0f;
return camPos + rayDir * defaultDistance;
}


QVector3D GLView::screenToWorldRawRay(const QPoint& pos)
{
int x = pos.x();
int y = height() - pos.y();

QVector3D camPos = m_viewMat.inverted().column(3).toVector3D();
QMatrix4x4 invMVP = (m_projectionMat * m_viewMat).inverted();

float ndcX = (2.0f * x) / width() - 1.0f;
float ndcY = (2.0f * y) / height() - 1.0f;

QVector4D farNDC(ndcX, ndcY, 1.0f, 1.0f);
QVector3D farWorld = (invMVP * farNDC).toVector3D() / (invMVP * farNDC).w();

const float rayLength = 100.0f;
QVector3D rayDir = (farWorld - camPos).normalized();
return camPos + rayDir * rayLength;
}

float GLView::rayLineDistance(
const QPoint& mousePos,
const QVector3D& rayOrigin,
const QVector3D& rayDir,
const QVector3D& lineStart,
const QVector3D& lineEnd
) {
QVector3D lineVec = lineEnd - lineStart;
QVector3D originToLineStart = rayOrigin - lineStart;

float a = QVector3D::dotProduct(rayDir, rayDir);
float b = QVector3D::dotProduct(rayDir, lineVec);
float c = QVector3D::dotProduct(lineVec, lineVec);
float d = QVector3D::dotProduct(rayDir, originToLineStart);
float e = QVector3D::dotProduct(lineVec, originToLineStart);
float denominator = a * c - b * b;

float s, t;
if (denominator < 1e-6) {
s = 0.0f;
t = d / a;
}
else {
s = (b * e - c * d) / denominator;
t = (a * e - b * d) / denominator;
}
s = qBound(0.0f, s, 1.0f);
t = qMax(0.0f, t);

auto worldToScreen = [&](const QVector3D& worldPos) -> QPointF {
QVector4D clipPos = m_projectionMat * m_viewMat * QVector4D(worldPos, 1.0f);
QVector3D ndcPos = clipPos.toVector3D() / clipPos.w();
float x = (ndcPos.x() + 1.0f) * 0.5f * width();
float y = height() - (ndcPos.y() + 1.0f) * 0.5f * height();
return QPointF(x, y);
};

QPointF lineStartScreen = worldToScreen(lineStart);
QPointF lineEndScreen = worldToScreen(lineEnd);
QPointF mouseScreen = QPointF(mousePos);

QPointF lineVecScreen = lineEndScreen - lineStartScreen;
QPointF mouseToStartScreen = mouseScreen - lineStartScreen;
float dotProduct = mouseToStartScreen.x() * lineVecScreen.x() + mouseToStartScreen.y() * lineVecScreen.y();
float len2 = lineVecScreen.x() * lineVecScreen.x() + lineVecScreen.y() * lineVecScreen.y();
float screenDistance;

if (len2 < 1e-6) {
screenDistance = hypot(mouseToStartScreen.x(), mouseToStartScreen.y());
}
else if (dotProduct <= 0.0f) {
screenDistance = hypot(mouseToStartScreen.x(), mouseToStartScreen.y());
}
else if (dotProduct >= len2) {
QPointF mouseToEndScreen = mouseScreen - lineEndScreen;
screenDistance = hypot(mouseToEndScreen.x(), mouseToEndScreen.y());
}
else {
float tScreen = dotProduct / len2;
QPointF closestOnLineScreen = lineStartScreen + tScreen * lineVecScreen;
QPointF delta = mouseScreen - closestOnLineScreen;
screenDistance = hypot(delta.x(), delta.y());
}

return screenDistance;
}

bool GLView::checkLineHit(const QPoint& mousePos)
{
QVector3D rayOrigin = m_viewMat.inverted().column(3).toVector3D();
QVector3D rayEnd = screenToWorldRawRay(mousePos);
QVector3D rayDir = (rayEnd - rayOrigin).normalized();

const float hitThreshold = 1.8f;
float minDist = FLT_MAX;
int hitIndex = -1;
auto& lines = m_lineDrawer.getAllLines();

for (size_t i = 0; i < lines.size(); ++i) {
const auto& line = lines[i];
QVector3D lineStart = line.first;
QVector3D lineEnd = line.second;

// 排除端点附近区域
float distToStart = rayLineDistance(mousePos, rayOrigin, rayDir, lineStart, lineStart);
float distToEnd = rayLineDistance(mousePos, rayOrigin, rayDir, lineEnd, lineEnd);
const float endpointSnapThreshold = 5.0f;
if (distToStart < endpointSnapThreshold || distToEnd < endpointSnapThreshold) {
continue;
}

float dist = rayLineDistance(mousePos, rayOrigin, rayDir, lineStart, lineEnd);
if (dist < minDist && dist < hitThreshold) {
minDist = dist;
hitIndex = static_cast<int>(i);
}
}

m_lineDrawer.setSelectedIndex(hitIndex);
return hitIndex != -1;
}

bool GLView::getRayPlaneIntersection(
const QVector3D& rayOrigin,
const QVector3D& rayDir,
const QVector3D& planeOrigin,
const QVector3D& planeNormal,
QVector3D& outIntersection
) {
float denominator = QVector3D::dotProduct(rayDir, planeNormal);
if (qAbs(denominator) < 1e-6) return false;

float t = QVector3D::dotProduct(planeOrigin - rayOrigin, planeNormal) / denominator;
if (t < 0) return false;

outIntersection = rayOrigin + rayDir * t;
return true;
}

void GLView::initializeGL()
{
initializeOpenGLFunctions();
glEnable(GL_DEPTH_TEST);
glEnable(GL_POLYGON_OFFSET_FILL); // 启用多边形偏移,解决平面与网格线深度冲突

initShader(m_lightShader, "./shader/modelLoading.vert", "./shader/modelLoading.frag");
initShader(m_viewCubeShader, "./shader/viewCubeTexture.vert", "./shader/viewCubeTexture.frag");
initShader(m_drawLineShader, "./shader/line.vert", "./shader/line.frag");

m_model = new Model(this);
m_camera.lastFrame = QTime::currentTime().msecsSinceStartOfDay() / 1000.0;

initWorkPlane();
m_lineDrawer.init();

m_viewCube = new ViewCube(this, ":/MainWindow/resources/viewcube.png", width(), height());
}

void GLView::resizeGL(int w, int h)
{
glViewport(0, 0, w, h);

m_camera.SCR_WIDTH = w;
m_camera.SCR_HEIGHT = h;

m_projectionMat.setToIdentity();
m_projectionMat.perspective(m_camera.Zoom, (float)w / h, 0.1f, 100.0f);
}

void GLView::paintGL()
{
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 同时清除深度缓冲

float currentFrame = QTime::currentTime().msecsSinceStartOfDay() / 1000.0;
m_camera.deltaTime = currentFrame - m_camera.lastFrame;
m_camera.lastFrame = currentFrame;

// 确保渲染顺序:先平面,再模型,最后直线(避免直线被遮挡)
m_lightShader.bind();
m_viewMat = b_drawLineState ? m_viewMat : m_camera.GetViewMatrix();
m_lightShader.setUniformValue("projection", m_projectionMat);
m_lightShader.setUniformValue("view", m_viewMat);
m_lightShader.setUniformValue("model", m_modelMat);

// 1. 先绘制工作平面(最底层)
m_workPlane->drawPlane();
// 2. 绘制模型(中间层)
m_model->Draw(m_lightShader);
m_lightShader.release();

// 3. 最后绘制直线(最上层,确保在平面上方可见)
m_drawLineShader.bind();
m_drawLineShader.setUniformValue("model", m_modelMat);
m_drawLineShader.setUniformValue("view", m_viewMat);
m_drawLineShader.setUniformValue("projection", m_projectionMat);
m_lineDrawer.draw(m_drawLineShader);
m_drawLineShader.release();

// 绘制ViewCube
if (m_viewCube) {
m_viewCubeShader.bind();
m_modelCube.setToIdentity();
m_modelCube.scale(0.5f);

m_viewMatNoTrans = b_drawLineState ? m_viewMatNoTrans : m_camera.GetViewMatrix4VieweCube();
m_projection4ViewCube.setToIdentity();
float aspect = (float)width() / height();
m_projection4ViewCube.perspective(45.0f, aspect, 0.1f, 100.0f);

QMatrix4x4 offViewCube;
offViewCube.setToIdentity();
QVector2D projOffset(0.90f, 0.80f);
offViewCube.translate(projOffset.x(), projOffset.y(), 0.0f);
m_finalProjection = offViewCube * m_projection4ViewCube;

m_viewCubeShader.setUniformValue("model", m_modelCube);
m_viewCubeShader.setUniformValue("view", m_viewMatNoTrans);
m_viewCubeShader.setUniformValue("projection", m_finalProjection);
m_viewCube->draw(m_viewCubeShader, m_modelCube, m_viewMatNoTrans, m_finalProjection);
m_viewCubeShader.release();
}
}

void GLView::initShader(QOpenGLShaderProgram& shader, const QString& vertexFile, const QString& fragFile)
{
bool result = shader.addShaderFromSourceFile(QOpenGLShader::Vertex, vertexFile);
if (!result) qDebug() << shader.log();

result = shader.addShaderFromSourceFile(QOpenGLShader::Fragment, fragFile);
if (!result) qDebug() << shader.log();

result = shader.link();
if (!result) qDebug() << shader.log();
}

bool GLView::event(QEvent* e)
{
makeCurrent();

if (e->type() == QEvent::MouseButtonPress) {
auto mouseEvent = static_cast<QMouseEvent*>(e);
if (mouseEvent->button() == Qt::LeftButton) {
ViewCube::CubeFaceType face = m_viewCube->getFirstHitFace(
mouseEvent->position().x(), mouseEvent->position().y(),
width(), height(), m_viewMatNoTrans, m_finalProjection
);
if (face != ViewCube::CubeFaceType::None) {
m_camera.FitView(static_cast<int>(face));
}
else if (b_drawLineState) {
bool isHit = b_selectLineState ? checkLineHit(mouseEvent->pos()) : false;
if (isHit) {
update();//更新(被选中的直线变为红色)
QPoint pos = mapFromGlobal(mouseEvent->pos());
m_onSelectMenu->move(pos);
m_onSelectMenu->show();
}
else {
QVector3D startPos = screenToWorld_3D_PointSnap(mouseEvent->pos());
m_lineDrawer.startDraw(startPos);

if (m_enableDrawPlane && m_workPlane) {
PlanePara workPara = m_workPlane->para();
QVector3D workNormal = workPara.normal.normalized();
QVector3D camFront = (m_viewMat.inverted().column(2).toVector3D()).normalized();

QVector3D projCamFront = camFront - QVector3D::dotProduct(camFront, workNormal) * workNormal;
projCamFront.normalize();
m_drawPlaneNormal = projCamFront;

if (QVector3D::dotProduct(m_drawPlaneNormal, camFront) < 0) {
m_drawPlaneNormal = -m_drawPlaneNormal;
}

m_drawPlaneOrigin = startPos;
}
}
}
}
else if (mouseEvent->button() == Qt::RightButton) {
if (b_drawLineState) {
m_lineDrawer.finishDraw();
b_drawLineState = false;
}
update();
}
}
else if (e->type() == QEvent::MouseButtonRelease) {
auto mouseEvent = static_cast<QMouseEvent*>(e);
if (mouseEvent->button() == Qt::LeftButton && b_drawLineState) {
m_lineDrawer.saveDrawnLine();
update();
}
}
else if (e->type() == QEvent::MouseMove) {
auto mouseEvent = static_cast<QMouseEvent*>(e);
if (m_lineDrawer.isDrawing()) {
QVector3D currentPos;
QVector3D camPos = m_viewMat.inverted().column(3).toVector3D();
QVector3D rawEnd = screenToWorld_3D_PointSnap(mouseEvent->pos());
QVector3D rayDir = (rawEnd - camPos).normalized();

bool hasValidSnapPoint = false;
auto endpoints = m_lineDrawer.getAllEndpoints();
const float endpointSnapThreshold = 0.1f;
for (const auto& endpoint : endpoints) {
if ((endpoint - rawEnd).length() < endpointSnapThreshold) {
hasValidSnapPoint = true;
break;
}
}

if (!hasValidSnapPoint) {
const float gridSnapThreshold = 0.1f;
for (const QVector3D& gridPoint : m_workPlane->gridPoints()) {
if ((gridPoint - rawEnd).length() < gridSnapThreshold) {
hasValidSnapPoint = true;
break;
}
}
}

if (m_enableDrawPlane && !hasValidSnapPoint) {
QVector3D constrainedEnd;
if (getRayPlaneIntersection(camPos, rayDir, m_drawPlaneOrigin, m_drawPlaneNormal, constrainedEnd)) {
currentPos = constrainedEnd;
}
else {
currentPos = rawEnd;
}
}
else {
currentPos = rawEnd;
}

m_lineDrawer.updateDraw(currentPos);
update();
}
}

if (!b_drawLineState && m_camera.handle(e)) {
update();
}

doneCurrent();
return QWidget::event(e);
}

void GLView::initWorkPlane()
{
PlanePara para;
para.origin = QVector3D(0.0f, -1.0f, 0.0f);
para.normal = QVector3D(0.0f, 1.0f, 0.0f); // 向上的法向量(Y轴)
para.halfLength = 5.0f;
para.gridCntPerEdge = 15;
para.offset = 0.0f; // 平面基础偏移(Y=0.1f)

m_workPlane = new WorkPlane();
m_workPlane->initPlane(para);
}

modeLoading.vert

#version 450 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;

out vec3 fragColor;
out vec2 screenPos; // 输出屏幕坐标(用于渐变)

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
uniform vec2 screenSize; // 屏幕宽高

void main() {
gl_Position = projection * view * model * vec4(aPos, 1.0);
fragColor = aColor;
// 计算屏幕坐标(标准化到0~1范围)
screenPos = (gl_Position.xy / gl_Position.w + 1.0) * 0.5;
}

modeLoading.frag

#version 450 core
in vec3 fragColor;
in vec2 screenPos; // 接收屏幕坐标
out vec4 FragColor;

// 渐变颜色参数(CAD风格深灰渐变)
vec3 topColor = vec3(0.25f, 0.25f, 0.25f); // 顶部稍亮
vec3 bottomColor = vec3(0.15f, 0.15f, 0.15f); // 底部稍暗

void main() {
// 绘制模型时用原有颜色,否则用渐变色背景
if (gl_FragCoord.z < 1.0) { // 若该像素是模型(z值小于1)
FragColor = vec4(fragColor, 1.0);
} else { // 否则是背景
// 按Y坐标插值计算渐变颜色
float t = screenPos.y; // t=0是底部,t=1是顶部
vec3 bgColor = mix(bottomColor, topColor, t);
FragColor = vec4(bgColor, 1.0);
}
}

viewCubeTexture.vert

#version 330 core
layout (location = 0) in vec3 aPos; // 顶点位置
layout (location = 1) in vec2 aTexCoord; // 纹理坐标

out vec2 TexCoord; // 传递给片段着色器

uniform mat4 model; // 模型矩阵
uniform mat4 view; // 视图矩阵
uniform mat4 projection; // 投影矩阵

void main() {
gl_Position = projection * view * model * vec4(aPos, 1.0);
TexCoord = aTexCoord;
}

viewCubeTexture.frag

#version 330 core
out vec4 FragColor;

in vec2 TexCoord; // 接收顶点着色器的纹理坐标

uniform sampler2D u_texture; // 纹理采样器

void main() {
FragColor = texture(u_texture, TexCoord); // 直接采样纹理
}

line.vert

#version 330 core
layout (location = 0) in vec3 aPos; // 直线顶点位置

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
}

line.frag

#version 330 core
out vec4 FragColor;

uniform vec4 lineColor; // 直线颜色(专用变量名,更贴合直线绘制场景)
uniform float lineWidth; // 可选:如需在着色器中处理线宽(需配合几何着色器)

void main()
{
FragColor = lineColor;
}

图-1

[源码与示例程序下载]