From 01866dfbfc4dedd4949c48bd9478611997daf713 Mon Sep 17 00:00:00 2001 From: Ajinkya Dahale Date: Fri, 4 Mar 2022 00:27:22 -0500 Subject: [PATCH] [Sketcher] Add methods and tools for joining curves Algorithm to join b-splines: The code simple concatenates the knots, poles, weights, and knot multiplicities together, removing data on the connection point of the second curve. Some further study is needed to see if/when it will give an exact/good connection. Icon courtesy @bitacovir. --- src/Mod/Sketcher/App/SketchObject.cpp | 141 +++++ src/Mod/Sketcher/App/SketchObject.h | 8 + src/Mod/Sketcher/App/SketchObjectPy.xml | 5 + src/Mod/Sketcher/App/SketchObjectPyImp.cpp | 22 + .../Sketcher/Gui/CommandSketcherBSpline.cpp | 94 ++++ src/Mod/Sketcher/Gui/Resources/Sketcher.qrc | 1 + .../icons/splines/Sketcher_JoinCurves.svg | 520 ++++++++++++++++++ src/Mod/Sketcher/Gui/Workbench.cpp | 6 +- 8 files changed, 795 insertions(+), 2 deletions(-) create mode 100644 src/Mod/Sketcher/Gui/Resources/icons/splines/Sketcher_JoinCurves.svg diff --git a/src/Mod/Sketcher/App/SketchObject.cpp b/src/Mod/Sketcher/App/SketchObject.cpp index 2efa30663f..825772a701 100644 --- a/src/Mod/Sketcher/App/SketchObject.cpp +++ b/src/Mod/Sketcher/App/SketchObject.cpp @@ -3267,6 +3267,147 @@ int SketchObject::split(int GeoId, const Base::Vector3d &point) return -1; } +int SketchObject::join(int geoId1, Sketcher::PointPos posId1, + int geoId2, Sketcher::PointPos posId2) +{ + // No need to check input data validity as this is an sketchobject managed operation + + Base::StateLocker lock(managedoperation, true); + + if (Sketcher::PointPos::start != posId1 && Sketcher::PointPos::end != posId1 && + Sketcher::PointPos::start != posId2 && Sketcher::PointPos::end != posId2) { + THROWM(ValueError,"Invalid position(s): points must be start or end points of a curve."); + return -1; + } + + if (geoId1 == geoId2) { + THROWM(ValueError,"Connecting the end points of the same curve is not yet supported."); + return -1; + } + + if (geoId1 < 0 || geoId1 > getHighestCurveIndex() || + geoId2 < 0 || geoId2 > getHighestCurveIndex()) { + return -1; + } + + // get the old splines + auto* geo1 = getGeometry(geoId1); + auto* geo2 = getGeometry(geoId2); + + if (GeometryFacade::getConstruction(geo1) != GeometryFacade::getConstruction(geo2)) { + THROWM(ValueError,"Cannot join construction and non-construction geometries."); + return -1; + } + + // TODO: make both curves b-splines here itself + if (geo1->getTypeId() != Part::GeomBSplineCurve::getClassTypeId()) { + THROWM(ValueError,"Please convert both curves to NURBS before attempting to join them."); + return -1; + } + + if (geo2->getTypeId() != Part::GeomBSplineCurve::getClassTypeId()) { + THROWM(ValueError,"Please convert both curves to NURBS before attempting to join them."); + return -1; + } + + // TODO: is there a cleaner way to get our mutable bsp's? + // we need the splines to be mutable because we may reverse them + // and/or change their degree + auto* constbsp1 = static_cast(geo1); + auto* constbsp2 = static_cast(geo2); + const Handle(Geom_BSplineCurve) curve1 = Handle(Geom_BSplineCurve)::DownCast(constbsp1->handle()); + const Handle(Geom_BSplineCurve) curve2 = Handle(Geom_BSplineCurve)::DownCast(constbsp2->handle()); + std::unique_ptr bsp1(new Part::GeomBSplineCurve(curve1)); + std::unique_ptr bsp2(new Part::GeomBSplineCurve(curve2)); + + if (bsp1->isPeriodic() || bsp2->isPeriodic()) { + THROWM(ValueError,"It is only possible to join non-periodic curves."); + return -1; + } + + // reverse the splines if needed: join end of 1st to start of 2nd + if (Sketcher::PointPos::start == posId1) + bsp1->reverse(); + if (Sketcher::PointPos::end == posId2) + bsp2->reverse(); + + // ensure the degrees of both curves are the same + if (bsp1->getDegree() < bsp2->getDegree()) + bsp1->increaseDegree(bsp2->getDegree()); + else if (bsp2->getDegree() < bsp1->getDegree()) + bsp2->increaseDegree(bsp1->getDegree()); + + // TODO: set up vectors for new poles, knots, mults + std::vector poles1 = bsp1->getPoles(); + std::vector weights1 = bsp1->getWeights(); + std::vector knots1 = bsp1->getKnots(); + std::vector mults1 = bsp1->getMultiplicities(); + std::vector poles2 = bsp2->getPoles(); + std::vector weights2 = bsp2->getWeights(); + std::vector knots2 = bsp2->getKnots(); + std::vector mults2 = bsp2->getMultiplicities(); + + std::vector newPoles(std::move(poles1)); + std::vector newWeights(std::move(weights1)); + std::vector newKnots(std::move(knots1)); + std::vector newMults(std::move(mults1)); + + poles2.erase(poles2.begin()); + newPoles.insert(newPoles.end(), + std::make_move_iterator(poles2.begin()), + std::make_move_iterator(poles2.end())); + + // TODO: Weights might need to be scaled + weights2.erase(weights2.begin()); + newWeights.insert(newWeights.end(), + std::make_move_iterator(weights2.begin()), + std::make_move_iterator(weights2.end())); + + // knots of the second spline come after all of the first + double offset = newKnots.back() - knots2.front(); + knots2.erase(knots2.begin()); + for (auto& knot : knots2) + knot += offset; + newKnots.insert(newKnots.end(), + std::make_move_iterator(knots2.begin()), + std::make_move_iterator(knots2.end())); + + // end knots can have a multiplicity of (degree + 1) + if (bsp1->getDegree() < newMults.back()) + newMults.back() = bsp1->getDegree(); + mults2.erase(mults2.begin()); + newMults.insert(newMults.end(), + std::make_move_iterator(mults2.begin()), + std::make_move_iterator(mults2.end())); + + Part::GeomBSplineCurve* newSpline = + new Part::GeomBSplineCurve(newPoles, newWeights, newKnots, newMults, + bsp1->getDegree(), false, true); + + int newGeoId = addGeometry(newSpline); + + if (newGeoId < 0) { + THROWM(ValueError,"Failed to create joined curve."); + return -1; + } + else { + exposeInternalGeometry(newGeoId); + setConstruction(newGeoId, GeometryFacade::getConstruction(geo1)); + + // TODO: transfer constraints on the non-connected ends + auto otherPosId1 = (Sketcher::PointPos::start == posId1) ? Sketcher::PointPos::end : Sketcher::PointPos::start; + auto otherPosId2 = (Sketcher::PointPos::start == posId2) ? Sketcher::PointPos::end : Sketcher::PointPos::start; + + transferConstraints(geoId1, otherPosId1, newGeoId, PointPos::start, true); + transferConstraints(geoId2, otherPosId2, newGeoId, PointPos::end, true); + + delGeometries({geoId1, geoId2}); + return 0; + } + + return -1; +} + bool SketchObject::isExternalAllowed(App::Document *pDoc, App::DocumentObject *pObj, eReasonList* rsn) const { if (rsn) diff --git a/src/Mod/Sketcher/App/SketchObject.h b/src/Mod/Sketcher/App/SketchObject.h index 346a2d03ea..51b2b990c3 100644 --- a/src/Mod/Sketcher/App/SketchObject.h +++ b/src/Mod/Sketcher/App/SketchObject.h @@ -289,6 +289,14 @@ public: int extend(int geoId, double increment, PointPos endPoint); /// split a curve int split(int geoId, const Base::Vector3d &point); + /*! + \brief Join one or two curves at the given end points + \details The combined curve will be a b-spline + \param geoId1, posId1, geoId2, posId2: the end points to join + \retval - 0 on success, -1 on failure + */ + int join(int geoId1, Sketcher::PointPos posId1, + int geoId2, Sketcher::PointPos posId2); /// adds symmetric geometric elements with respect to the refGeoId (line or point) int addSymmetric(const std::vector &geoIdList, int refGeoId, Sketcher::PointPos refPosId=Sketcher::PointPos::none); diff --git a/src/Mod/Sketcher/App/SketchObjectPy.xml b/src/Mod/Sketcher/App/SketchObjectPy.xml index bf054202d2..9314b262ff 100644 --- a/src/Mod/Sketcher/App/SketchObjectPy.xml +++ b/src/Mod/Sketcher/App/SketchObjectPy.xml @@ -222,6 +222,11 @@ If there is no such constraint an exception is raised. split a curve with a given id at a given reference point + + + join two curves at the given end points + + add a symmetric geometric objects to the sketch with respect to a reference point or line diff --git a/src/Mod/Sketcher/App/SketchObjectPyImp.cpp b/src/Mod/Sketcher/App/SketchObjectPyImp.cpp index d4e5b02ec4..c250390dcc 100644 --- a/src/Mod/Sketcher/App/SketchObjectPyImp.cpp +++ b/src/Mod/Sketcher/App/SketchObjectPyImp.cpp @@ -1169,6 +1169,28 @@ PyObject* SketchObjectPy::split(PyObject *args) Py_Return; } +PyObject* SketchObjectPy::join(PyObject *args) +{ + int GeoId1(Sketcher::GeoEnum::GeoUndef), GeoId2(Sketcher::GeoEnum::GeoUndef); + int PosId1 = static_cast(Sketcher::PointPos::none), + PosId2 = static_cast(Sketcher::PointPos::none); + + if (!PyArg_ParseTuple(args, "iiii", &GeoId1, &PosId1, &GeoId2, &PosId2)) + return 0; + + if (this->getSketchObjectPtr()->join(GeoId1, (Sketcher::PointPos) PosId1, + GeoId2, (Sketcher::PointPos) PosId2)) { + std::stringstream str; + str << "Not able to join the curves with end points: (" + << GeoId1 << ", " << PosId1 << "), (" + << GeoId2 << ", " << PosId2 << ")"; + PyErr_SetString(PyExc_ValueError, str.str().c_str()); + return 0; + } + + Py_Return; +} + PyObject* SketchObjectPy::addSymmetric(PyObject *args) { PyObject *pcObj; diff --git a/src/Mod/Sketcher/Gui/CommandSketcherBSpline.cpp b/src/Mod/Sketcher/Gui/CommandSketcherBSpline.cpp index 4bc0290740..03e6b51ae1 100644 --- a/src/Mod/Sketcher/Gui/CommandSketcherBSpline.cpp +++ b/src/Mod/Sketcher/Gui/CommandSketcherBSpline.cpp @@ -1196,6 +1196,99 @@ bool CmdSketcherInsertKnot::isActive() return isSketcherBSplineActive(getActiveGuiDocument(), true); } +DEF_STD_CMD_A(CmdSketcherJoinCurves) + +CmdSketcherJoinCurves::CmdSketcherJoinCurves() + : Command("Sketcher_JoinCurves") +{ + sAppModule = "Sketcher"; + sGroup = "Sketcher"; + sMenuText = QT_TR_NOOP("Join curves"); + sToolTipText = QT_TR_NOOP("Join two curves at selected end points"); + sWhatsThis = "Sketcher_JoinCurves"; + sStatusTip = sToolTipText; + sPixmap = "Sketcher_JoinCurves"; + sAccel = ""; + eType = ForEdit; +} + +void CmdSketcherJoinCurves::activated(int iMsg) +{ + Q_UNUSED(iMsg); + + // TODO: confirm whether or not we need to check for OCC version +#if OCC_VERSION_HEX < 0x060900 + QMessageBox::warning(Gui::getMainWindow(), + QObject::tr("Wrong OCE/OCC version"), + QObject::tr("This version of OCE/OCC " + "does not support knot operation. " + "You need 6.9.0 or higher")); + return; +#endif + + // get the selection + std::vector selection; + selection = getSelection().getSelectionEx(0, Sketcher::SketchObject::getClassTypeId()); + + // only one sketch with its subelements are allowed to be selected + if (selection.size() != 1) { + return; + } + + // get the needed lists and objects + const std::vector &SubNames = selection[0].getSubNames(); + if (SubNames.size() == 0) { + // Check that something is selected + QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Selection is empty"), + QObject::tr("Nothing is selected. Please select end points of curves.")); + return; + } + + if (SubNames.size() != 2) { + QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Wrong selection"), + QObject::tr("Two end points should be selected.")); + return; + } + + Sketcher::SketchObject* Obj = static_cast(selection[0].getObject()); + + + int GeoIds[2]; + Sketcher::PointPos PosIds[2]; + getIdsFromName(SubNames[0], Obj, GeoIds[0], PosIds[0]); + getIdsFromName(SubNames[1], Obj, GeoIds[1], PosIds[1]); + + Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Join Curves")); + bool applied = false; + + try { + Gui::cmdAppObjectArgs(selection[0].getObject(), "join(%d, %d, %d, %d) ", + GeoIds[0], static_cast(PosIds[0]), + GeoIds[1], static_cast(PosIds[1])); + applied = true; + + // Warning: GeoId list will have changed + } + catch (const Base::Exception& e) { + QMessageBox::warning(Gui::getMainWindow(), QObject::tr("Error"), + QObject::tr(getStrippedPythonExceptionString(e).c_str())); + getSelection().clearSelection(); + } + + if (applied) + Gui::Command::commitCommand(); + else + Gui::Command::abortCommand(); + + tryAutoRecomputeIfNotSolve(Obj); + getSelection().clearSelection(); +} + +bool CmdSketcherJoinCurves::isActive(void) +{ + return isSketcherBSplineActive(getActiveGuiDocument(), true); +} + void CreateSketcherCommandsBSpline() { Gui::CommandManager &rcCmdMgr = Gui::Application::Instance->commandManager(); @@ -1213,4 +1306,5 @@ void CreateSketcherCommandsBSpline() rcCmdMgr.addCommand(new CmdSketcherDecreaseKnotMultiplicity()); rcCmdMgr.addCommand(new CmdSketcherCompModifyKnotMultiplicity()); rcCmdMgr.addCommand(new CmdSketcherInsertKnot()); + rcCmdMgr.addCommand(new CmdSketcherJoinCurves()); } diff --git a/src/Mod/Sketcher/Gui/Resources/Sketcher.qrc b/src/Mod/Sketcher/Gui/Resources/Sketcher.qrc index 0b0ded17d2..942e69851a 100644 --- a/src/Mod/Sketcher/Gui/Resources/Sketcher.qrc +++ b/src/Mod/Sketcher/Gui/Resources/Sketcher.qrc @@ -216,6 +216,7 @@ icons/splines/Sketcher_BSplineIncreaseDegree.svg icons/splines/Sketcher_BSplineIncreaseKnotMultiplicity.svg icons/splines/Sketcher_BSplineInsertKnot.svg + icons/splines/Sketcher_JoinCurves.svg icons/splines/Sketcher_BSplineKnotMultiplicity.svg icons/splines/Sketcher_BSplinePoleWeight.svg icons/splines/Sketcher_BSplinePolygon.svg diff --git a/src/Mod/Sketcher/Gui/Resources/icons/splines/Sketcher_JoinCurves.svg b/src/Mod/Sketcher/Gui/Resources/icons/splines/Sketcher_JoinCurves.svg new file mode 100644 index 0000000000..a84bed166e --- /dev/null +++ b/src/Mod/Sketcher/Gui/Resources/icons/splines/Sketcher_JoinCurves.svg @@ -0,0 +1,520 @@ + + + Sketcher_BSplineJoin + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Sketcher_BSplineJoin + + + [bitacovir] + + + Sketcher_Create_Periodic_BSpline + 06-03-2022 + + + + FreeCAD + + + + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Sketcher/Gui/Workbench.cpp b/src/Mod/Sketcher/Gui/Workbench.cpp index d4afbcc56f..04e4d10eda 100644 --- a/src/Mod/Sketcher/Gui/Workbench.cpp +++ b/src/Mod/Sketcher/Gui/Workbench.cpp @@ -423,7 +423,8 @@ inline void SketcherAddWorkbenchBSplines(Gui::MenuItem& bspline) << "Sketcher_BSplineDecreaseDegree" << "Sketcher_BSplineIncreaseKnotMultiplicity" << "Sketcher_BSplineDecreaseKnotMultiplicity" - << "Sketcher_BSplineInsertKnot"; + << "Sketcher_BSplineInsertKnot" + << "Sketcher_JoinCurves"; } template <> @@ -434,7 +435,8 @@ inline void SketcherAddWorkbenchBSplines(Gui::ToolBarItem& bsp << "Sketcher_BSplineIncreaseDegree" << "Sketcher_BSplineDecreaseDegree" << "Sketcher_CompModifyKnotMultiplicity" - << "Sketcher_BSplineInsertKnot"; + << "Sketcher_BSplineInsertKnot" + << "Sketcher_JoinCurves"; } template