From b47d72f8def4d861a3902b58cc91cf62000f7c8d Mon Sep 17 00:00:00 2001 From: wmayer Date: Wed, 3 Apr 2024 15:23:14 +0200 Subject: [PATCH] RE: Add function to approximate B-Spline from points --- .../App/AppReverseEngineering.cpp | 164 ++++++++++++ src/Mod/ReverseEngineering/Gui/CMakeLists.txt | 3 + src/Mod/ReverseEngineering/Gui/Command.cpp | 35 +++ .../Gui/FitBSplineCurve.cpp | 182 +++++++++++++ .../ReverseEngineering/Gui/FitBSplineCurve.h | 81 ++++++ .../ReverseEngineering/Gui/FitBSplineCurve.ui | 249 ++++++++++++++++++ src/Mod/ReverseEngineering/Gui/Workbench.cpp | 3 +- 7 files changed, 716 insertions(+), 1 deletion(-) create mode 100644 src/Mod/ReverseEngineering/Gui/FitBSplineCurve.cpp create mode 100644 src/Mod/ReverseEngineering/Gui/FitBSplineCurve.h create mode 100644 src/Mod/ReverseEngineering/Gui/FitBSplineCurve.ui diff --git a/src/Mod/ReverseEngineering/App/AppReverseEngineering.cpp b/src/Mod/ReverseEngineering/App/AppReverseEngineering.cpp index 8403212f38..311cf73e8b 100644 --- a/src/Mod/ReverseEngineering/App/AppReverseEngineering.cpp +++ b/src/Mod/ReverseEngineering/App/AppReverseEngineering.cpp @@ -74,6 +74,7 @@ class Module : public Py::ExtensionModule public: Module() : Py::ExtensionModule("ReverseEngineering") { + add_keyword_method("approxCurve", &Module::approxCurve, "Approximate curve"); add_keyword_method("approxSurface",&Module::approxSurface, "approxSurface(Points, UDegree=3, VDegree=3, NbUPoles=6, NbVPoles=6,\n" "Smooth=True, Weight=0.1, Grad=1.0, Bend=0.0, Curv=0.0\n" @@ -159,6 +160,169 @@ public: } private: + static std::vector getPoints(PyObject* pts, bool closed) + { + std::vector data; + if (PyObject_TypeCheck(pts, &(Points::PointsPy::Type))) { + std::vector normal; + auto pypts = static_cast(pts); + Points::PointKernel* points = pypts->getPointKernelPtr(); + points->getPoints(data, normal, 0.0); + } + else { + Py::Sequence l(pts); + data.reserve(l.size()); + for (Py::Sequence::iterator it = l.begin(); it != l.end(); ++it) { + Py::Tuple t(*it); + data.emplace_back( + Py::Float(t.getItem(0)), + Py::Float(t.getItem(1)), + Py::Float(t.getItem(2)) + ); + } + } + + if (closed) { + if (!data.empty()) { + data.push_back(data.front()); + } + } + + return data; + } + + static PyObject* approx1(const Py::Tuple& args, const Py::Dict& kwds) + { + PyObject* pts {}; + PyObject* closed = Py_False; + int minDegree = 3; // NOLINT + int maxDegree = 8; // NOLINT + int cont = int(GeomAbs_C2); + double tol3d = 1.0e-3; // NOLINT + + static const std::array kwds_approx{"Points", + "Closed", + "MinDegree", + "MaxDegree", + "Continuity", + "Tolerance", + nullptr}; + if (!Base::Wrapped_ParseTupleAndKeywords(args.ptr(), kwds.ptr(), "O|O!iiid", kwds_approx, + &pts, &PyBool_Type, &closed, &minDegree, + &maxDegree, &cont, &tol3d)) { + return nullptr; + } + + std::vector data = getPoints(pts, Base::asBoolean(closed)); + + Part::GeomBSplineCurve curve; + curve.approximate(data, minDegree, maxDegree, GeomAbs_Shape(cont), tol3d); + return curve.getPyObject(); + } + + static PyObject* approx2(const Py::Tuple& args, const Py::Dict& kwds) + { + PyObject* pts {}; + char* parType {}; + PyObject* closed = Py_False; + int minDegree = 3; // NOLINT + int maxDegree = 8; // NOLINT + int cont = int(GeomAbs_C2); + double tol3d = 1.0e-3; // NOLINT + + static const std::array kwds_approx{"Points", + "ParametrizationType", + "Closed", + "MinDegree", + "MaxDegree", + "Continuity", + "Tolerance", + nullptr}; + if (!Base::Wrapped_ParseTupleAndKeywords(args.ptr(), kwds.ptr(), "Os|O!iiid", kwds_approx, + &pts, &parType, &PyBool_Type, &closed, &minDegree, + &maxDegree, &cont, &tol3d)) { + return nullptr; + } + + std::vector data = getPoints(pts, Base::asBoolean(closed)); + + Approx_ParametrizationType pt {Approx_ChordLength}; + std::string pstr = parType; + if (pstr == "Uniform") { + pt = Approx_IsoParametric; + } + else if (pstr == "Centripetal") { + pt = Approx_Centripetal; + } + + Part::GeomBSplineCurve curve; + curve.approximate(data, pt, minDegree, maxDegree, GeomAbs_Shape(cont), tol3d); + return curve.getPyObject(); + } + + static PyObject* approx3(const Py::Tuple& args, const Py::Dict& kwds) + { + PyObject* pts {}; + double weight1 {}; + double weight2 {}; + double weight3 {}; + PyObject* closed = Py_False; + int maxDegree = 8; // NOLINT + int cont = int(GeomAbs_C2); + double tol3d = 1.0e-3; // NOLINT + + static const std::array kwds_approx{"Points", + "Weight1", + "Weight2", + "Weight3", + "Closed", + "MaxDegree", + "Continuity", + "Tolerance", + nullptr}; + if (!Base::Wrapped_ParseTupleAndKeywords(args.ptr(), kwds.ptr(), "Oddd|O!iid", kwds_approx, + &pts, &weight1, &weight2, &weight3, + &PyBool_Type, &closed, + &maxDegree, &cont, &tol3d)) { + return nullptr; + } + + std::vector data = getPoints(pts, Base::asBoolean(closed)); + + Part::GeomBSplineCurve curve; + curve.approximate(data, weight1, weight2, weight3, maxDegree, GeomAbs_Shape(cont), tol3d); + return curve.getPyObject(); + } + + Py::Object approxCurve(const Py::Tuple& args, const Py::Dict& kwds) + { + try { + using approxFunc = std::function; + + std::vector funcs; + funcs.emplace_back(approx3); + funcs.emplace_back(approx2); + funcs.emplace_back(approx1); + + for (const auto& func : funcs) { + if (PyObject* py = func(args, kwds)) { + return Py::asObject(py); + } + + PyErr_Clear(); + } + + throw Py::ValueError("Wrong arguments ReverseEngineering.approxCurve()"); + } + catch (const Base::Exception& e) { + std::string msg = e.what(); + if (msg.empty()) { + msg = "ReverseEngineering.approxCurve() failed"; + } + throw Py::RuntimeError(msg); + } + } + Py::Object approxSurface(const Py::Tuple& args, const Py::Dict& kwds) { PyObject *o; diff --git a/src/Mod/ReverseEngineering/Gui/CMakeLists.txt b/src/Mod/ReverseEngineering/Gui/CMakeLists.txt index 2bf3d465a0..1cdcd8944c 100644 --- a/src/Mod/ReverseEngineering/Gui/CMakeLists.txt +++ b/src/Mod/ReverseEngineering/Gui/CMakeLists.txt @@ -23,6 +23,7 @@ qt_create_resource_file(${ReverseEngineering_TR_QRC} ${QM_SRCS}) qt_add_resources(ReenGui_QRC_SRCS Resources/ReverseEngineering.qrc ${ReverseEngineering_TR_QRC}) set(Dialogs_UIC_SRCS + FitBSplineCurve.ui FitBSplineSurface.ui Poisson.ui Segmentation.ui @@ -32,6 +33,8 @@ set(Dialogs_UIC_SRCS SET(Dialogs_SRCS ${Dialogs_UIC_HDRS} ${Dialogs_UIC_SRCS} + FitBSplineCurve.cpp + FitBSplineCurve.h FitBSplineSurface.cpp FitBSplineSurface.h Poisson.cpp diff --git a/src/Mod/ReverseEngineering/Gui/Command.cpp b/src/Mod/ReverseEngineering/Gui/Command.cpp index aa46f9f888..ad3b390e68 100644 --- a/src/Mod/ReverseEngineering/Gui/Command.cpp +++ b/src/Mod/ReverseEngineering/Gui/Command.cpp @@ -51,6 +51,7 @@ #include #include +#include "FitBSplineCurve.h" #include "FitBSplineSurface.h" #include "Poisson.h" #include "Segmentation.h" @@ -59,6 +60,39 @@ using namespace std; +DEF_STD_CMD_A(CmdApproxCurve) + +CmdApproxCurve::CmdApproxCurve() + : Command("Reen_ApproxCurve") +{ + sAppModule = "Reen"; + sGroup = QT_TR_NOOP("Reverse Engineering"); + sMenuText = QT_TR_NOOP("Approximate B-spline curve..."); + sToolTipText = QT_TR_NOOP("Approximate a B-spline curve"); + sWhatsThis = "Reen_ApproxCurve"; + sStatusTip = sToolTipText; +} + +void CmdApproxCurve::activated(int) +{ + App::DocumentObjectT objT; + auto obj = Gui::Selection().getObjectsOfType(App::GeoFeature::getClassTypeId()); + if (obj.size() != 1 || !(obj.at(0)->isDerivedFrom(Points::Feature::getClassTypeId()))) { + QMessageBox::warning(Gui::getMainWindow(), + qApp->translate("Reen_ApproxSurface", "Wrong selection"), + qApp->translate("Reen_ApproxSurface", "Please select a point cloud.")); + return; + } + + objT = obj.front(); + Gui::Control().showDialog(new ReenGui::TaskFitBSplineCurve(objT)); +} + +bool CmdApproxCurve::isActive() +{ + return (hasActiveDocument() && !Gui::Control().activeDialog()); +} + DEF_STD_CMD_A(CmdApproxSurface) CmdApproxSurface::CmdApproxSurface() @@ -648,6 +682,7 @@ bool CmdViewTriangulation::isActive() void CreateReverseEngineeringCommands() { Gui::CommandManager& rcCmdMgr = Gui::Application::Instance->commandManager(); + rcCmdMgr.addCommand(new CmdApproxCurve()); rcCmdMgr.addCommand(new CmdApproxSurface()); rcCmdMgr.addCommand(new CmdApproxPlane()); rcCmdMgr.addCommand(new CmdApproxCylinder()); diff --git a/src/Mod/ReverseEngineering/Gui/FitBSplineCurve.cpp b/src/Mod/ReverseEngineering/Gui/FitBSplineCurve.cpp new file mode 100644 index 0000000000..d411ff4c34 --- /dev/null +++ b/src/Mod/ReverseEngineering/Gui/FitBSplineCurve.cpp @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +/*************************************************************************** + * Copyright (c) 2024 Werner Mayer * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + **************************************************************************/ + +#include "PreCompiled.h" +#ifndef _PreComp_ +#include +#include +#endif + +#include +#include + +#include "FitBSplineCurve.h" +#include "ui_FitBSplineCurve.h" + + +using namespace ReenGui; + +class FitBSplineCurveWidget::Private +{ +public: + Ui_FitBSplineCurve ui {}; + App::DocumentObjectT obj {}; +}; + +/* TRANSLATOR ReenGui::FitBSplineCurveWidget */ + +FitBSplineCurveWidget::FitBSplineCurveWidget(const App::DocumentObjectT& obj, QWidget* parent) + : d(new Private()) +{ + Q_UNUSED(parent); + d->ui.setupUi(this); + d->obj = obj; + + // clang-format off + connect(d->ui.checkBox, &QCheckBox::toggled, + this, &FitBSplineCurveWidget::toggleParametrizationType); + connect(d->ui.groupBoxSmooth, &QGroupBox::toggled, + this, &FitBSplineCurveWidget::toggleSmoothing); + // clang-format on +} + +FitBSplineCurveWidget::~FitBSplineCurveWidget() +{ + delete d; +} + +void FitBSplineCurveWidget::toggleParametrizationType(bool on) +{ + d->ui.paramType->setEnabled(on); + if (on) { + d->ui.groupBoxSmooth->setChecked(false); + } +} + +void FitBSplineCurveWidget::toggleSmoothing(bool on) +{ + if (on) { + d->ui.checkBox->setChecked(false); + d->ui.paramType->setEnabled(false); + } +} + +bool FitBSplineCurveWidget::accept() +{ + try { + tryAccept(); + } + catch (const Base::Exception& e) { + Gui::Command::abortCommand(); + QMessageBox::warning(this, tr("Input error"), QString::fromLatin1(e.what())); + return false; + } + + return true; +} + +void FitBSplineCurveWidget::tryAccept() +{ + QString document = QString::fromStdString(d->obj.getDocumentPython()); + QString object = QString::fromStdString(d->obj.getObjectPython()); + + QStringList arguments; + arguments.append( + QString::fromLatin1("Points=getattr(%1, %1.getPropertyNameOfGeometry())").arg(object)); + if (!d->ui.groupBoxSmooth->isChecked()) { + arguments.append(QString::fromLatin1("MinDegree = %1").arg(d->ui.degreeMin->value())); + } + arguments.append(QString::fromLatin1("MaxDegree = %1").arg(d->ui.degreeMax->value())); + arguments.append(QString::fromLatin1("Continuity = %1").arg(d->ui.continuity->currentIndex())); + if (d->ui.checkBoxClosed->isChecked()) { + arguments.append(QString::fromLatin1("Closed = True")); + } + else { + arguments.append(QString::fromLatin1("Closed = False")); + } + if (d->ui.checkBox->isChecked()) { + int index = d->ui.paramType->currentIndex(); + arguments.append(QString::fromLatin1("ParametrizationType = %1").arg(index)); + } + if (d->ui.groupBoxSmooth->isChecked()) { + arguments.append(QString::fromLatin1("Weight1 = %1").arg(d->ui.curveLength->value())); + arguments.append(QString::fromLatin1("Weight2 = %1").arg(d->ui.curvature->value())); + arguments.append(QString::fromLatin1("Weight3 = %1").arg(d->ui.torsion->value())); + } + + QString argument = arguments.join(QLatin1String(", ")); + QString command = QString::fromLatin1("%1.addObject(\"Part::Spline\", \"Spline\").Shape = " + "ReverseEngineering.approxCurve(%2).toShape()") + .arg(document, argument); + + tryCommand(command); +} + +void FitBSplineCurveWidget::exeCommand(const QString& cmd) +{ + Gui::WaitCursor wc; + Gui::Command::addModule(Gui::Command::App, "ReverseEngineering"); + Gui::Command::openCommand(QT_TRANSLATE_NOOP("Command", "Fit B-Spline")); + Gui::Command::runCommand(Gui::Command::Doc, cmd.toLatin1()); + Gui::Command::commitCommand(); + Gui::Command::updateActive(); +} + +void FitBSplineCurveWidget::tryCommand(const QString& cmd) +{ + try { + exeCommand(cmd); + } + catch (const Base::Exception& e) { + Gui::Command::abortCommand(); + e.ReportException(); + } +} + + +void FitBSplineCurveWidget::changeEvent(QEvent* e) +{ + QWidget::changeEvent(e); + if (e->type() == QEvent::LanguageChange) { + d->ui.retranslateUi(this); + } +} + + +/* TRANSLATOR ReenGui::TaskFitBSplineCurve */ + +TaskFitBSplineCurve::TaskFitBSplineCurve(const App::DocumentObjectT& obj) + : widget {new FitBSplineCurveWidget(obj)} +{ + addTaskBox(widget); +} + +void TaskFitBSplineCurve::open() +{} + +bool TaskFitBSplineCurve::accept() +{ + return widget->accept(); +} + +#include "moc_FitBSplineCurve.cpp" diff --git a/src/Mod/ReverseEngineering/Gui/FitBSplineCurve.h b/src/Mod/ReverseEngineering/Gui/FitBSplineCurve.h new file mode 100644 index 0000000000..ce340e4523 --- /dev/null +++ b/src/Mod/ReverseEngineering/Gui/FitBSplineCurve.h @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later + +/*************************************************************************** + * Copyright (c) 2024 Werner Mayer * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD 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 * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + **************************************************************************/ + +#ifndef REENGUI_FITBSPLINECURVE_H +#define REENGUI_FITBSPLINECURVE_H + +#include +#include + + +namespace ReenGui +{ + +class FitBSplineCurveWidget: public QWidget +{ + Q_OBJECT + +public: + explicit FitBSplineCurveWidget(const App::DocumentObjectT&, QWidget* parent = nullptr); + ~FitBSplineCurveWidget() override; + + bool accept(); + +protected: + void changeEvent(QEvent* e) override; + +private: + void toggleParametrizationType(bool on); + void toggleSmoothing(bool on); + void tryAccept(); + void exeCommand(const QString&); + void tryCommand(const QString&); + +private: + class Private; + Private* d; +}; + +class TaskFitBSplineCurve: public Gui::TaskView::TaskDialog +{ + Q_OBJECT + +public: + explicit TaskFitBSplineCurve(const App::DocumentObjectT&); + +public: + void open() override; + bool accept() override; + + QDialogButtonBox::StandardButtons getStandardButtons() const override + { + return QDialogButtonBox::Ok | QDialogButtonBox::Cancel; + } + +private: + FitBSplineCurveWidget* widget; +}; + +} // namespace ReenGui + +#endif // REENGUI_FITBSPLINECURVE_H diff --git a/src/Mod/ReverseEngineering/Gui/FitBSplineCurve.ui b/src/Mod/ReverseEngineering/Gui/FitBSplineCurve.ui new file mode 100644 index 0000000000..e2e7e51875 --- /dev/null +++ b/src/Mod/ReverseEngineering/Gui/FitBSplineCurve.ui @@ -0,0 +1,249 @@ + + + ReenGui::FitBSplineCurve + + + + 0 + 0 + 360 + 375 + + + + Fit B-spline surface + + + + + + Parameters + + + + + + Maximum degree + + + + + + + false + + + + Chord length + + + + + Centripetal + + + + + Iso-Parametric + + + + + + + + Continuity + + + + + + + 2 + + + 11 + + + 6 + + + + + + + 1 + + + 11 + + + 2 + + + + + + + Parametrization type + + + + + + + + C0 + + + + + G1 + + + + + C1 + + + + + G2 + + + + + C2 + + + + + C3 + + + + + CN + + + + + + + + Minimum degree + + + + + + + Closed curve + + + + + + + + + + Smoothing + + + true + + + + + + Torsion + + + + + + + Curve length + + + + + + + Curvature + + + + + + + 1.000000000000000 + + + 0.100000000000000 + + + 1.000000000000000 + + + + + + + 1.000000000000000 + + + 0.100000000000000 + + + 1.000000000000000 + + + + + + + 1.000000000000000 + + + 0.100000000000000 + + + 1.000000000000000 + + + + + + + + + + Qt::Vertical + + + + 20 + 15 + + + + + + + + degreeMin + degreeMax + continuity + checkBox + paramType + checkBoxClosed + groupBoxSmooth + curveLength + curvature + torsion + + + + diff --git a/src/Mod/ReverseEngineering/Gui/Workbench.cpp b/src/Mod/ReverseEngineering/Gui/Workbench.cpp index fcc098aba7..662f9fee6d 100644 --- a/src/Mod/ReverseEngineering/Gui/Workbench.cpp +++ b/src/Mod/ReverseEngineering/Gui/Workbench.cpp @@ -74,7 +74,8 @@ Gui::MenuItem* Workbench::setupMenuBar() const << "Reen_ApproxSphere" << "Reen_ApproxPolynomial" << "Separator" - << "Reen_ApproxSurface"; + << "Reen_ApproxSurface" + << "Reen_ApproxCurve"; *reen << approx; return root;