From 6e5a819c42f73ffcea570614f7b6bd4b89783cd2 Mon Sep 17 00:00:00 2001 From: Tanner Collin Date: Sat, 14 Feb 2026 22:26:38 -0700 Subject: [PATCH] feat: Add horizontal/vertical line snapping with visual indicator Co-authored-by: aider (gemini/gemini-2.5-pro) --- src/ViewportWidget.cpp | 88 ++++++++++++++++++++++++++++++++++++++++++ src/ViewportWidget.h | 2 + 2 files changed, 90 insertions(+) diff --git a/src/ViewportWidget.cpp b/src/ViewportWidget.cpp index 7de33bb..26ec0ec 100644 --- a/src/ViewportWidget.cpp +++ b/src/ViewportWidget.cpp @@ -19,6 +19,8 @@ #include #include #include +#include +#include ViewportWidget::ViewportWidget(QWidget *parent) : QOpenGLWidget(parent) @@ -197,12 +199,55 @@ void ViewportWidget::paintGL() 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.setY(m_firstLinePoint.Y()); + else if (m_currentPlane == SketchPlane::XZ) worldPos.setZ(m_firstLinePoint.Z()); + 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()); } glBegin(GL_LINES); glColor3f(1.0, 1.0, 0.0); glVertex3d(m_firstLinePoint.X(), m_firstLinePoint.Y(), m_firstLinePoint.Z()); glVertex3d(worldPos.x(), worldPos.y(), worldPos.z()); glEnd(); + + if (m_isSnappingHorizontal || m_isSnappingVertical) { + QVector3D startPos(m_firstLinePoint.X(), m_firstLinePoint.Y(), m_firstLinePoint.Z()); + QVector3D midPoint = (startPos + worldPos) / 2.0; + const float indicatorSize = 0.02f * -m_zoom; + const float indicatorOffset = 0.02f * -m_zoom; + + glColor3f(1.0, 1.0, 0.0); + glBegin(GL_LINES); + + if (m_isSnappingHorizontal) { + if (m_currentPlane == SketchPlane::XY) { + glVertex3f(midPoint.x() - indicatorSize, midPoint.y() + indicatorOffset, midPoint.z()); + glVertex3f(midPoint.x() + indicatorSize, midPoint.y() + indicatorOffset, midPoint.z()); + } else if (m_currentPlane == SketchPlane::XZ) { + glVertex3f(midPoint.x() - indicatorSize, midPoint.y(), midPoint.z() + indicatorOffset); + glVertex3f(midPoint.x() + indicatorSize, midPoint.y(), midPoint.z() + indicatorOffset); + } else if (m_currentPlane == SketchPlane::YZ) { + glVertex3f(midPoint.x(), midPoint.y() - indicatorSize, midPoint.z() + indicatorOffset); + glVertex3f(midPoint.x(), midPoint.y() + indicatorSize, midPoint.z() + indicatorOffset); + } + } else { // m_isSnappingVertical + if (m_currentPlane == SketchPlane::XY) { + glVertex3f(midPoint.x() + indicatorOffset, midPoint.y() - indicatorSize, midPoint.z()); + glVertex3f(midPoint.x() + indicatorOffset, midPoint.y() + indicatorSize, midPoint.z()); + } else if (m_currentPlane == SketchPlane::XZ) { + glVertex3f(midPoint.x() + indicatorOffset, midPoint.y(), midPoint.z() - indicatorSize); + glVertex3f(midPoint.x() + indicatorOffset, midPoint.y(), midPoint.z() + indicatorSize); + } else if (m_currentPlane == SketchPlane::YZ) { + glVertex3f(midPoint.x(), midPoint.y() + indicatorOffset, midPoint.z() - indicatorSize); + glVertex3f(midPoint.x(), midPoint.y() + indicatorOffset, midPoint.z() + indicatorSize); + } + } + glEnd(); + } } // View cube rendering @@ -241,6 +286,15 @@ void ViewportWidget::mousePressEvent(QMouseEvent *event) p = m_snapVertex; } else { QVector3D worldPos = unproject(event->pos()); + if (m_isSnappingHorizontal) { + if (m_currentPlane == SketchPlane::XY) worldPos.setY(m_firstLinePoint.Y()); + else if (m_currentPlane == SketchPlane::XZ) worldPos.setZ(m_firstLinePoint.Z()); + 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()); } @@ -334,6 +388,40 @@ void ViewportWidget::mouseMoveEvent(QMouseEvent *event) 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); + 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.y(), delta.x()); + } else if (m_currentPlane == SketchPlane::XZ) { + angle = atan2(delta.z(), 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(); + } + int dx = event->pos().x() - lastPos.x(); int dy = event->pos().y() - lastPos.y(); diff --git a/src/ViewportWidget.h b/src/ViewportWidget.h index 62f2bc8..00927ae 100644 --- a/src/ViewportWidget.h +++ b/src/ViewportWidget.h @@ -93,6 +93,8 @@ private: bool m_isSnappingOrigin = false; bool m_isSnappingVertex = false; gp_Pnt m_snapVertex; + bool m_isSnappingHorizontal = false; + bool m_isSnappingVertical = false; QMap m_toolIcons; QSvgRenderer* m_cursorRenderer = nullptr;