From 8424097127327c4dbfd0b891119bc903d2e9b719 Mon Sep 17 00:00:00 2001 From: Tanner Collin Date: Sun, 15 Feb 2026 11:09:28 -0700 Subject: [PATCH] feat: Implement in-viewport sketch plane selection Co-authored-by: aider (gemini/gemini-2.5-pro) --- src/ApplicationController.cpp | 39 ++++---- src/ApplicationController.h | 2 + src/ViewportWidget.cpp | 167 ++++++++++++++++++++++++++++++---- src/ViewportWidget.h | 9 +- 4 files changed, 181 insertions(+), 36 deletions(-) diff --git a/src/ApplicationController.cpp b/src/ApplicationController.cpp index 113539b..5255b65 100644 --- a/src/ApplicationController.cpp +++ b/src/ApplicationController.cpp @@ -102,29 +102,32 @@ bool ApplicationController::saveDocumentAs() void ApplicationController::beginSketchCreation() { - QStringList items; - items << "Top (XY)" << "Front (XZ)" << "Right (YZ)"; + emit planeSelectionModeStarted(); +} - bool ok; - QString item = QInputDialog::getItem(m_mainWindow, "Select Sketch Plane", - "Plane:", items, 0, false, &ok); - if (ok && !item.isEmpty()) { - auto feature = new SketchFeature("Sketch"); - m_activeSketch = feature; - ViewportWidget::SketchPlane plane; - if (item == "Top (XY)") { - plane = ViewportWidget::SketchPlane::XY; +void ApplicationController::onPlaneSelected(ViewportWidget::SketchPlane plane) +{ + auto feature = new SketchFeature("Sketch"); + m_activeSketch = feature; + + switch (plane) { + case ViewportWidget::SketchPlane::XY: feature->setPlane(SketchFeature::SketchPlane::XY); - } else if (item == "Front (XZ)") { - plane = ViewportWidget::SketchPlane::XZ; + break; + case ViewportWidget::SketchPlane::XZ: feature->setPlane(SketchFeature::SketchPlane::XZ); - } else { // "Right (YZ)" - plane = ViewportWidget::SketchPlane::YZ; + break; + case ViewportWidget::SketchPlane::YZ: feature->setPlane(SketchFeature::SketchPlane::YZ); - } - m_document->addFeature(feature); - emit sketchModeStarted(plane); + break; + case ViewportWidget::SketchPlane::NONE: + delete feature; + m_activeSketch = nullptr; + return; } + + m_document->addFeature(feature); + emit sketchModeStarted(plane); } void ApplicationController::addLine(const gp_Pnt& start, const gp_Pnt& end) diff --git a/src/ApplicationController.h b/src/ApplicationController.h index 15031d7..72498ad 100644 --- a/src/ApplicationController.h +++ b/src/ApplicationController.h @@ -38,9 +38,11 @@ public slots: bool saveDocumentAs(); void beginSketchCreation(); + void onPlaneSelected(ViewportWidget::SketchPlane plane); void endSketch(); signals: + void planeSelectionModeStarted(); void sketchModeStarted(ViewportWidget::SketchPlane plane); void sketchModeEnded(); void currentFileChanged(const QString& path); diff --git a/src/ViewportWidget.cpp b/src/ViewportWidget.cpp index 238c09c..6154ea8 100644 --- a/src/ViewportWidget.cpp +++ b/src/ViewportWidget.cpp @@ -135,6 +135,10 @@ void ViewportWidget::paintGL() glMatrixMode(GL_MODELVIEW); glLoadMatrixf(model.constData()); + if (m_isSelectingPlane) { + drawSelectionPlanes(); + } + if (m_currentPlane != SketchPlane::NONE) { m_sketchGrid->paintGL(static_cast(m_currentPlane), projection, model); } @@ -198,7 +202,7 @@ void ViewportWidget::paintGL() } if (m_isDefiningLine && m_activeTool == static_cast(ApplicationController::ToolType::Line)) { - QVector3D worldPos = unproject(m_currentMousePos); + QVector3D worldPos = unproject(m_currentMousePos, m_currentPlane); if (m_isSnappingOrigin) { worldPos.setX(0); worldPos.setY(0); @@ -286,16 +290,27 @@ void ViewportWidget::resizeGL(int w, int h) void ViewportWidget::mousePressEvent(QMouseEvent *event) { - if (event->button() == Qt::LeftButton && 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()); - if (m_isSnappingHorizontal) { - if (m_currentPlane == SketchPlane::XY) worldPos.setZ(m_firstLinePoint.Z()); + 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) { @@ -312,8 +327,9 @@ void ViewportWidget::mousePressEvent(QMouseEvent *event) } else { emit lineAdded(m_firstLinePoint, p); m_firstLinePoint = p; + } + update(); } - update(); } else { lastPos = event->pos(); } @@ -323,9 +339,17 @@ 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); + QVector3D worldPos = unproject(m_currentMousePos, m_currentPlane); const float snapRectHalfSize = 0.0075f * -m_zoom; switch (m_currentPlane) { @@ -354,7 +378,7 @@ void ViewportWidget::mouseMoveEvent(QMouseEvent *event) 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); + QVector3D worldPos = unproject(m_currentMousePos, m_currentPlane); const float snapRectHalfSize = 0.0075f * -m_zoom; for (Feature* feature : m_document->features()) { @@ -402,7 +426,7 @@ void ViewportWidget::mouseMoveEvent(QMouseEvent *event) m_isSnappingVertical = false; if (m_isDefiningLine && !m_isSnappingOrigin && !m_isSnappingVertex) { - QVector3D worldPos = unproject(m_currentMousePos); + QVector3D worldPos = unproject(m_currentMousePos, m_currentPlane); QVector3D startPos(m_firstLinePoint.X(), m_firstLinePoint.Y(), m_firstLinePoint.Z()); QVector3D delta = worldPos - startPos; @@ -538,6 +562,13 @@ void ViewportWidget::onSketchModeStarted(SketchPlane plane) animGroup->start(QAbstractAnimation::DeleteWhenStopped); } +void ViewportWidget::onPlaneSelectionModeStarted() +{ + m_isSelectingPlane = true; + m_highlightedPlane = SketchPlane::NONE; + update(); +} + void ViewportWidget::onSketchModeEnded() { auto* animGroup = new QParallelAnimationGroup(this); @@ -678,7 +709,7 @@ void ViewportWidget::onActiveToolChanged(int tool) } } -QVector3D ViewportWidget::unproject(const QPoint& screenPos) +QVector3D ViewportWidget::unproject(const QPoint& screenPos, SketchPlane plane) { QMatrix4x4 model; model.translate(m_panX, m_panY, m_zoom); @@ -711,7 +742,7 @@ QVector3D ViewportWidget::unproject(const QPoint& screenPos) QVector3D rayDir = (QVector3D(farPoint_world) - rayOrigin).normalized(); QVector3D planeNormal; - switch (m_currentPlane) { + 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; @@ -754,3 +785,105 @@ void ViewportWidget::drawSketch(const SketchFeature* sketch) glEnable(GL_DEPTH_TEST); } +void ViewportWidget::drawSelectionPlanes() +{ + const float planeSize = 5.0f; + const float planeOffset = 1.0f; + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glLineWidth(2.0f); + glDisable(GL_DEPTH_TEST); + + // Draw back to front for proper blending + + // XY Plane (Top), normal is Y (green) + glColor4f(0.0f, 1.0f, 0.0f, m_highlightedPlane == SketchPlane::XY ? 0.5f : 0.2f); + if (m_highlightedPlane == SketchPlane::XY) { + glBegin(GL_QUADS); + glVertex3f(planeOffset, 0, planeOffset); + glVertex3f(planeOffset + planeSize, 0, planeOffset); + glVertex3f(planeOffset + planeSize, 0, planeOffset + planeSize); + glVertex3f(planeOffset, 0, planeOffset + planeSize); + glEnd(); + } + glColor4f(0.0f, 1.0f, 0.0f, 1.0f); + glBegin(GL_LINE_LOOP); + glVertex3f(planeOffset, 0, planeOffset); + glVertex3f(planeOffset + planeSize, 0, planeOffset); + glVertex3f(planeOffset + planeSize, 0, planeOffset + planeSize); + glVertex3f(planeOffset, 0, planeOffset + planeSize); + glEnd(); + + // XZ Plane (Front), normal is Z (blue) + glColor4f(0.0f, 0.0f, 1.0f, m_highlightedPlane == SketchPlane::XZ ? 0.5f : 0.2f); + if (m_highlightedPlane == SketchPlane::XZ) { + glBegin(GL_QUADS); + glVertex3f(planeOffset, planeOffset, 0); + glVertex3f(planeOffset + planeSize, planeOffset, 0); + glVertex3f(planeOffset + planeSize, planeOffset + planeSize, 0); + glVertex3f(planeOffset, planeOffset + planeSize, 0); + glEnd(); + } + glColor4f(0.0f, 0.0f, 1.0f, 1.0f); + glBegin(GL_LINE_LOOP); + glVertex3f(planeOffset, planeOffset, 0); + glVertex3f(planeOffset + planeSize, planeOffset, 0); + glVertex3f(planeOffset + planeSize, planeOffset + planeSize, 0); + glVertex3f(planeOffset, planeOffset + planeSize, 0); + glEnd(); + + // YZ Plane (Right), normal is X (red) + glColor4f(1.0f, 0.0f, 0.0f, m_highlightedPlane == SketchPlane::YZ ? 0.5f : 0.2f); + if (m_highlightedPlane == SketchPlane::YZ) { + glBegin(GL_QUADS); + glVertex3f(0, planeOffset, planeOffset); + glVertex3f(0, planeOffset + planeSize, planeOffset); + glVertex3f(0, planeOffset + planeSize, planeOffset + planeSize); + glVertex3f(0, planeOffset, planeOffset + planeSize); + glEnd(); + } + glColor4f(1.0f, 0.0f, 0.0f, 1.0f); + glBegin(GL_LINE_LOOP); + glVertex3f(0, planeOffset, planeOffset); + glVertex3f(0, planeOffset + planeSize, planeOffset); + glVertex3f(0, planeOffset + planeSize, planeOffset + planeSize); + glVertex3f(0, planeOffset, planeOffset + planeSize); + glEnd(); + + 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; +} + diff --git a/src/ViewportWidget.h b/src/ViewportWidget.h index 00927ae..d7059b1 100644 --- a/src/ViewportWidget.h +++ b/src/ViewportWidget.h @@ -43,6 +43,7 @@ public: public slots: void onSketchModeStarted(SketchPlane plane); void onSketchModeEnded(); + void onPlaneSelectionModeStarted(); void onActiveToolChanged(int tool); float xRotation() const; @@ -62,6 +63,7 @@ public slots: signals: void lineAdded(const gp_Pnt& start, const gp_Pnt& end); + void planeSelected(SketchPlane plane); protected: void initializeGL() override; @@ -75,9 +77,11 @@ protected: private: QVector3D project(const QVector3D& worldCoord, const QMatrix4x4& modelView, const QMatrix4x4& projection, const QRect& viewport); - QVector3D unproject(const QPoint& screenPos); + QVector3D unproject(const QPoint& screenPos, SketchPlane plane); void drawAxisLabels(QPainter& painter, const QMatrix4x4& modelView, const QMatrix4x4& projection); void drawSketch(const SketchFeature* sketch); + void drawSelectionPlanes(); + ViewportWidget::SketchPlane checkPlaneSelection(const QPoint& screenPos); QMatrix4x4 projection; ViewCube* m_viewCube; @@ -86,6 +90,9 @@ private: Document* m_document = nullptr; SketchPlane m_currentPlane = SketchPlane::NONE; + bool m_isSelectingPlane = false; + SketchPlane m_highlightedPlane = SketchPlane::NONE; + int m_activeTool = 0; bool m_isDefiningLine = false; gp_Pnt m_firstLinePoint;