From fc05c6bae39c90db0caa86bdc2b15de3ae0cb831 Mon Sep 17 00:00:00 2001 From: Ajinkya Dahale Date: Mon, 20 Feb 2023 07:47:43 +0530 Subject: [PATCH] [Sketcher] Create DSH for drawing B-splines by interpolation Only creates 1-degree splines by describing knots. This commit is part of a project funded by the Open Toolchain Foundation under the title "Open Toolchain Foundation - Curve drawing tool in Sketcher Workbench" --- src/Mod/Sketcher/App/ConstraintPyImp.cpp | 8 +- src/Mod/Sketcher/Gui/CMakeLists.txt | 1 + src/Mod/Sketcher/Gui/CommandCreateGeo.cpp | 13 +- .../DrawSketchHandlerBSplineByInterpolation.h | 512 ++++++++++++++++++ 4 files changed, 530 insertions(+), 4 deletions(-) create mode 100644 src/Mod/Sketcher/Gui/DrawSketchHandlerBSplineByInterpolation.h diff --git a/src/Mod/Sketcher/App/ConstraintPyImp.cpp b/src/Mod/Sketcher/App/ConstraintPyImp.cpp index dd33c0a5ca..35c9705c54 100644 --- a/src/Mod/Sketcher/App/ConstraintPyImp.cpp +++ b/src/Mod/Sketcher/App/ConstraintPyImp.cpp @@ -317,10 +317,14 @@ int ConstraintPy::PyInit(PyObject* args, PyObject* /*kwd*/) valid = true; - if(strstr(ConstraintType,"BSplineControlPoint")) { + if (strstr(ConstraintType,"BSplineControlPoint")) { this->getConstraintPtr()->AlignmentType=BSplineControlPoint; } - else { + else if (strstr(ConstraintType,"BSplineKnotPoint")) { + this->getConstraintPtr()->AlignmentType=BSplineKnotPoint; + } + else + { this->getConstraintPtr()->AlignmentType=Undef; valid = false; } diff --git a/src/Mod/Sketcher/Gui/CMakeLists.txt b/src/Mod/Sketcher/Gui/CMakeLists.txt index 5de66a2124..74f4fdedbf 100644 --- a/src/Mod/Sketcher/Gui/CMakeLists.txt +++ b/src/Mod/Sketcher/Gui/CMakeLists.txt @@ -66,6 +66,7 @@ SET(SketcherGui_SRCS DrawSketchHandlerArcOfHyperbola.h DrawSketchHandlerArcOfParabola.h DrawSketchHandlerBSpline.h + DrawSketchHandlerBSplineByInterpolation.h DrawSketchHandlerPoint.h DrawSketchHandlerFillet.h DrawSketchHandlerTrimming.h diff --git a/src/Mod/Sketcher/Gui/CommandCreateGeo.cpp b/src/Mod/Sketcher/Gui/CommandCreateGeo.cpp index 33ab5e4c6b..45a87f306d 100644 --- a/src/Mod/Sketcher/Gui/CommandCreateGeo.cpp +++ b/src/Mod/Sketcher/Gui/CommandCreateGeo.cpp @@ -56,6 +56,7 @@ #include "DrawSketchHandlerArcOfHyperbola.h" #include "DrawSketchHandlerArcOfParabola.h" #include "DrawSketchHandlerBSpline.h" +#include "DrawSketchHandlerBSplineByInterpolation.h" #include "DrawSketchHandlerCarbonCopy.h" #include "DrawSketchHandlerCircle.h" #include "DrawSketchHandlerEllipse.h" @@ -947,9 +948,14 @@ void CmdSketcherCompCreateBSpline::activated(int iMsg) { if (iMsg == 0) { ActivateHandler(getActiveGuiDocument(), new DrawSketchHandlerBSpline(iMsg)); - } else if (iMsg == 1) { + } + else if (iMsg == 1) { ActivateHandler(getActiveGuiDocument(), new DrawSketchHandlerBSpline(iMsg)); - } else { + } + else if (iMsg == 2) { + ActivateHandler(getActiveGuiDocument(), new DrawSketchHandlerBSplineByInterpolation(0)); + } + else { return; } @@ -974,6 +980,9 @@ Gui::Action * CmdSketcherCompCreateBSpline::createAction() QAction* periodicbspline = pcAction->addAction(QString()); periodicbspline->setIcon(Gui::BitmapFactory().iconFromTheme("Sketcher_Create_Periodic_BSpline")); + QAction* bsplinebyknot = pcAction->addAction(QString()); + bsplinebyknot->setIcon(Gui::BitmapFactory().iconFromTheme("Sketcher_Create_BSplineByKnot")); + _pcAction = pcAction; languageChange(); diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerBSplineByInterpolation.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerBSplineByInterpolation.h new file mode 100644 index 0000000000..9c5c783b25 --- /dev/null +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerBSplineByInterpolation.h @@ -0,0 +1,512 @@ +/*************************************************************************** + * Copyright (c) 2023 Ajinkya Dahale * + * * + * 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 * + * * + ***************************************************************************/ + +#ifndef SKETCHERGUI_DrawSketchHandlerBSplineByInterpolation_H +#define SKETCHERGUI_DrawSketchHandlerBSplineByInterpolation_H + +#include +#include + +#include "GeometryCreationMode.h" + + +namespace SketcherGui { + +extern GeometryCreationMode geometryCreationMode; // defined in CommandCreateGeo.cpp + +class DrawSketchHandlerBSplineByInterpolation: public DrawSketchHandler +{ +public: + explicit DrawSketchHandlerBSplineByInterpolation(int constructionMethod) + : Mode(STATUS_SEEK_FIRST_POINT) + , MousePressMode(MOUSE_NOT_PRESSED) + , ConstrMethod(constructionMethod) + , SplineDegree(3) + , IsClosed(false) + { + addSugConstraint(); + applyCursor(); + } + + virtual ~DrawSketchHandlerBSplineByInterpolation() = default; + + /// modes + enum SELECT_MODE { + STATUS_SEEK_FIRST_POINT, + STATUS_SEEK_ADDITIONAL_POINTS, + STATUS_CLOSE + }; + + // TODO: this kind of behavior will be useful in a superclass + // when LMB is pressed it's a transitional state so some undos can't be done + // (like delete last knot) + enum MOUSE_PRESS_MODE { + MOUSE_PRESSED, + MOUSE_NOT_PRESSED + }; + + void mouseMove(Base::Vector2d onSketchPos) override + { + prevCursorPosition = onSketchPos; + + if (Mode==STATUS_SEEK_FIRST_POINT) { + setPositionText(onSketchPos); + + if (seekAutoConstraint(sugConstr.back(), onSketchPos, Base::Vector2d(0.f,0.f))) { + renderSuggestConstraintsCursor(sugConstr.back()); + return; + } + } + else if (Mode==STATUS_SEEK_ADDITIONAL_POINTS) { + + drawControlPolygonToPosition(onSketchPos); + + drawCursorToPosition(onSketchPos); + + if (seekAutoConstraint(sugConstr.back(), onSketchPos, Base::Vector2d(0.f,0.f))) { + renderSuggestConstraintsCursor(sugConstr.back()); + return; + } + } + } + + bool pressButton(Base::Vector2d onSketchPos) override + { + prevCursorPosition = onSketchPos; + + MousePressMode = MOUSE_PRESSED; + + if (Mode == STATUS_SEEK_FIRST_POINT) { + BSplineKnots.push_back(onSketchPos); + + Mode = STATUS_SEEK_ADDITIONAL_POINTS; + + // insert circle point for knot, defer internal alignment constraining. + try { + Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Add Knot Point")); + + //Add knot + Gui::cmdAppObjectArgs(sketchgui->getObject(), "addGeometry(Part.Point(App.Vector(%f,%f,0)),True)", + BSplineKnots.back().x, BSplineKnots.back().y); + + knotGeoIds.push_back(getHighestCurveIndex()); + } + catch (const Base::Exception& e) { + Base::Console().Error("%s\n", e.what()); + Gui::Command::abortCommand(); + + static_cast(sketchgui->getObject())->solve(); + + return false; + } + + //Gui::Command::commitCommand(); + + //static_cast(sketchgui->getObject())->solve(); + + // add auto constraints on knot + if (!sugConstr.back().empty()) { + createAutoConstraints(sugConstr.back(), knotGeoIds.back(), Sketcher::PointPos::mid, false); + } + + static_cast(sketchgui->getObject())->solve(); + + addSugConstraint(); + + } + else if (Mode == STATUS_SEEK_ADDITIONAL_POINTS) { + BSplineKnots.push_back(onSketchPos); + + // check if coincident with first knot + for(auto & ac : sugConstr.back()) { + if( ac.Type == Sketcher::Coincident && ac.GeoId == knotGeoIds[0] && ac.PosId == Sketcher::PointPos::start ) { + IsClosed = true; + } + } + + if (IsClosed) { + Mode = STATUS_CLOSE; + + if (ConstrMethod == 1) { // if periodic we do not need the last pole + BSplineKnots.pop_back(); + sugConstr.pop_back(); + + return true; + } + } + + // insert point for knot, defer internal alignment constraining. + try { + + //Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Add Pole circle")); + + //Add pole + Gui::cmdAppObjectArgs(sketchgui->getObject(), "addGeometry(Part.Point(App.Vector(%f,%f,0)),True)", + BSplineKnots.back().x,BSplineKnots.back().y); + + knotGeoIds.push_back(getHighestCurveIndex()); + } + catch (const Base::Exception& e) { + Base::Console().Error("%s\n", e.what()); + Gui::Command::abortCommand(); + + static_cast(sketchgui->getObject())->solve(); + + return false; + } + + // add auto constraints on knot + if (!sugConstr.back().empty()) { + createAutoConstraints(sugConstr.back(), knotGeoIds.back(), Sketcher::PointPos::mid, false); + } + + if (!IsClosed) { + addSugConstraint(); + } + + } + return true; + } + + bool releaseButton(Base::Vector2d onSketchPos) override + { + prevCursorPosition = onSketchPos; + MousePressMode = MOUSE_NOT_PRESSED; + + return finishCommand(onSketchPos); + } + + void registerPressedKey(bool pressed, int key) override + { + if (SoKeyboardEvent::D == key && pressed) { + SplineDegree = QInputDialog::getInt( + Gui::getMainWindow(), + QObject::tr("B-Spline Degree"), + QObject::tr("Define B-Spline Degree, between 1 and %1:") + .arg(QString::number(Geom_BSplineCurve::MaxDegree())), + SplineDegree, 1, Geom_BSplineCurve::MaxDegree(), 1); + // FIXME: Pressing Esc here also finishes the B-Spline creation. + // The user may only want to exit the dialog. + } + // On pressing Backspace delete last knot + else if (SoKeyboardEvent::BACKSPACE == key && pressed) { + // when mouse is pressed we are in a transitional state so don't mess with it + if (MOUSE_PRESSED == MousePressMode) + return; + + // can only delete last knot if it exists + if (STATUS_SEEK_FIRST_POINT == Mode || + STATUS_CLOSE == Mode) + return; + + // if only first knot exists it's equivalent to canceling current spline + if (knotGeoIds.size() == 1) { + // this also exits b-spline creation if continuous mode is off + this->quit(); + return; + } + + // reverse the steps of press/release button + try { + // already ensured that CurrentConstraint == EditCurve.size() > 1 + const int delGeoId = knotGeoIds.back(); + const auto& constraints = static_cast(sketchgui->getObject())->Constraints.getValues(); + for (int i = constraints.size() - 1; i >= 0; --i) { + if (delGeoId == constraints[i]->First || + delGeoId == constraints[i]->Second || + delGeoId == constraints[i]->Third) + Gui::cmdAppObjectArgs(sketchgui->getObject(), "delConstraint(%d)", i); + } + + // Remove knot + Gui::cmdAppObjectArgs(sketchgui->getObject(), "delGeometry(%d)", delGeoId); + + static_cast(sketchgui->getObject())->solve(); + + knotGeoIds.pop_back(); + BSplineKnots.pop_back(); + + // last entry is kept, as it corresponds to the current knot, but the one corresponding to the erased knot is removed + sugConstr.erase(std::prev(std::prev(sugConstr.end()))); + + + // run this in the end to draw lines and position text + drawControlPolygonToPosition(prevCursorPosition); + drawCursorToPosition(prevCursorPosition); + } + catch (const Base::Exception& e) { + Base::Console().Error("%s\n", e.what()); + // some commands might have already deleted some constraints/geometries but not others + Gui::Command::abortCommand(); + + static_cast(sketchgui->getObject())->solve(); + + return; + } + } + // TODO: On pressing, say, W, modify last pole's weight + // TODO: On pressing, say, M, modify next knot's multiplicity + + return; + } + + void quit() override + { + // We must see if we need to create a B-spline before cancelling everything + // and now just like any other Handler, + + ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Mod/Sketcher"); + + bool continuousMode = hGrp->GetBool("ContinuousCreationMode",true); + + if (knotGeoIds.size() > 1) { + // create B-spline from existing knots + Mode=STATUS_CLOSE; + finishCommand(Base::Vector2d(0.f,0.f)); + } + else if(knotGeoIds.size() == 1) { + // if we just have one point and we can not close anything, then cancel this creation but continue according to continuous mode + //sketchgui->getDocument()->undo(1); + + Gui::Command::abortCommand(); + + tryAutoRecomputeIfNotSolve(static_cast(sketchgui->getObject())); + + if(!continuousMode){ + DrawSketchHandler::quit(); + } + else { + // This code disregards existing data and enables the continuous creation mode. + resetHandlerState(); + } + } + else { // we have no data (CurrentConstraint == 0) so user when right-clicking really wants to exit + DrawSketchHandler::quit(); + } + } + +private: + void resetHandlerState() + { + Mode = STATUS_SEEK_FIRST_POINT; + applyCursor(); + + SplineDegree = 3; + + sugConstr.clear(); + knotGeoIds.clear(); + BSplineKnots.clear(); + + eraseEditCurve(); + + addSugConstraint(); + + IsClosed = false; + } + + QString getCrosshairCursorSVGName() const override + { + return QString::fromLatin1("Sketcher_Pointer_Create_BSpline"); + } + + void addSugConstraint() + { + std::vector sugConstr1; + sugConstr.push_back(std::move(sugConstr1)); + } + + // NOTE: In this context, it is not a control polygon, but a 1-degree interpolation + void drawControlPolygonToPosition(Base::Vector2d position) + { + + std::vector editcurve(BSplineKnots); + editcurve.push_back(position); + + drawEdit(editcurve); + } + + void drawCursorToPosition(Base::Vector2d position) + { + if (!BSplineKnots.empty()) { + float length = (position - BSplineKnots.back()).Length(); + float angle = (position - BSplineKnots.back()).GetAngle(Base::Vector2d(1.f,0.f)); + + if (showCursorCoords()) { + SbString text; + std::string lengthString = lengthToDisplayFormat(length, 1); + std::string angleString = angleToDisplayFormat((angle != -FLOAT_MAX) ? angle * 180 / M_PI : 0, 1); + text.sprintf(" (%s, %s)", lengthString.c_str(), angleString.c_str()); + setPositionText(position, text); + } + } + } + + void eraseEditCurve() + { + drawEdit(std::vector()); + } + + bool finishCommand(Base::Vector2d position) + { + if (Mode==STATUS_CLOSE) { + // TODO: make modifications for construction by knots + unsetCursor(); + resetPositionText(); + + std::stringstream stream; + std::string controlpoints; + + // TODO: for knots compute new control points and use those instead + for (auto & knot : BSplineKnots) { + stream << "App.Vector(" << knot.x << "," << knot.y << "),"; + } + + controlpoints = stream.str(); + + // remove last comma and add brackets + int index = controlpoints.rfind(','); + controlpoints.resize(index); + + controlpoints.insert(0,1,'['); + controlpoints.append(1,']'); + + int currentgeoid = getHighestCurveIndex(); + + // TODO: Adjust this or remove condition if needed + unsigned int maxDegree = 1; + + try { + //Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Add B-spline curve")); + + // {"poles", "mults", "knots", "periodic", "degree", "weights", "CheckRational", NULL}; + // FIXME: can we get by without naming the variable? + // TODO: variable degrees? + QString cmdstr = QString::fromLatin1("_bsp = Part.BSplineCurve()\n" + "_bsp.interpolate(%1, PeriodicFlag=%2)\n" + ).arg(QString::fromLatin1(controlpoints.c_str()), + QString::fromLatin1(ConstrMethod == 0 ?"False":"True")); + Gui::Command::runCommand(Gui::Command::Gui, cmdstr.toLatin1()); + Gui::cmdAppObjectArgs(sketchgui->getObject(), "addGeometry(_bsp,%s)", + geometryCreationMode==Construction?"True":"False"); + Gui::Command::runCommand(Gui::Command::Gui, "del(_bsp)\n"); + + currentgeoid++; + + // autoconstraints were added to the circles of the poles, which is ok because they must go to the + // right position, or the user will freak-out if they appear out of the autoconstrained position. + // However, autoconstraints on the first and last knot, in normal non-periodic b-splines (with appropriate endpoint knot multiplicity) + // as the ones created by this tool are intended for the b-spline endpoints, and not for the knots, + // so here we retrieve any autoconstraint on those knots' center and mangle it to the endpoint. + // TODO: this will be done to the first and last knots instead for ConstrMethod==2 + if (ConstrMethod == 0) { + for(auto & constr : static_cast(sketchgui->getObject())->Constraints.getValues()) { + if(constr->First == knotGeoIds[0] && constr->FirstPos == Sketcher::PointPos::start) { + constr->First = currentgeoid; + constr->FirstPos = Sketcher::PointPos::start; + } + else if(constr->First == knotGeoIds.back() && constr->FirstPos == Sketcher::PointPos::start) { + constr->First = currentgeoid; + constr->FirstPos = Sketcher::PointPos::end; + } + } + } + + // TODO: Change this for construction by knots + // Constraint pole circles to B-spline. + std::stringstream cstream; + + cstream << "conList = []\n"; + + for (size_t i = 0; i < knotGeoIds.size(); i++) { + cstream << "conList.append(Sketcher.Constraint('InternalAlignment:Sketcher::BSplineKnotPoint'," << knotGeoIds[0] + i + << "," << static_cast(Sketcher::PointPos::start) << "," << currentgeoid << "," << i << "))\n"; + } + + cstream << Gui::Command::getObjectCmd(sketchgui->getObject()) << ".addConstraint(conList)\n"; + cstream << "del conList\n"; + + Gui::Command::doCommand(Gui::Command::Doc, cstream.str().c_str()); + + // for showing the knots on creation + Gui::cmdAppObjectArgs(sketchgui->getObject(), "exposeInternalGeometry(%d)", currentgeoid); + } + catch (const Base::Exception& e) { + Base::Console().Error("%s\n", e.what()); + Gui::Command::abortCommand(); + + tryAutoRecomputeIfNotSolve(static_cast(sketchgui->getObject())); + + return false; + } + + Gui::Command::commitCommand(); + + tryAutoRecomputeIfNotSolve(static_cast(sketchgui->getObject())); + + ParameterGrp::handle hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/Mod/Sketcher"); + bool continuousMode = hGrp->GetBool("ContinuousCreationMode",true); + + if(continuousMode){ + // This code enables the continuous creation mode. + resetHandlerState(); + + drawCursorToPosition(position); + + /* It is ok not to call to purgeHandler + * in continuous creation mode because the + * handler is destroyed by the quit() method on pressing the + * right button of the mouse */ + } + else{ + sketchgui->purgeHandler(); // no code after this line, Handler get deleted in ViewProvider + } + } + else { + drawCursorToPosition(position); + } + + return true; + } + +protected: + SELECT_MODE Mode; + MOUSE_PRESS_MODE MousePressMode; + + // Stores position of the knots of the BSpline. + std::vector BSplineKnots; + + // suggested autoconstraints for knots. + // A new one must be added e.g. using addSugConstraint() before adding a new knot. + std::vector> sugConstr; + + int ConstrMethod; + unsigned int SplineDegree; + bool IsClosed; + std::vector knotGeoIds; + Base::Vector2d prevCursorPosition; +}; + + +} // namespace SketcherGui + + +#endif // SKETCHERGUI_DrawSketchHandlerBSplineByInterpolation_H