#include "ViewportWidget.h" #include "Camera.h" #include "ViewCube.h" #include "SketchGrid.h" #include "Document.h" #include "FeatureBrowser.h" #include "SketchFeature.h" #include "SketchLine.h" #include "SketchObject.h" #include "ApplicationController.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include struct PntComparator { bool operator()(const gp_Pnt& a, const gp_Pnt& b) const { // A tolerance is needed for floating point comparisons double tol = 1e-9; if (std::abs(a.X() - b.X()) > tol) return a.X() < b.X(); if (std::abs(a.Y() - b.Y()) > tol) return a.Y() < b.Y(); if (std::abs(a.Z() - b.Z()) > tol) return a.Z() < b.Z(); return false; // They are considered equal } }; ViewportWidget::ViewportWidget(QWidget *parent) : QOpenGLWidget(parent) { m_camera = new Camera(this); connect(m_camera, &Camera::cameraChanged, this, QOverload<>::of(&QWidget::update)); m_viewCube = new ViewCube(); m_sketchGrid = new SketchGrid(); m_featureBrowser = new FeatureBrowser(); setMouseTracking(true); setFocusPolicy(Qt::StrongFocus); m_currentPlane = SketchPlane::XY; m_toolIcons.insert(static_cast(ApplicationController::ToolType::Line), new QSvgRenderer(QString(":/icons/line.svg"), this)); m_toolIcons.insert(static_cast(ApplicationController::ToolType::Rectangle), new QSvgRenderer(QString(":/icons/rectangle.svg"), this)); m_toolIcons.insert(static_cast(ApplicationController::ToolType::Circle), new QSvgRenderer(QString(":/icons/circle.svg"), this)); m_cursorRenderer = new QSvgRenderer(QString(":/icons/cursor.svg"), this); } ViewportWidget::~ViewportWidget() { makeCurrent(); delete m_shaderProgram; delete m_viewCube; delete m_sketchGrid; m_vbo.destroy(); m_vao.destroy(); doneCurrent(); delete m_featureBrowser; } void ViewportWidget::setDocument(Document* document) { m_document = document; m_featureBrowser->setDocument(document); } void ViewportWidget::initializeGL() { initializeOpenGLFunctions(); glClearColor(0.2f, 0.3f, 0.3f, 1.0f); initShaders(); m_vao.create(); QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao); m_vbo.create(); m_vbo.bind(); m_vbo.allocate(nullptr, 0); // Allocate when drawing // Position attribute m_shaderProgram->enableAttributeArray(0); m_shaderProgram->setAttributeBuffer(0, GL_FLOAT, 0, 3, 0); m_vbo.release(); glEnable(GL_DEPTH_TEST); m_viewCube->initializeGL(); m_sketchGrid->initializeGL(); } void ViewportWidget::paintGL() { // Main scene rendering glViewport(0, 0, width(), height()); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); if (!m_shaderProgram || !m_shaderProgram->isLinked()) { return; } glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); QMatrix4x4 model = m_camera->modelViewMatrix(); m_shaderProgram->bind(); m_shaderProgram->setUniformValue(m_projMatrixLoc, projection); m_shaderProgram->setUniformValue(m_mvMatrixLoc, model); QOpenGLVertexArrayObject::Binder vaoBinder(&m_vao); // Sketch grid rendering if (m_isSelectingPlane) { if (m_highlightedPlane != SketchPlane::NONE) { m_sketchGrid->paintGL(static_cast(m_highlightedPlane), m_shaderProgram, m_colorLoc); } } if (m_currentPlane != SketchPlane::NONE) { m_sketchGrid->paintGL(static_cast(m_currentPlane), m_shaderProgram, m_colorLoc); } if (m_isSelectingPlane) { drawSelectionPlanes(); } if (m_document) { for (Feature* feature : m_document->features()) { if (auto sketch = dynamic_cast(feature)) { drawSketch(sketch); } } } QVector vertices; if (m_isSnappingOrigin) { const float rectSize = 0.0075f * -m_camera->zoom(); if (m_currentPlane == SketchPlane::XY) { vertices << -rectSize << 0 << -rectSize << rectSize << 0 << -rectSize; vertices << rectSize << 0 << -rectSize << rectSize << 0 << rectSize; vertices << rectSize << 0 << rectSize << -rectSize << 0 << rectSize; vertices << -rectSize << 0 << rectSize << -rectSize << 0 << -rectSize; } else if (m_currentPlane == SketchPlane::XZ) { vertices << -rectSize << -rectSize << 0 << rectSize << -rectSize << 0; vertices << rectSize << -rectSize << 0 << rectSize << rectSize << 0; vertices << rectSize << rectSize << 0 << -rectSize << rectSize << 0; vertices << -rectSize << rectSize << 0 << -rectSize << -rectSize << 0; } else if (m_currentPlane == SketchPlane::YZ) { vertices << 0 << -rectSize << -rectSize << 0 << rectSize << -rectSize; vertices << 0 << rectSize << -rectSize << 0 << rectSize << rectSize; vertices << 0 << rectSize << rectSize << 0 << -rectSize << rectSize; vertices << 0 << -rectSize << rectSize << 0 << -rectSize << -rectSize; } m_shaderProgram->setUniformValue(m_colorLoc, QVector4D(1.0f, 1.0f, 0.0f, 0.5f)); m_vbo.bind(); m_vbo.allocate(vertices.constData(), vertices.size() * sizeof(GLfloat)); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glDrawArrays(GL_LINES, 0, vertices.size() / 3); glDisable(GL_BLEND); } else if (m_isSnappingVertex) { const float rectSize = 0.0075f * -m_camera->zoom(); const auto& v = m_snapVertex; if (m_currentPlane == SketchPlane::XY) { vertices << v.X() - rectSize << v.Y() << v.Z() - rectSize << v.X() + rectSize << v.Y() << v.Z() - rectSize; vertices << v.X() + rectSize << v.Y() << v.Z() - rectSize << v.X() + rectSize << v.Y() << v.Z() + rectSize; vertices << v.X() + rectSize << v.Y() << v.Z() + rectSize << v.X() - rectSize << v.Y() << v.Z() + rectSize; vertices << v.X() - rectSize << v.Y() << v.Z() + rectSize << v.X() - rectSize << v.Y() << v.Z() - rectSize; } else if (m_currentPlane == SketchPlane::XZ) { vertices << v.X() - rectSize << v.Y() - rectSize << v.Z() << v.X() + rectSize << v.Y() - rectSize << v.Z(); vertices << v.X() + rectSize << v.Y() - rectSize << v.Z() << v.X() + rectSize << v.Y() + rectSize << v.Z(); vertices << v.X() + rectSize << v.Y() + rectSize << v.Z() << v.X() - rectSize << v.Y() + rectSize << v.Z(); vertices << v.X() - rectSize << v.Y() + rectSize << v.Z() << v.X() - rectSize << v.Y() - rectSize << v.Z(); } else if (m_currentPlane == SketchPlane::YZ) { vertices << v.X() << v.Y() - rectSize << v.Z() - rectSize << v.X() << v.Y() + rectSize << v.Z() - rectSize; vertices << v.X() << v.Y() + rectSize << v.Z() - rectSize << v.X() << v.Y() + rectSize << v.Z() + rectSize; vertices << v.X() << v.Y() + rectSize << v.Z() + rectSize << v.X() << v.Y() - rectSize << v.Z() + rectSize; vertices << v.X() << v.Y() - rectSize << v.Z() + rectSize << v.X() << v.Y() - rectSize << v.Z() - rectSize; } m_shaderProgram->setUniformValue(m_colorLoc, QVector4D(1.0f, 1.0f, 0.0f, 0.5f)); m_vbo.bind(); m_vbo.allocate(vertices.constData(), vertices.size() * sizeof(GLfloat)); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glDrawArrays(GL_LINES, 0, vertices.size() / 3); glDisable(GL_BLEND); } if (m_isDefiningLine && m_activeTool == static_cast(ApplicationController::ToolType::Line)) { vertices.clear(); QVector3D worldPos = unproject(m_currentMousePos, m_currentPlane); if (m_isSnappingOrigin) { worldPos.setX(0); worldPos.setY(0); worldPos.setZ(0); } else if (m_isSnappingVertex) { worldPos.setX(m_snapVertex.X()); worldPos.setY(m_snapVertex.Y()); worldPos.setZ(m_snapVertex.Z()); } else if (m_isSnappingHorizontal) { if (m_currentPlane == SketchPlane::XY) worldPos.setZ(m_firstLinePoint.Z()); else if (m_currentPlane == SketchPlane::XZ) worldPos.setY(m_firstLinePoint.Y()); else if (m_currentPlane == SketchPlane::YZ) worldPos.setZ(m_firstLinePoint.Z()); } else if (m_isSnappingVertical) { if (m_currentPlane == SketchPlane::XY) worldPos.setX(m_firstLinePoint.X()); else if (m_currentPlane == SketchPlane::XZ) worldPos.setX(m_firstLinePoint.X()); else if (m_currentPlane == SketchPlane::YZ) worldPos.setY(m_firstLinePoint.Y()); } vertices << m_firstLinePoint.X() << m_firstLinePoint.Y() << m_firstLinePoint.Z(); vertices << worldPos.x() << worldPos.y() << worldPos.z(); m_shaderProgram->setUniformValue(m_colorLoc, QVector4D(1.0f, 1.0f, 0.0f, 1.0f)); m_vbo.bind(); m_vbo.allocate(vertices.constData(), vertices.size() * sizeof(GLfloat)); glDrawArrays(GL_LINES, 0, 2); if (m_isSnappingHorizontal || m_isSnappingVertical) { vertices.clear(); QVector3D startPos(m_firstLinePoint.X(), m_firstLinePoint.Y(), m_firstLinePoint.Z()); QVector3D midPoint = (startPos + worldPos) / 2.0; const float indicatorSize = 0.02f * -m_camera->zoom(); const float indicatorOffset = 0.02f * -m_camera->zoom(); if (m_isSnappingHorizontal) { if (m_currentPlane == SketchPlane::XY) { vertices << midPoint.x() - indicatorSize << midPoint.y() << midPoint.z() + indicatorOffset; vertices << midPoint.x() + indicatorSize << midPoint.y() << midPoint.z() + indicatorOffset; } else if (m_currentPlane == SketchPlane::XZ) { vertices << midPoint.x() - indicatorSize << midPoint.y() + indicatorOffset << midPoint.z(); vertices << midPoint.x() + indicatorSize << midPoint.y() + indicatorOffset << midPoint.z(); } else if (m_currentPlane == SketchPlane::YZ) { vertices << midPoint.x() << midPoint.y() - indicatorSize << midPoint.z() + indicatorOffset; vertices << midPoint.x() << midPoint.y() + indicatorSize << midPoint.z() + indicatorOffset; } } else { // m_isSnappingVertical if (m_currentPlane == SketchPlane::XY) { vertices << midPoint.x() + indicatorOffset << midPoint.y() << midPoint.z() - indicatorSize; vertices << midPoint.x() + indicatorOffset << midPoint.y() << midPoint.z() + indicatorSize; } else if (m_currentPlane == SketchPlane::XZ) { vertices << midPoint.x() + indicatorOffset << midPoint.y() - indicatorSize << midPoint.z(); vertices << midPoint.x() + indicatorOffset << midPoint.y() + indicatorSize << midPoint.z(); } else if (m_currentPlane == SketchPlane::YZ) { vertices << midPoint.x() << midPoint.y() + indicatorOffset << midPoint.z() - indicatorSize; vertices << midPoint.x() << midPoint.y() + indicatorOffset << midPoint.z() + indicatorSize; } } m_vbo.bind(); m_vbo.allocate(vertices.constData(), vertices.size() * sizeof(GLfloat)); glDrawArrays(GL_LINES, 0, 2); } } m_shaderProgram->release(); // View cube rendering QMatrix4x4 viewCubeModel; viewCubeModel.rotate(m_camera->xRotation() / 16.0f, 1, 0, 0); viewCubeModel.rotate(m_camera->yRotation() / 16.0f, 0, 1, 0); m_viewCube->paintGL(m_shaderProgram, m_colorLoc, viewCubeModel, width(), height()); glViewport(0, 0, width(), height()); glDisable(GL_DEPTH_TEST); glDisable(GL_CULL_FACE); QPainter painter(this); if (m_currentPlane != SketchPlane::NONE) { drawAxisLabels(painter, model, projection); } m_featureBrowser->paint(painter, width(), height()); painter.end(); } void ViewportWidget::resizeGL(int w, int h) { projection.setToIdentity(); projection.perspective(45.0f, w / float(h), 0.01f, 100.0f); } void ViewportWidget::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { if (m_isSelectingPlane) { if (m_highlightedPlane != SketchPlane::NONE) { emit planeSelected(m_highlightedPlane); m_isSelectingPlane = false; m_highlightedPlane = SketchPlane::NONE; update(); } return; } if (m_currentPlane != SketchPlane::NONE && m_activeTool == static_cast(ApplicationController::ToolType::Line)) { gp_Pnt p; if (m_isSnappingOrigin) { p.SetCoord(0, 0, 0); } else if (m_isSnappingVertex) { p = m_snapVertex; } else { QVector3D worldPos = unproject(event->pos(), m_currentPlane); if (m_isSnappingHorizontal) { if (m_currentPlane == SketchPlane::XY) worldPos.setZ(m_firstLinePoint.Z()); else if (m_currentPlane == SketchPlane::XZ) worldPos.setY(m_firstLinePoint.Y()); else if (m_currentPlane == SketchPlane::YZ) worldPos.setZ(m_firstLinePoint.Z()); } else if (m_isSnappingVertical) { if (m_currentPlane == SketchPlane::XY) worldPos.setX(m_firstLinePoint.X()); else if (m_currentPlane == SketchPlane::XZ) worldPos.setX(m_firstLinePoint.X()); else if (m_currentPlane == SketchPlane::YZ) worldPos.setY(m_firstLinePoint.Y()); } p.SetCoord(worldPos.x(), worldPos.y(), worldPos.z()); } if (!m_isDefiningLine) { m_firstLinePoint = p; m_isDefiningLine = true; } else { emit lineAdded(m_firstLinePoint, p); m_firstLinePoint = p; } update(); } } else { lastPos = event->pos(); } } void ViewportWidget::mouseMoveEvent(QMouseEvent *event) { m_currentMousePos = event->pos(); if (m_isSelectingPlane) { SketchPlane newHighlight = checkPlaneSelection(m_currentMousePos); if (newHighlight != m_highlightedPlane) { m_highlightedPlane = newHighlight; update(); } } bool shouldSnap = false; if (m_currentPlane != SketchPlane::NONE && m_activeTool != static_cast(ApplicationController::ToolType::None)) { QVector3D worldPos = unproject(m_currentMousePos, m_currentPlane); const float snapRectHalfSize = 0.0075f * -m_camera->zoom(); switch (m_currentPlane) { case SketchPlane::XY: shouldSnap = qAbs(worldPos.x()) < snapRectHalfSize && qAbs(worldPos.z()) < snapRectHalfSize; break; case SketchPlane::XZ: shouldSnap = qAbs(worldPos.x()) < snapRectHalfSize && qAbs(worldPos.y()) < snapRectHalfSize; break; case SketchPlane::YZ: shouldSnap = qAbs(worldPos.y()) < snapRectHalfSize && qAbs(worldPos.z()) < snapRectHalfSize; break; case SketchPlane::NONE: break; } } if (shouldSnap != m_isSnappingOrigin) { m_isSnappingOrigin = shouldSnap; if (m_isSnappingOrigin) { m_isSnappingVertex = false; } update(); } bool oldIsSnappingVertex = m_isSnappingVertex; m_isSnappingVertex = false; if (!m_isSnappingOrigin && m_document && m_currentPlane != SketchPlane::NONE && m_activeTool != static_cast(ApplicationController::ToolType::None)) { QVector3D worldPos = unproject(m_currentMousePos, m_currentPlane); const float snapRectHalfSize = 0.0075f * -m_camera->zoom(); for (Feature* feature : m_document->features()) { if (auto sketch = dynamic_cast(feature)) { for (const auto& obj : sketch->objects()) { if (obj->type() == SketchObject::ObjectType::Line) { auto line = static_cast(obj); const gp_Pnt vertices[] = {line->startPoint(), line->endPoint()}; for (const auto& vertex : vertices) { bool isClose = false; switch (m_currentPlane) { case SketchPlane::XY: isClose = qAbs(worldPos.x() - vertex.X()) < snapRectHalfSize && qAbs(worldPos.z() - vertex.Z()) < snapRectHalfSize; break; case SketchPlane::XZ: isClose = qAbs(worldPos.x() - vertex.X()) < snapRectHalfSize && qAbs(worldPos.y() - vertex.Y()) < snapRectHalfSize; break; case SketchPlane::YZ: isClose = qAbs(worldPos.y() - vertex.Y()) < snapRectHalfSize && qAbs(worldPos.z() - vertex.Z()) < snapRectHalfSize; break; case SketchPlane::NONE: break; } if (isClose) { m_isSnappingVertex = true; m_snapVertex = vertex; goto end_snap_check; } } } } } } end_snap_check:; } if (oldIsSnappingVertex != m_isSnappingVertex) { update(); } bool oldIsSnappingHorizontal = m_isSnappingHorizontal; bool oldIsSnappingVertical = m_isSnappingVertical; m_isSnappingHorizontal = false; m_isSnappingVertical = false; if (m_isDefiningLine && !m_isSnappingOrigin && !m_isSnappingVertex) { QVector3D worldPos = unproject(m_currentMousePos, m_currentPlane); QVector3D startPos(m_firstLinePoint.X(), m_firstLinePoint.Y(), m_firstLinePoint.Z()); QVector3D delta = worldPos - startPos; if (delta.length() > 1e-6) { const double snapAngleThreshold = qDegreesToRadians(2.0); double angle = 0; if (m_currentPlane == SketchPlane::XY) { angle = atan2(delta.z(), delta.x()); } else if (m_currentPlane == SketchPlane::XZ) { angle = atan2(delta.y(), delta.x()); } else if (m_currentPlane == SketchPlane::YZ) { angle = atan2(delta.z(), delta.y()); } if (qAbs(sin(angle)) < sin(snapAngleThreshold)) { m_isSnappingHorizontal = true; } else if (qAbs(cos(angle)) < sin(snapAngleThreshold)) { m_isSnappingVertical = true; } } } if (oldIsSnappingHorizontal != m_isSnappingHorizontal || oldIsSnappingVertical != m_isSnappingVertical) { update(); } if (event->buttons() & Qt::MiddleButton) { m_camera->processMouseMovement(event, lastPos); } lastPos = event->pos(); update(); } void ViewportWidget::wheelEvent(QWheelEvent *event) { m_camera->processWheel(event); } void ViewportWidget::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Escape) { if (m_isDefiningLine) { m_isDefiningLine = false; emit toolDeactivated(); update(); return; } if (m_isSelectingPlane) { m_isSelectingPlane = false; m_highlightedPlane = SketchPlane::NONE; m_currentPlane = SketchPlane::XY; update(); return; } } QOpenGLWidget::keyPressEvent(event); } void ViewportWidget::onSketchModeStarted(SketchPlane plane) { m_currentPlane = plane; m_camera->saveState(); float targetXRot = m_camera->xRotation(); float targetYRot = m_camera->yRotation(); switch (plane) { case SketchPlane::XY: // Top view targetXRot = 90 * 16; targetYRot = 0; break; case SketchPlane::XZ: // Front view targetXRot = 0; targetYRot = 0; break; case SketchPlane::YZ: // Right view targetXRot = 0; targetYRot = -90 * 16; break; case SketchPlane::NONE: break; } auto* animGroup = new QParallelAnimationGroup(this); auto* xRotAnim = new QPropertyAnimation(m_camera, "xRotation"); xRotAnim->setDuration(300); xRotAnim->setStartValue(m_camera->xRotation()); xRotAnim->setEndValue(targetXRot); xRotAnim->setEasingCurve(QEasingCurve::InOutQuad); animGroup->addAnimation(xRotAnim); auto* yRotAnim = new QPropertyAnimation(m_camera, "yRotation"); yRotAnim->setDuration(300); yRotAnim->setStartValue(m_camera->yRotation()); yRotAnim->setEndValue(targetYRot); yRotAnim->setEasingCurve(QEasingCurve::InOutQuad); animGroup->addAnimation(yRotAnim); auto* panXAnim = new QPropertyAnimation(m_camera, "panX"); panXAnim->setDuration(300); panXAnim->setStartValue(m_camera->panX()); panXAnim->setEndValue(0.0f); panXAnim->setEasingCurve(QEasingCurve::InOutQuad); animGroup->addAnimation(panXAnim); auto* panYAnim = new QPropertyAnimation(m_camera, "panY"); panYAnim->setDuration(300); panYAnim->setStartValue(m_camera->panY()); panYAnim->setEndValue(0.0f); panYAnim->setEasingCurve(QEasingCurve::InOutQuad); animGroup->addAnimation(panYAnim); auto* zoomAnim = new QPropertyAnimation(m_camera, "zoom"); zoomAnim->setDuration(300); zoomAnim->setStartValue(m_camera->zoom()); zoomAnim->setEndValue(-20.0f); zoomAnim->setEasingCurve(QEasingCurve::InOutQuad); animGroup->addAnimation(zoomAnim); animGroup->start(QAbstractAnimation::DeleteWhenStopped); } void ViewportWidget::onPlaneSelectionModeStarted() { m_isSelectingPlane = true; m_highlightedPlane = SketchPlane::NONE; m_currentPlane = SketchPlane::NONE; update(); } void ViewportWidget::onSketchModeEnded() { auto* animGroup = new QParallelAnimationGroup(this); auto* xRotAnim = new QPropertyAnimation(m_camera, "xRotation"); xRotAnim->setDuration(300); xRotAnim->setStartValue(m_camera->xRotation()); xRotAnim->setEndValue(m_camera->savedXRot()); xRotAnim->setEasingCurve(QEasingCurve::InOutQuad); animGroup->addAnimation(xRotAnim); auto* yRotAnim = new QPropertyAnimation(m_camera, "yRotation"); yRotAnim->setDuration(300); yRotAnim->setStartValue(m_camera->yRotation()); yRotAnim->setEndValue(m_camera->savedYRot()); yRotAnim->setEasingCurve(QEasingCurve::InOutQuad); animGroup->addAnimation(yRotAnim); auto* panXAnim = new QPropertyAnimation(m_camera, "panX"); panXAnim->setDuration(300); panXAnim->setStartValue(m_camera->panX()); panXAnim->setEndValue(m_camera->savedPanX()); panXAnim->setEasingCurve(QEasingCurve::InOutQuad); animGroup->addAnimation(panXAnim); auto* panYAnim = new QPropertyAnimation(m_camera, "panY"); panYAnim->setDuration(300); panYAnim->setStartValue(m_camera->panY()); panYAnim->setEndValue(m_camera->savedPanY()); panYAnim->setEasingCurve(QEasingCurve::InOutQuad); animGroup->addAnimation(panYAnim); auto* zoomAnim = new QPropertyAnimation(m_camera, "zoom"); zoomAnim->setDuration(300); zoomAnim->setStartValue(m_camera->zoom()); zoomAnim->setEndValue(m_camera->savedZoom()); zoomAnim->setEasingCurve(QEasingCurve::InOutQuad); animGroup->addAnimation(zoomAnim); connect(animGroup, &QParallelAnimationGroup::finished, this, [this]() { // Return to showing the base XY grid when not in a sketch m_currentPlane = SketchPlane::XY; update(); }); animGroup->start(QAbstractAnimation::DeleteWhenStopped); } QVector3D ViewportWidget::project(const QVector3D& worldCoord, const QMatrix4x4& modelView, const QMatrix4x4& projection, const QRect& viewport) { QVector4D clipCoord = projection * modelView * QVector4D(worldCoord, 1.0); if (qFuzzyCompare(clipCoord.w(), 0.0f)) { return QVector3D(-1, -1, -1); } QVector3D ndc(clipCoord.x() / clipCoord.w(), clipCoord.y() / clipCoord.w(), clipCoord.z() / clipCoord.w()); float winX = viewport.x() + viewport.width() * (ndc.x() + 1.0) / 2.0; float winY = viewport.y() + viewport.height() * (1.0 - (ndc.y() + 1.0) / 2.0); // Y is inverted return QVector3D(winX, winY, (ndc.z() + 1.0) / 2.0); } void ViewportWidget::drawAxisLabels(QPainter& painter, const QMatrix4x4& modelView, const QMatrix4x4& projection) { painter.setPen(Qt::white); painter.setFont(QFont("Arial", 10)); const int range = 50; const int step = 5; auto drawLabelsForAxis = [&](int axis_idx) { for (int i = -range; i <= range; i += step) { if (i == 0) continue; QVector3D worldCoord; worldCoord[axis_idx] = i; QVector3D screenPos = project(worldCoord, modelView, projection, rect()); if (screenPos.z() < 1.0f) { // Not clipped painter.drawText(screenPos.toPoint(), QString::number(i)); } } }; if (m_currentPlane == SketchPlane::XY) { drawLabelsForAxis(0); // X drawLabelsForAxis(2); // Y } else if (m_currentPlane == SketchPlane::XZ) { drawLabelsForAxis(0); // X drawLabelsForAxis(1); // Z } else if (m_currentPlane == SketchPlane::YZ) { drawLabelsForAxis(1); // Y drawLabelsForAxis(2); // Z } } void ViewportWidget::onActiveToolChanged(int tool) { m_activeTool = tool; m_isDefiningLine = false; if (tool == static_cast(ApplicationController::ToolType::None)) { unsetCursor(); } else { if (m_toolIcons.contains(tool)) { QSvgRenderer* renderer = m_toolIcons.value(tool); if (renderer && renderer->isValid()) { const QSize cursorSize(48, 48); QPixmap cursorPixmap(cursorSize); cursorPixmap.fill(Qt::transparent); QPainter painter(&cursorPixmap); painter.setRenderHint(QPainter::Antialiasing); // Render arrow cursor from SVG if (m_cursorRenderer && m_cursorRenderer->isValid()) { m_cursorRenderer->render(&painter, QRectF(0, 0, 14, 22)); } // Render tool icon const QSize iconSize(32, 32); QPixmap iconPixmap(iconSize); iconPixmap.fill(Qt::transparent); QPainter iconPainter(&iconPixmap); renderer->render(&iconPainter); iconPainter.end(); // Invert and draw icon onto cursor pixmap QImage iconImage = iconPixmap.toImage(); iconImage.invertPixels(QImage::InvertRgb); painter.drawImage(QRect(12, 12, iconSize.width(), iconSize.height()), iconImage); painter.end(); setCursor(QCursor(cursorPixmap, 0, 0)); } } else { unsetCursor(); } } } QVector3D ViewportWidget::unproject(const QPoint& screenPos, SketchPlane plane) { QMatrix4x4 model = m_camera->modelViewMatrix(); bool invertible; QMatrix4x4 inv = (projection * model).inverted(&invertible); if (!invertible) { return QVector3D(); } float ndcX = (2.0f * screenPos.x()) / width() - 1.0f; float ndcY = 1.0f - (2.0f * screenPos.y()) / height(); QVector4D nearPoint_ndc(ndcX, ndcY, -1.0f, 1.0f); QVector4D farPoint_ndc(ndcX, ndcY, 1.0f, 1.0f); QVector4D nearPoint_world = inv * nearPoint_ndc; QVector4D farPoint_world = inv * farPoint_ndc; if (qFuzzyCompare(nearPoint_world.w(), 0.0f) || qFuzzyCompare(farPoint_world.w(), 0.0f)) { return QVector3D(); } nearPoint_world /= nearPoint_world.w(); farPoint_world /= farPoint_world.w(); QVector3D rayOrigin(nearPoint_world); QVector3D rayDir = (QVector3D(farPoint_world) - rayOrigin).normalized(); QVector3D planeNormal; switch (plane) { case SketchPlane::XY: planeNormal = QVector3D(0, 1, 0); break; case SketchPlane::XZ: planeNormal = QVector3D(0, 0, 1); break; case SketchPlane::YZ: planeNormal = QVector3D(1, 0, 0); break; case SketchPlane::NONE: return QVector3D(); } float denom = QVector3D::dotProduct(planeNormal, rayDir); if (qAbs(denom) > 1e-6) { QVector3D p0(0,0,0); float t = QVector3D::dotProduct(p0 - rayOrigin, planeNormal) / denom; return rayOrigin + t * rayDir; } return QVector3D(); } void ViewportWidget::drawSketch(const SketchFeature* sketch) { glDisable(GL_DEPTH_TEST); glLineWidth(2.0f); QVector lineVertices; std::map vertexCounts; for (const auto& obj : sketch->objects()) { if (obj->type() == SketchObject::ObjectType::Line) { auto line = static_cast(obj); const auto& start = line->startPoint(); const auto& end = line->endPoint(); lineVertices << start.X() << start.Y() << start.Z(); lineVertices << end.X() << end.Y() << end.Z(); vertexCounts[start]++; vertexCounts[end]++; } } QVector circleVertices; const float radius = 0.005f * -m_camera->zoom(); const int numSegments = 16; QMatrix4x4 invModelView = m_camera->modelViewMatrix().inverted(); QVector3D rightVec = invModelView.column(0).toVector3D(); QVector3D upVec = invModelView.column(1).toVector3D(); for (const auto& pair : vertexCounts) { if (pair.second == 1) { const gp_Pnt& centerPnt = pair.first; QVector3D center(centerPnt.X(), centerPnt.Y(), centerPnt.Z()); for (int i = 0; i < numSegments; ++i) { float angle1 = (2.0f * M_PI * float(i)) / float(numSegments); float angle2 = (2.0f * M_PI * float(i + 1)) / float(numSegments); QVector3D p1 = center + radius * (cos(angle1) * rightVec + sin(angle1) * upVec); QVector3D p2 = center + radius * (cos(angle2) * rightVec + sin(angle2) * upVec); circleVertices << p1.x() << p1.y() << p1.z(); circleVertices << p2.x() << p2.y() << p2.z(); } } } m_shaderProgram->setUniformValue(m_colorLoc, QVector4D(1.0f, 1.0f, 1.0f, 1.0f)); m_vbo.bind(); if (!lineVertices.isEmpty()) { m_vbo.allocate(lineVertices.constData(), lineVertices.size() * sizeof(GLfloat)); glDrawArrays(GL_LINES, 0, lineVertices.size() / 3); } if (!circleVertices.isEmpty()) { m_vbo.allocate(circleVertices.constData(), circleVertices.size() * sizeof(GLfloat)); glDrawArrays(GL_LINES, 0, circleVertices.size() / 3); } glEnable(GL_DEPTH_TEST); } void ViewportWidget::initShaders() { m_shaderProgram = new QOpenGLShaderProgram(this); if (!m_shaderProgram->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/simple.vert")) { qCritical() << "Vertex shader compilation failed:" << m_shaderProgram->log(); return; } if (!m_shaderProgram->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/simple.frag")) { qCritical() << "Fragment shader compilation failed:" << m_shaderProgram->log(); return; } if (!m_shaderProgram->link()) { qCritical() << "Shader program linking failed:" << m_shaderProgram->log(); return; } m_projMatrixLoc = m_shaderProgram->uniformLocation("projectionMatrix"); m_mvMatrixLoc = m_shaderProgram->uniformLocation("modelViewMatrix"); m_colorLoc = m_shaderProgram->uniformLocation("objectColor"); } void ViewportWidget::drawSelectionPlanes() { const float planeSize = 5.0f; const float planeOffset = 1.0f; glDisable(GL_CULL_FACE); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glLineWidth(2.0f); glDisable(GL_DEPTH_TEST); m_vbo.bind(); QVector vertices; auto drawPlane = [&](const QVector& quadVerts, bool highlighted) { // Draw fill if (highlighted) { m_shaderProgram->setUniformValue(m_colorLoc, QVector4D(1.0f, 1.0f, 0.0f, 0.5f)); m_vbo.allocate(quadVerts.constData(), quadVerts.size() * sizeof(GLfloat)); glDrawArrays(GL_TRIANGLE_FAN, 0, 4); } // Draw outline m_shaderProgram->setUniformValue(m_colorLoc, QVector4D(1.0f, 1.0f, 0.0f, 1.0f)); m_vbo.allocate(quadVerts.constData(), quadVerts.size() * sizeof(GLfloat)); glDrawArrays(GL_LINE_LOOP, 0, 4); }; // XY Plane (Top) QVector xyQuad = { planeOffset, 0, planeOffset, planeOffset + planeSize, 0, planeOffset, planeOffset + planeSize, 0, planeOffset + planeSize, planeOffset, 0, planeOffset + planeSize }; drawPlane(xyQuad, m_highlightedPlane == SketchPlane::XY); // XZ Plane (Front) QVector xzQuad = { planeOffset, planeOffset, 0, planeOffset + planeSize, planeOffset, 0, planeOffset + planeSize, planeOffset + planeSize, 0, planeOffset, planeOffset + planeSize, 0 }; drawPlane(xzQuad, m_highlightedPlane == SketchPlane::XZ); // YZ Plane (Right) QVector yzQuad = { 0, planeOffset, planeOffset, 0, planeOffset + planeSize, planeOffset, 0, planeOffset + planeSize, planeOffset + planeSize, 0, planeOffset, planeOffset + planeSize }; drawPlane(yzQuad, m_highlightedPlane == SketchPlane::YZ); glEnable(GL_CULL_FACE); glEnable(GL_DEPTH_TEST); glDisable(GL_BLEND); } ViewportWidget::SketchPlane ViewportWidget::checkPlaneSelection(const QPoint& screenPos) { const float planeSize = 5.0f; const float planeOffset = 1.0f; QVector3D intersection; // Check front to back to handle overlaps // YZ plane (Right) intersection = unproject(screenPos, SketchPlane::YZ); if (intersection.y() >= planeOffset && intersection.y() <= planeOffset + planeSize && intersection.z() >= planeOffset && intersection.z() <= planeOffset + planeSize) { return SketchPlane::YZ; } // XZ plane (Front) intersection = unproject(screenPos, SketchPlane::XZ); if (intersection.x() >= planeOffset && intersection.x() <= planeOffset + planeSize && intersection.y() >= planeOffset && intersection.y() <= planeOffset + planeSize) { return SketchPlane::XZ; } // XY plane (Top) intersection = unproject(screenPos, SketchPlane::XY); if (intersection.x() >= planeOffset && intersection.x() <= planeOffset + planeSize && intersection.z() >= planeOffset && intersection.z() <= planeOffset + planeSize) { return SketchPlane::XY; } return SketchPlane::NONE; }