From 0f274c2e071c3545dea0ee3d312d264f5807dbce Mon Sep 17 00:00:00 2001 From: Paddle Date: Wed, 15 Mar 2023 11:08:54 +0100 Subject: [PATCH] Sketcher: Snap: initial implementation: - creation of SnapManager class. - Move grid snap to this new class. - Add snap to object. - Add snap at angle. --- src/Mod/Sketcher/Gui/CMakeLists.txt | 2 + src/Mod/Sketcher/Gui/Command.cpp | 275 +++++++++++-- src/Mod/Sketcher/Gui/Resources/Sketcher.qrc | 2 + .../Resources/icons/general/Sketcher_Snap.svg | 386 ++++++++++++++++++ .../general/Sketcher_Snap_Deactivated.svg | 377 +++++++++++++++++ src/Mod/Sketcher/Gui/SnapManager.cpp | 364 +++++++++++++++++ src/Mod/Sketcher/Gui/SnapManager.h | 125 ++++++ src/Mod/Sketcher/Gui/ViewProviderSketch.cpp | 43 +- src/Mod/Sketcher/Gui/ViewProviderSketch.h | 22 +- src/Mod/Sketcher/Gui/Workbench.cpp | 3 +- 10 files changed, 1526 insertions(+), 73 deletions(-) create mode 100644 src/Mod/Sketcher/Gui/Resources/icons/general/Sketcher_Snap.svg create mode 100644 src/Mod/Sketcher/Gui/Resources/icons/general/Sketcher_Snap_Deactivated.svg create mode 100644 src/Mod/Sketcher/Gui/SnapManager.cpp create mode 100644 src/Mod/Sketcher/Gui/SnapManager.h diff --git a/src/Mod/Sketcher/Gui/CMakeLists.txt b/src/Mod/Sketcher/Gui/CMakeLists.txt index 3320d06710..5de66a2124 100644 --- a/src/Mod/Sketcher/Gui/CMakeLists.txt +++ b/src/Mod/Sketcher/Gui/CMakeLists.txt @@ -144,6 +144,8 @@ SET(SketcherGui_SRCS SketchRectangularArrayDialog.cpp SketcherRegularPolygonDialog.h SketcherRegularPolygonDialog.cpp + SnapManager.cpp + SnapManager.h TaskDlgEditSketch.cpp TaskDlgEditSketch.h ViewProviderPython.cpp diff --git a/src/Mod/Sketcher/Gui/Command.cpp b/src/Mod/Sketcher/Gui/Command.cpp index 1af8fcbeb4..6de8a85aa8 100644 --- a/src/Mod/Sketcher/Gui/Command.cpp +++ b/src/Mod/Sketcher/Gui/Command.cpp @@ -995,7 +995,7 @@ public: updateCheckBox(checkbox, propvalue); }; - updateCheckBox(gridSnap, sketchView->getSnapMode() == SnapMode::SnapToGrid); + ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Mod/Sketcher/General"); updateCheckBoxFromProperty(gridAutoSpacing, sketchView->GridAuto); @@ -1005,10 +1005,6 @@ public: void languageChange() { - gridSnap->setText(tr("Grid Snap")); - gridSnap->setToolTip(tr("New points will snap to the nearest grid line.\nPoints must be set closer than a fifth of the grid spacing to a grid line to snap.")); - gridSnap->setStatusTip(gridSnap->toolTip()); - gridAutoSpacing->setText(tr("Grid Auto Spacing")); gridAutoSpacing->setToolTip(tr("Resize grid automatically depending on zoom.")); gridAutoSpacing->setStatusTip(gridAutoSpacing->toolTip()); @@ -1020,8 +1016,6 @@ public: protected: QWidget* createWidget(QWidget* parent) override { - gridSnap = new QCheckBox(); - gridAutoSpacing = new QCheckBox(); sizeLabel = new QLabel(); @@ -1034,27 +1028,12 @@ protected: QWidget* gridSizeW = new QWidget(parent); auto* layout = new QGridLayout(gridSizeW); - layout->addWidget(gridSnap, 0, 0); - layout->addWidget(gridAutoSpacing, 1, 0); - layout->addWidget(sizeLabel, 2, 0); - layout->addWidget(gridSizeBox, 2, 1); + layout->addWidget(gridAutoSpacing, 0, 0, 1, 2); + layout->addWidget(sizeLabel, 1, 0); + layout->addWidget(gridSizeBox, 1, 1); languageChange(); - QObject::connect(gridSnap, &QCheckBox::stateChanged, [this](int state) { - auto* sketchView = getView(); - - if(sketchView) { - if(state == Qt::Checked) { - sketchView->setSnapMode(SnapMode::SnapToGrid); - } - else { - sketchView->setSnapMode(SnapMode::None); - } - } - }); - - QObject::connect(gridAutoSpacing, &QCheckBox::stateChanged, [this](int state) { auto* sketchView = getView(); @@ -1086,7 +1065,6 @@ private: } private: - QCheckBox * gridSnap; QCheckBox * gridAutoSpacing; QLabel * sizeLabel; Gui::QuantitySpinBox * gridSizeBox; @@ -1213,6 +1191,250 @@ bool CmdSketcherGrid::isActive() return false; } +/* Snap tool */ +class SnapSpaceAction : public QWidgetAction +{ +public: + SnapSpaceAction(QObject* parent) : QWidgetAction(parent) { + setEnabled(false); + } + + void updateWidget() { + + auto* sketchView = getView(); + + if (sketchView) { + + auto updateCheckBox = [](QCheckBox* checkbox, bool value) { + auto checked = checkbox->checkState() == Qt::Checked; + + if (value != checked) { + const QSignalBlocker blocker(checkbox); + checkbox->setChecked(value); + } + }; + + ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Mod/Sketcher/General"); + + updateCheckBox(snapToObjects, hGrp->GetBool("SnapToObjects", true)); + + updateCheckBox(snapToGrid, hGrp->GetBool("SnapToGrid", false)); + + snapAngle->setValue(hGrp->GetFloat("SnapAngle", 5.0)); + + bool snapActivated = hGrp->GetBool("Snap", true); + snapToObjects->setEnabled(snapActivated); + snapToGrid->setEnabled(snapActivated); + angleLabel->setEnabled(snapActivated); + snapAngle->setEnabled(snapActivated); + } + } + + void languageChange() + { + + snapToObjects->setText(tr("Snap to objects")); + snapToObjects->setToolTip(tr("New points will snap to the currently preselected object. It will also snap to the middle of lines and arcs.")); + snapToObjects->setStatusTip(snapToObjects->toolTip()); + + snapToGrid->setText(tr("Snap to Grid")); + snapToGrid->setToolTip(tr("New points will snap to the nearest grid line.\nPoints must be set closer than a fifth of the grid spacing to a grid line to snap.")); + snapToGrid->setStatusTip(snapToGrid->toolTip()); + + angleLabel->setText(tr("Snap angle")); + snapAngle->setToolTip(tr("Angular step for tools that use 'Snap at Angle' (line for instance). Hold CTRL to enable 'Snap at Angle'. The angle start from the East axis (horizontal right)")); + } + +protected: + QWidget* createWidget(QWidget* parent) override + { + snapToObjects = new QCheckBox(); + + snapToGrid = new QCheckBox(); + + angleLabel = new QLabel(); + + snapAngle = new Gui::QuantitySpinBox(); + snapAngle->setProperty("unit", QVariant(QStringLiteral("deg"))); + snapAngle->setObjectName(QStringLiteral("snapAngle")); + snapAngle->setMaximum(99999999.0); + snapAngle->setMinimum(0); + + QWidget* snapW = new QWidget(parent); + auto* layout = new QGridLayout(snapW); + layout->addWidget(snapToGrid, 0, 0, 1, 2); + layout->addWidget(snapToObjects, 1, 0, 1, 2); + layout->addWidget(angleLabel, 2, 0); + layout->addWidget(snapAngle, 2, 1); + + languageChange(); + + QObject::connect(snapToObjects, &QCheckBox::stateChanged, [this](int state) { + auto* sketchView = getView(); + + if (sketchView) { + ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Mod/Sketcher/General"); + hGrp->SetBool("SnapToObjects", state == Qt::Checked); + } + }); + + QObject::connect(snapToGrid, &QCheckBox::stateChanged, [this](int state) { + auto* sketchView = getView(); + if (sketchView) { + ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Mod/Sketcher/General"); + hGrp->SetBool("SnapToGrid", state == Qt::Checked); + } + }); + + QObject::connect(snapAngle, qOverload(&Gui::QuantitySpinBox::valueChanged), [this](double val) { + auto* sketchView = getView(); + + if (sketchView) { + ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Mod/Sketcher/General"); + hGrp->SetFloat("SnapAngle", val); + } + }); + + return snapW; + } + +private: + ViewProviderSketch* getView() { + Gui::Document* doc = Gui::Application::Instance->activeDocument(); + + if (doc) { + return dynamic_cast(doc->getInEdit()); + } + + return nullptr; + } + +private: + QCheckBox* snapToObjects; + QCheckBox* snapToGrid; + QLabel* angleLabel; + Gui::QuantitySpinBox* snapAngle; +}; + +class CmdSketcherSnap : public Gui::Command +{ +public: + CmdSketcherSnap(); + virtual ~CmdSketcherSnap() {} + virtual const char* className() const + { + return "CmdSketcherSnap"; + } + virtual void languageChange(); +protected: + virtual void activated(int iMsg); + virtual bool isActive(void); + virtual Gui::Action* createAction(void); +private: + void updateIcon(bool value); + + CmdSketcherSnap(const CmdSketcherSnap&) = delete; + CmdSketcherSnap(CmdSketcherSnap&&) = delete; + CmdSketcherSnap& operator= (const CmdSketcherSnap&) = delete; + CmdSketcherSnap& operator= (CmdSketcherSnap&&) = delete; +}; + +CmdSketcherSnap::CmdSketcherSnap() + : Command("Sketcher_Snap") +{ + sAppModule = "Sketcher"; + sGroup = "Sketcher"; + sMenuText = QT_TR_NOOP("Toggle Snap"); + sToolTipText = QT_TR_NOOP("Toggle all snapping functionalities. In the menu you can toggle individually 'Snap to Grid', 'Snap to Objects' and further snap settings"); + sWhatsThis = "Sketcher_Snap"; + sStatusTip = sToolTipText; + eType = 0; +} + +void CmdSketcherSnap::updateIcon(bool value) +{ + static QIcon active = Gui::BitmapFactory().iconFromTheme("Sketcher_Snap"); + static QIcon inactive = Gui::BitmapFactory().iconFromTheme("Sketcher_Snap_Deactivated"); + + auto* pcAction = qobject_cast(getAction()); + pcAction->setIcon(value ? active : inactive); +} + +void CmdSketcherSnap::activated(int iMsg) +{ + Q_UNUSED(iMsg); + + ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Mod/Sketcher/General"); + bool value = !hGrp->GetBool("Snap", true); + + hGrp->SetBool("Snap", value); + + updateIcon(value); + + //Update the widget : + if (!_pcAction) + return; + + Gui::ActionGroup* pcAction = qobject_cast(_pcAction); + QList a = pcAction->actions(); + + auto* ssa = static_cast(a[0]); + ssa->updateWidget(); +} + +Gui::Action* CmdSketcherSnap::createAction() +{ + auto* pcAction = new Gui::ActionGroup(this, Gui::getMainWindow()); + pcAction->setDropDownMenu(true); + pcAction->setExclusive(false); + applyCommandData(this->className(), pcAction); + + SnapSpaceAction* ssa = new SnapSpaceAction(pcAction); + pcAction->addAction(ssa); + + _pcAction = pcAction; + + QObject::connect(pcAction, &Gui::ActionGroup::aboutToShow, [ssa](QMenu* menu) { + Q_UNUSED(menu) + ssa->updateWidget(); + }); + + // set the right pixmap + ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Mod/Sketcher/General"); + updateIcon(hGrp->GetBool("Snap", true)); + + return pcAction; +} + +void CmdSketcherSnap::languageChange() +{ + Command::languageChange(); + + if (!_pcAction) + return; + + Gui::ActionGroup* pcAction = qobject_cast(_pcAction); + QList a = pcAction->actions(); + + auto* ssa = static_cast(a[0]); + ssa->languageChange(); +} + +bool CmdSketcherSnap::isActive() +{ + auto* vp = getInactiveHandlerEditModeSketchViewProvider(); + + if (vp) { + ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Mod/Sketcher/General"); + bool value = hGrp->GetBool("Snap", true); + + updateIcon(value); + + return true; + } + + return false; +} void CreateSketcherCommands() { @@ -1230,4 +1452,5 @@ void CreateSketcherCommands() rcCmdMgr.addCommand(new CmdSketcherMergeSketches()); rcCmdMgr.addCommand(new CmdSketcherViewSection()); rcCmdMgr.addCommand(new CmdSketcherGrid()); + rcCmdMgr.addCommand(new CmdSketcherSnap()); } diff --git a/src/Mod/Sketcher/Gui/Resources/Sketcher.qrc b/src/Mod/Sketcher/Gui/Resources/Sketcher.qrc index 207021d7e5..8786bcdf87 100644 --- a/src/Mod/Sketcher/Gui/Resources/Sketcher.qrc +++ b/src/Mod/Sketcher/Gui/Resources/Sketcher.qrc @@ -106,6 +106,8 @@ icons/general/Sketcher_ViewSketch.svg icons/general/Sketcher_GridToggle.svg icons/general/Sketcher_GridToggle_Deactivated.svg + icons/general/Sketcher_Snap.svg + icons/general/Sketcher_Snap_Deactivated.svg icons/geometry/Sketcher_AlterFillet.svg diff --git a/src/Mod/Sketcher/Gui/Resources/icons/general/Sketcher_Snap.svg b/src/Mod/Sketcher/Gui/Resources/icons/general/Sketcher_Snap.svg new file mode 100644 index 0000000000..612a8fac4d --- /dev/null +++ b/src/Mod/Sketcher/Gui/Resources/icons/general/Sketcher_Snap.svg @@ -0,0 +1,386 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + [wmayer] + + + 2011-10-10 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Part/Gui/Resources/icons/Part_Section.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Sketcher/Gui/Resources/icons/general/Sketcher_Snap_Deactivated.svg b/src/Mod/Sketcher/Gui/Resources/icons/general/Sketcher_Snap_Deactivated.svg new file mode 100644 index 0000000000..833108cf87 --- /dev/null +++ b/src/Mod/Sketcher/Gui/Resources/icons/general/Sketcher_Snap_Deactivated.svg @@ -0,0 +1,377 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + [wmayer] + + + 2011-10-10 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Part/Gui/Resources/icons/Part_Section.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Sketcher/Gui/SnapManager.cpp b/src/Mod/Sketcher/Gui/SnapManager.cpp new file mode 100644 index 0000000000..56638de4bc --- /dev/null +++ b/src/Mod/Sketcher/Gui/SnapManager.cpp @@ -0,0 +1,364 @@ +/*************************************************************************** + * Copyright (c) 2023 Pierre-Louis Boyer * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + +#include "PreCompiled.h" +#ifndef _PreComp_ +#include +#endif // #ifndef _PreComp_ + +#include + +#include "SnapManager.h" +#include "ViewProviderSketch.h" + + +using namespace SketcherGui; +using namespace Sketcher; + +/************************************ Attorney *******************************************/ + +inline int ViewProviderSketchSnapAttorney::getPreselectPoint(const ViewProviderSketch& vp) +{ + return vp.getPreselectPoint(); +} + +inline int ViewProviderSketchSnapAttorney::getPreselectCross(const ViewProviderSketch& vp) +{ + return vp.getPreselectCross(); +} + +inline int ViewProviderSketchSnapAttorney::getPreselectCurve(const ViewProviderSketch& vp) +{ + return vp.getPreselectCurve(); +} + +/**************************** ParameterObserver nested class *****************************/ +SnapManager::ParameterObserver::ParameterObserver(SnapManager& client) : client(client) +{ + initParameters(); + subscribeToParameters(); +} + +SnapManager::ParameterObserver::~ParameterObserver() +{ + unsubscribeToParameters(); +} + +void SnapManager::ParameterObserver::initParameters() +{ + // static map to avoid substantial if/else branching + // + // key->first => String of parameter, + // key->second => Update function to be called for the parameter, + str2updatefunction = { + {"Snap", + [this](const std::string& param) {updateSnapParameter(param); }}, + {"SnapToObjects", + [this](const std::string& param) {updateSnapToObjectParameter(param); }}, + {"SnapToGrid", + [this](const std::string& param) {updateSnapToGridParameter(param); }}, + {"SnapAngle", + [this](const std::string& param) {updateSnapAngleParameter(param); }}, + }; + + for (auto& val : str2updatefunction) { + auto string = val.first; + auto function = val.second; + + function(string); + } +} + +void SnapManager::ParameterObserver::updateSnapParameter(const std::string& parametername) +{ + ParameterGrp::handle hGrp = getParameterGrpHandle(); + + client.snapRequested = hGrp->GetBool(parametername.c_str(), true); +} + +void SnapManager::ParameterObserver::updateSnapToObjectParameter(const std::string& parametername) +{ + ParameterGrp::handle hGrp = getParameterGrpHandle(); + + client.snapToObjectsRequested = hGrp->GetBool(parametername.c_str(), true); +} + +void SnapManager::ParameterObserver::updateSnapToGridParameter(const std::string& parametername) +{ + ParameterGrp::handle hGrp = getParameterGrpHandle(); + + client.snapToGridRequested = hGrp->GetBool(parametername.c_str(), false); +} + +void SnapManager::ParameterObserver::updateSnapAngleParameter(const std::string& parametername) +{ + ParameterGrp::handle hGrp = getParameterGrpHandle(); + + client.snapAngle = fmod(hGrp->GetFloat(parametername.c_str(), 5.) * M_PI / 180, 2 * M_PI); +} + +void SnapManager::ParameterObserver::subscribeToParameters() +{ + try { + ParameterGrp::handle hGrp = getParameterGrpHandle(); + hGrp->Attach(this); + } + catch (const Base::ValueError& e) { // ensure that if parameter strings are not well-formed, the exception is not propagated + Base::Console().Error("SnapManager: Malformed parameter string: %s\n", e.what()); + } +} + +void SnapManager::ParameterObserver::unsubscribeToParameters() +{ + try { + ParameterGrp::handle hGrp = getParameterGrpHandle(); + hGrp->Detach(this); + } + catch (const Base::ValueError& e) {// ensure that if parameter strings are not well-formed, the program is not terminated when calling the noexcept destructor. + Base::Console().Error("SnapManager: Malformed parameter string: %s\n", e.what()); + } +} + +void SnapManager::ParameterObserver::OnChange(Base::Subject& rCaller, const char* sReason) +{ + (void)rCaller; + + auto key = str2updatefunction.find(sReason); + if (key != str2updatefunction.end()) { + auto string = key->first; + auto function = key->second; + + function(string); + } +} + +ParameterGrp::handle SnapManager::ParameterObserver::getParameterGrpHandle() +{ + return App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Mod/Sketcher/General"); +} + +//**************************** SnapManager class ****************************** + +SnapManager::SnapManager(ViewProviderSketch &vp):viewProvider(vp), angleSnapEnabled(false), referencePoint(Base::Vector2d(0.,0.)), lastMouseAngle(0.0) +{ + // Create parameter observer and initialise watched parameters + pObserver = std::make_unique(*this); +} + +SnapManager::~SnapManager() {} + +bool SnapManager::snap(double& x, double& y) +{ + if (!snapRequested) + { + return false; + } + + //In order of priority : + + // 1 - Snap at an angle + if (angleSnapEnabled && QApplication::keyboardModifiers() == Qt::ControlModifier) { + return snapAtAngle(x, y); + } + else { + lastMouseAngle = 0.0; + } + + // 2 - Snap to objects + if (snapToObjectsRequested + && snapToObject(x, y)) { + return true; + } + + // 3 - Snap to grid + if (snapToGridRequested /*&& viewProvider.ShowGrid.getValue() */ ) { //Snap to grid is enabled even if the grid is not visible. + return snapToGrid(x, y); + } + + return false; +} + +bool SnapManager::snapAtAngle(double& x, double& y) +{ + Base::Vector2d pointToOverride(x, y); + double length = (pointToOverride - referencePoint).Length(); + + double angle1 = (pointToOverride - referencePoint).Angle(); + double angle2 = angle1 + (angle1 < 0. ? 2 : -2) * M_PI; + lastMouseAngle = abs(angle1 - lastMouseAngle) < abs(angle2 - lastMouseAngle) ? angle1 : angle2; + + double angle = round(lastMouseAngle / snapAngle) * snapAngle; + pointToOverride = referencePoint + length * Base::Vector2d(cos(angle), sin(angle)); + x = pointToOverride.x; + y = pointToOverride.y; + + return true; +} + +bool SnapManager::snapToObject(double& x, double& y) +{ + Sketcher::SketchObject* Obj = viewProvider.getSketchObject(); + int geoId = GeoEnum::GeoUndef; + Sketcher::PointPos posId = Sketcher::PointPos::none; + + int VtId = ViewProviderSketchSnapAttorney::getPreselectPoint(viewProvider); + int CrsId = ViewProviderSketchSnapAttorney::getPreselectCross(viewProvider); + int CrvId = ViewProviderSketchSnapAttorney::getPreselectCurve(viewProvider); + + if (CrsId == 0 || VtId >= 0) { + if (CrsId == 0) { + geoId = Sketcher::GeoEnum::RtPnt; + posId = Sketcher::PointPos::start; + } + else if (VtId >= 0) { + Obj->getGeoVertexIndex(VtId, geoId, posId); + } + + x = Obj->getPoint(geoId, posId).x; + y = Obj->getPoint(geoId, posId).y; + return true; + } + else if (CrsId == 1) { //H_Axis + y = 0; + return true; + } + else if (CrsId == 2) { //V_Axis + x = 0; + return true; + } + else if (CrvId >= 0 || CrvId <= Sketcher::GeoEnum::RefExt) { //Curves + + const Part::Geometry* geo = Obj->getGeometry(CrvId); + + Base::Vector3d pointToOverride(x, y, 0.); + + double pointParam = 0.0; + auto curve = dynamic_cast(geo); + if (curve) { + try { + curve->closestParameter(pointToOverride, pointParam); + pointToOverride = curve->pointAtParameter(pointParam); + } + catch (Base::CADKernelError& e) { + e.ReportException(); + return false; + } + + //If it is a line, then we check if we need to snap to the middle. + if (geo->getTypeId() == Part::GeomLineSegment::getClassTypeId()) { + const Part::GeomLineSegment* line = static_cast(geo); + snapToLineMiddle(pointToOverride, line); + } + + //If it is an arc, then we check if we need to snap to the middle (not the center). + if (geo->getTypeId() == Part::GeomArcOfCircle::getClassTypeId()) { + const Part::GeomArcOfCircle* arc = static_cast(geo); + snapToArcMiddle(pointToOverride, arc); + } + + x = pointToOverride.x; + y = pointToOverride.y; + + return true; + } + } + + return false; +} + +bool SnapManager::snapToGrid(double& x, double& y) +{ + // Snap Tolerance in pixels + const double snapTol = viewProvider.getGridSize() / 5; + + double tmpX = x, tmpY = y; + + viewProvider.getClosestGridPoint(tmpX, tmpY); + + bool snapped = false; + + // Check if x within snap tolerance + if (x < tmpX + snapTol && x > tmpX - snapTol) { + x = tmpX; // Snap X Mouse Position + snapped = true; + } + + // Check if y within snap tolerance + if (y < tmpY + snapTol && y > tmpY - snapTol) { + y = tmpY; // Snap Y Mouse Position + snapped = true; + } + + return snapped; +} + +bool SnapManager::snapToLineMiddle(Base::Vector3d& pointToOverride, const Part::GeomLineSegment* line) +{ + Base::Vector3d startPoint = line->getStartPoint(); + Base::Vector3d endPoint = line->getEndPoint(); + Base::Vector3d midPoint = (startPoint + endPoint) / 2; + + //Check if we are at middle of the line and if so snap to it. + if ((pointToOverride - midPoint).Length() < (endPoint - startPoint).Length() * 0.05) { + pointToOverride = midPoint; + return true; + } + + return false; +} + +bool SnapManager::snapToArcMiddle(Base::Vector3d& pointToOverride, const Part::GeomArcOfCircle* arc) +{ + Base::Vector3d centerPoint = arc->getCenter(); + Base::Vector3d startVec = (arc->getStartPoint() - centerPoint); + Base::Vector3d middleVec = startVec + (arc->getEndPoint() - centerPoint); + + /* Handle the case of arc angle = 180 */ + if (middleVec.Length() < Precision::Confusion()) { + middleVec.x = startVec.y; + middleVec.y = -startVec.x; + } + else { + middleVec = middleVec / middleVec.Length() * arc->getRadius(); + } + + Base::Vector2d mVec = Base::Vector2d(middleVec.x, middleVec.y); + Base::Vector3d pointVec = pointToOverride - centerPoint; + Base::Vector2d pVec = Base::Vector2d(pointVec.x, pointVec.y); + + double u, v; + arc->getRange(u, v, true); + if (v < u) + v += 2 * M_PI; + double angle = v - u; + int revert = angle < M_PI ? 1 : -1; + + /*To know if we are close to the middle of the arc, we are going to compare the angle of the + * (mouse cursor - center) to the angle of the middle of the arc. If it's less than 10% of the arc angle, then we snap. + */ + if (fabs(pVec.Angle() - (revert * mVec).Angle()) < 0.10 * angle) { + pointToOverride = centerPoint + middleVec * revert; + return true; + } + + return false; +} \ No newline at end of file diff --git a/src/Mod/Sketcher/Gui/SnapManager.h b/src/Mod/Sketcher/Gui/SnapManager.h new file mode 100644 index 0000000000..18f2a94ad0 --- /dev/null +++ b/src/Mod/Sketcher/Gui/SnapManager.h @@ -0,0 +1,125 @@ +/*************************************************************************** + * Copyright (c) 2023 Pierre-Louis Boyer * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file COPYING.LIB. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + * SnapManager initially funded by the Open Toolchain Foundation * + ***************************************************************************/ + +#ifndef SKETCHERGUI_SnapManager_H +#define SKETCHERGUI_SnapManager_H + + +#include + + +namespace SketcherGui { + +class ViewProviderSketch; + + +class ViewProviderSketchSnapAttorney { +private: + + static inline int getPreselectPoint(const ViewProviderSketch& vp); + static inline int getPreselectCross(const ViewProviderSketch& vp); + static inline int getPreselectCurve(const ViewProviderSketch& vp); + + friend class SnapManager; +}; + +/* This class is used to manage the overriding of mouse pointer coordinates in Sketcher +* (in Edit-Mode) depending on the situation. Those situations are in priority order : +* 1 - Snap at angle: For tools like Slot, Arc, Line, Ellipse, this enables to constrain the angle at steps of 5° (or customized angle). +* This is useful to make features at a certain angle (45° for example) +* 2 - Snap to object: This snaps the mouse pointer onto objects. +* 3 - Snap to grid: This snaps the mouse pointer on the grid. +*/ +class SnapManager +{ + + /** @brief Class for monitoring changes in parameters affecting Snapping + * @details + * + * This nested class is a helper responsible for attaching to the parameters relevant for + * SnapManager, initialising the SnapManager to the current configuration + * and handle in real time any change to their values. + */ + class ParameterObserver : public ParameterGrp::ObserverType + { + public: + explicit ParameterObserver(SnapManager& client); + ~ParameterObserver() override; + + void subscribeToParameters(); + + void unsubscribeToParameters(); + + /** Observer for parameter group. */ + void OnChange(Base::Subject& rCaller, const char* sReason) override; + + private: + void initParameters(); + void updateSnapParameter(const std::string& parametername); + void updateSnapToObjectParameter(const std::string& parametername); + void updateSnapToGridParameter(const std::string& parametername); + void updateSnapAngleParameter(const std::string& parametername); + + static ParameterGrp::handle getParameterGrpHandle(); + + private: + std::map> str2updatefunction; + SnapManager& client; + }; + +public: + explicit SnapManager(ViewProviderSketch &vp); + ~SnapManager(); + + bool snap(double& x, double& y); + bool snapAtAngle(double& x, double& y); + bool snapToObject(double& x, double& y); + bool snapToGrid(double& x, double& y); + + bool snapToLineMiddle(Base::Vector3d& pointToOverride, const Part::GeomLineSegment* line); + bool snapToArcMiddle(Base::Vector3d& pointToOverride, const Part::GeomArcOfCircle* arc); + + bool angleSnapEnabled; + Base::Vector2d referencePoint; + +private: + double snapAngle; + double lastMouseAngle; + + bool snapRequested; + bool snapToObjectsRequested; + bool snapToGridRequested; + + /// Reference to ViewProviderSketch in order to access the public and the Attorney Interface + ViewProviderSketch & viewProvider; + + /// Observer to track all the needed parameters. + std::unique_ptr pObserver; +}; + + +} // namespace SketcherGui + + +#endif // SKETCHERGUI_SnapManager_H + diff --git a/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp b/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp index 62d81b0f8c..e74c17b76f 100644 --- a/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp +++ b/src/Mod/Sketcher/Gui/ViewProviderSketch.cpp @@ -65,6 +65,7 @@ #include "DrawSketchHandler.h" #include "EditDatumDialog.h" #include "EditModeCoinManager.h" +#include "SnapManager.h" #include "TaskDlgEditSketch.h" #include "TaskSketcherValidation.h" #include "Utils.h" @@ -304,6 +305,7 @@ ViewProviderSketch::ViewProviderSketch() Mode(STATUS_NONE), listener(nullptr), editCoinManager(nullptr), + snapManager(nullptr), pObserver(std::make_unique(*this)), sketchHandler(nullptr), viewOrientationFactor(1) @@ -551,36 +553,11 @@ bool ViewProviderSketch::keyPressed(bool pressed, int key) return true; // handle all other key events } -void ViewProviderSketch::setSnapMode(SnapMode mode) +void ViewProviderSketch::setAngleSnapping(bool enable, Base::Vector2d referencePoint) { - snapMode = mode; // to be redirected to SnapManager -} - -SnapMode ViewProviderSketch::getSnapMode() const -{ - return snapMode; // to be redirected to SnapManager -} - -void ViewProviderSketch::snapToGrid(double &x, double &y) // Paddle, when resolving this conflict, make sure to use the function in ViewProviderGridExtension -{ - if (snapMode == SnapMode::SnapToGrid && ShowGrid.getValue()) { - // Snap Tolerance in pixels - const double snapTol = getGridSize() / 5; - - double tmpX = x, tmpY = y; - - getClosestGridPoint(tmpX, tmpY); - - // Check if x within snap tolerance - if (x < tmpX + snapTol && x > tmpX - snapTol) { - x = tmpX; // Snap X Mouse Position - } - - // Check if y within snap tolerance - if (y < tmpY + snapTol && y > tmpY - snapTol) { - y = tmpY; // Snap Y Mouse Position - } - } + assert(snapManager); + snapManager->angleSnapEnabled = enable; + snapManager->referencePoint = referencePoint; } void ViewProviderSketch::getProjectingLine(const SbVec2s& pnt, const Gui::View3DInventorViewer *viewer, SbLine& line) const @@ -679,7 +656,7 @@ bool ViewProviderSketch::mouseButtonPressed(int Button, bool pressed, const SbVe try { getCoordsOnSketchPlane(pos,normal,x,y); - snapToGrid(x, y); + snapManager->snap(x, y); } catch (const Base::ZeroDivisionError&) { return false; @@ -1148,7 +1125,7 @@ bool ViewProviderSketch::mouseMove(const SbVec2s &cursorPos, Gui::View3DInventor double x,y; try { getCoordsOnSketchPlane(line.getPosition(),line.getDirection(),x,y); - snapToGrid(x, y); + snapManager->snap(x, y); } catch (const Base::ZeroDivisionError&) { return false; @@ -1275,7 +1252,7 @@ bool ViewProviderSketch::mouseMove(const SbVec2s &cursorPos, Gui::View3DInventor SbLine line2; getProjectingLine(DoubleClick::prvCursorPos, viewer, line2); getCoordsOnSketchPlane(line2.getPosition(),line2.getDirection(),drag.xInit,drag.yInit); - snapToGrid(drag.xInit, drag.yInit); + snapManager->snap(drag.xInit, drag.yInit); } else { drag.resetVector(); } @@ -2877,6 +2854,7 @@ bool ViewProviderSketch::setEdit(int ModNum) preselection.reset(); selection.reset(); editCoinManager = std::make_unique(*this); + snapManager = std::make_unique(*this); auto editDoc = Gui::Application::Instance->editDocument(); App::DocumentObject *editObj = getSketchObject(); @@ -3123,6 +3101,7 @@ void ViewProviderSketch::unsetEdit(int ModNum) deactivateHandler(); editCoinManager = nullptr; + snapManager = nullptr; preselection.reset(); selection.reset(); this->detachSelection(); diff --git a/src/Mod/Sketcher/Gui/ViewProviderSketch.h b/src/Mod/Sketcher/Gui/ViewProviderSketch.h index eb1765f864..f8280420ff 100644 --- a/src/Mod/Sketcher/Gui/ViewProviderSketch.h +++ b/src/Mod/Sketcher/Gui/ViewProviderSketch.h @@ -85,15 +85,9 @@ namespace Sketcher { namespace SketcherGui { class EditModeCoinManager; +class SnapManager; class DrawSketchHandler; -enum class SnapMode { // to be moved to SnapManager - None, - SnapToObject, - SnapToAngle, - SnapToGrid, -}; - using GeoList = Sketcher::GeoList; using GeoListFacade = Sketcher::GeoListFacade; @@ -484,8 +478,10 @@ public: void onSelectionChanged(const Gui::SelectionChanges& msg) override; //@} - void setSnapMode(SnapMode mode); - SnapMode getSnapMode() const; + /** @name Toggle angle snapping and set the reference point */ + //@{ + /// Toggle angle snapping and set the reference point + void setAngleSnapping(bool enable, Base::Vector2d referencePoint = Base::Vector2d(0., 0.)); /** @name Access to Sketch and Solver objects */ //@{ @@ -566,6 +562,7 @@ public: //@{ friend class ViewProviderSketchDrawSketchHandlerAttorney; friend class ViewProviderSketchCoinAttorney; + friend class ViewProviderSketchSnapAttorney; friend class ViewProviderSketchShortcutListenerAttorney; //@} protected: @@ -660,9 +657,6 @@ private: /** @name miscelanea utilities */ //@{ - /// snap points x,y (mouse coordinates) onto grid if enabled - void snapToGrid(double &x, double &y); - /// moves a selected constraint void moveConstraint(int constNum, const Base::Vector2d &toPos); @@ -784,6 +778,8 @@ private: std::unique_ptr editCoinManager; + std::unique_ptr snapManager; + std::unique_ptr pObserver; std::unique_ptr sketchHandler; @@ -792,8 +788,6 @@ private: SoNodeSensor cameraSensor; int viewOrientationFactor; // stores if sketch viewed from front or back - - SnapMode snapMode = SnapMode::None; // temporary - to be moved to SnapManager }; } // namespace PartGui diff --git a/src/Mod/Sketcher/Gui/Workbench.cpp b/src/Mod/Sketcher/Gui/Workbench.cpp index 5b8bf0fef0..9babd176dc 100644 --- a/src/Mod/Sketcher/Gui/Workbench.cpp +++ b/src/Mod/Sketcher/Gui/Workbench.cpp @@ -188,7 +188,8 @@ inline void SketcherAddWorkbenchSketchEditModeActions(Gui::ToolBarItem& sketch) sketch << "Sketcher_LeaveSketch" << "Sketcher_ViewSketch" << "Sketcher_ViewSection" - << "Sketcher_Grid"; + << "Sketcher_Grid" + << "Sketcher_Snap"; } template