// SPDX-License-Identifier: LGPL-2.1-or-later /*************************************************************************** * Copyright (c) 2020 Werner Mayer * * * * 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 #include #include #include #include #include #include #include #include #include #include #include #include "RemeshGmsh.h" #include "ui_RemeshGmsh.h" using namespace MeshGui; class GmshWidget::Private { public: explicit Private(QWidget* parent) : gmsh(parent) { /* coverity[uninit_ctor] Members of ui are set in setupUI() */ } void appendText(const QString& text, bool error) { syntax->setParagraphType( error ? Gui::DockWnd::ReportHighlighter::Error : Gui::DockWnd::ReportHighlighter::Message ); QTextCursor cursor(ui.outputWindow->document()); cursor.beginEditBlock(); cursor.movePosition(QTextCursor::End); cursor.insertText(text); cursor.endEditBlock(); ui.outputWindow->ensureCursorVisible(); } public: Ui_RemeshGmsh ui {}; QPointer label; QPointer syntax; QProcess gmsh; QElapsedTimer time; }; GmshWidget::GmshWidget(QWidget* parent, Qt::WindowFlags fl) : QWidget(parent, fl) , d(new Private(parent)) { d->ui.setupUi(this); setupConnections(); d->ui.fileChooser->onRestore(); d->syntax = new Gui::DockWnd::ReportHighlighter(d->ui.outputWindow); d->ui.outputWindow->setReadOnly(true); // 2D Meshing algorithms // https://gmsh.info/doc/texinfo/gmsh.html#index-Mesh_002eAlgorithm enum { MeshAdapt = 1, Automatic = 2, Delaunay = 5, FrontalDelaunay = 6, BAMG = 7, FrontalDelaunayForQuads = 8, PackingOfParallelograms = 9, QuasiStructuredQuad = 11 }; d->ui.method->addItem(tr("Automatic"), static_cast(Automatic)); d->ui.method->addItem(tr("Adaptive"), static_cast(MeshAdapt)); d->ui.method->addItem(QStringLiteral("Delaunay"), static_cast(Delaunay)); d->ui.method->addItem(tr("Frontal"), static_cast(FrontalDelaunay)); d->ui.method->addItem(QStringLiteral("BAMG"), static_cast(BAMG)); d->ui.method->addItem(tr("Frontal quad"), static_cast(FrontalDelaunayForQuads)); d->ui.method->addItem(tr("Parallelograms"), static_cast(PackingOfParallelograms)); d->ui.method->addItem(tr("Quasi-structured quad"), static_cast(QuasiStructuredQuad)); } GmshWidget::~GmshWidget() { d->ui.fileChooser->onSave(); } void GmshWidget::setupConnections() { // clang-format off connect(&d->gmsh, &QProcess::started, this, &GmshWidget::started); connect(&d->gmsh, qOverload(&QProcess::finished), this, &GmshWidget::finished); connect(&d->gmsh, &QProcess::errorOccurred, this, &GmshWidget::errorOccurred); connect(&d->gmsh, &QProcess::readyReadStandardError, this, &GmshWidget::readyReadStandardError); connect(&d->gmsh, &QProcess::readyReadStandardOutput, this, &GmshWidget::readyReadStandardOutput); connect(d->ui.killButton, &QPushButton::clicked, this, &GmshWidget::onKillButtonClicked); connect(d->ui.clearButton, &QPushButton::clicked, this, &GmshWidget::onClearButtonClicked); // clang-format on } void GmshWidget::changeEvent(QEvent* e) { if (e->type() == QEvent::LanguageChange) { d->ui.retranslateUi(this); } QWidget::changeEvent(e); } bool GmshWidget::writeProject(QString& inpFile, QString& outFile) { Q_UNUSED(inpFile) Q_UNUSED(outFile) return false; } bool GmshWidget::loadOutput() { return false; } int GmshWidget::meshingAlgorithm() const { return d->ui.method->itemData(d->ui.method->currentIndex()).toInt(); } double GmshWidget::getAngle() const { return d->ui.angle->value().getValue(); } double GmshWidget::getMaxSize() const { return d->ui.maxSize->value().getValue(); } double GmshWidget::getMinSize() const { return d->ui.minSize->value().getValue(); } void GmshWidget::accept() { if (d->gmsh.state() == QProcess::Running) { Base::Console().warning("Cannot start gmsh because it's already running\n"); return; } // clang-format off QString inpFile; QString outFile; if (writeProject(inpFile, outFile)) { // ./gmsh - -bin -2 /tmp/mesh.geo -o /tmp/best.stl QString proc = d->ui.fileChooser->fileName(); if (proc.isEmpty()) { proc = QLatin1String("gmsh"); } QStringList args; args << QLatin1String("-") << QLatin1String("-bin") << QLatin1String("-2") << inpFile << QLatin1String("-o") << outFile; d->gmsh.start(proc, args); d->time.start(); d->ui.labelTime->setText(tr("Time:")); } // clang-format on } void GmshWidget::readyReadStandardError() { QByteArray msg = d->gmsh.readAllStandardError(); if (msg.startsWith("\0[1m\0[31m")) { msg = msg.mid(9); } if (msg.endsWith("\0[0m")) { msg.chop(5); } QString text = QString::fromUtf8(msg.data()); d->appendText(text, true); } void GmshWidget::readyReadStandardOutput() { QByteArray msg = d->gmsh.readAllStandardOutput(); QString text = QString::fromUtf8(msg.data()); d->appendText(text, false); } void GmshWidget::onKillButtonClicked() { if (d->gmsh.state() == QProcess::Running) { d->gmsh.kill(); d->gmsh.waitForFinished(1000); d->ui.killButton->setDisabled(true); } } void GmshWidget::onClearButtonClicked() { d->ui.outputWindow->clear(); } void GmshWidget::started() { d->ui.killButton->setEnabled(true); if (!d->label) { d->label = new Gui::StatusWidget(this); d->label->setAttribute(Qt::WA_DeleteOnClose); d->label->setStatusText(tr("Running Gmsh…")); d->label->show(); } } void GmshWidget::finished(int /*exitCode*/, QProcess::ExitStatus exitStatus) { d->ui.killButton->setDisabled(true); if (d->label) { d->label->close(); } d->ui.labelTime->setText(QStringLiteral("%1 %2 ms").arg(tr("Time:")).arg(d->time.elapsed())); if (exitStatus == QProcess::NormalExit) { loadOutput(); } } void GmshWidget::errorOccurred(QProcess::ProcessError error) { QString msg; switch (error) { case QProcess::FailedToStart: msg = tr("Failed to start"); break; default: break; } if (!msg.isEmpty()) { QMessageBox::warning(this, tr("Error"), msg); } } void GmshWidget::reject() { onKillButtonClicked(); } // ------------------------------------------------- class RemeshGmsh::Private { public: explicit Private(Mesh::Feature* mesh) : mesh(mesh) {} public: App::DocumentObjectWeakPtrT mesh; MeshCore::MeshKernel copy; std::string stlFile; std::string geoFile; }; RemeshGmsh::RemeshGmsh(Mesh::Feature* mesh, QWidget* parent, Qt::WindowFlags fl) : GmshWidget(parent, fl) , d(new Private(mesh)) { // Copy mesh that is used each time when applying Gmsh's remeshing function d->copy = mesh->Mesh.getValue().getKernel(); d->stlFile = App::Application::getTempFileName() + "mesh.stl"; d->geoFile = App::Application::getTempFileName() + "mesh.geo"; } RemeshGmsh::~RemeshGmsh() = default; bool RemeshGmsh::writeProject(QString& inpFile, QString& outFile) { // clang-format off if (!d->mesh.expired()) { Base::FileInfo stl(d->stlFile); MeshCore::MeshOutput output(d->copy); Base::ofstream stlOut(stl, std::ios::out | std::ios::binary); output.SaveBinarySTL(stlOut); stlOut.close(); // Parameters int algorithm = meshingAlgorithm(); double maxSize = getMaxSize(); if (maxSize == 0.0) { maxSize = 1.0e22; } double minSize = getMinSize(); double angle = getAngle(); int maxAngle = 120; int minAngle = 20; // Gmsh geo file Base::FileInfo geo(d->geoFile); Base::ofstream geoOut(geo, std::ios::out); // Examples on how to use Gmsh: https://sfepy.org/doc-devel/preprocessing.html // https://gmsh.info//doc/texinfo/gmsh.html // https://docs.salome-platform.org/latest/gui/GMSHPLUGIN/gmsh_2d_3d_hypo_page.html geoOut << "// geo file for meshing with Gmsh meshing software created by FreeCAD\n" << "If(GMSH_MAJOR_VERSION < 4)\n" << " Error(\"Too old Gmsh version %g.%g. At least 4.x is required\", GMSH_MAJOR_VERSION, GMSH_MINOR_VERSION);\n" << " Exit;\n" << "EndIf\n" << "Merge \"" << stl.filePath() << "\";\n\n" << "// 2D mesh algorithm (1=MeshAdapt, 2=Automatic, 5=Delaunay, 6=Frontal, 7=BAMG, 8=Frontal Quad, 9=Packing of Parallelograms, 11=Quasi-structured Quad)\n" << "Mesh.Algorithm = " << algorithm << ";\n\n" << "// 3D mesh algorithm (1=Delaunay, 2=New Delaunay, 4=Frontal, 7=MMG3D, 9=R-tree, 10=HTX)\n" << "// Mesh.Algorithm3D = 1;\n\n" << "Mesh.CharacteristicLengthMax = " << maxSize << ";\n" << "Mesh.CharacteristicLengthMin = " << minSize << ";\n\n" << "// We first classify (\"color\") the surfaces by splitting the original surface\n" << "// along sharp geometrical features. This will create new discrete surfaces,\n" << "// curves and points.\n" << "angle = DefineNumber[" << angle << ", Min " << minAngle << ", Max " << maxAngle << ", Step 1,\n" << " Name \"Parameters/Angle for surface detection\" ];\n\n" << "forceParametrizablePatches = DefineNumber[0, Choices{0,1},\n" << " Name \"Parameters/Create surfaces guaranteed to be parametrizable\"];\n\n" << "includeBoundary = 1;\n" << "ClassifySurfaces{angle * Pi/180, includeBoundary, forceParametrizablePatches};\n" << "// Create a geometry for all the discrete curves and surfaces in the mesh, by\n" << "// computing a parametrization for each one\n" << "CreateGeometry;\n\n" << "// Create a volume as usual\n" << "Surface Loop(1) = Surface{:};\n" << "Volume(1) = {1};\n"; geoOut.close(); inpFile = QString::fromUtf8(d->geoFile.c_str()); outFile = QString::fromUtf8(d->stlFile.c_str()); return true; } return false; // clang-format on } bool RemeshGmsh::loadOutput() { if (d->mesh.expired()) { return false; } // Now read-in modified mesh Base::FileInfo stl(d->stlFile); Base::FileInfo geo(d->geoFile); Mesh::MeshObject kernel; MeshCore::MeshInput input(kernel.getKernel()); Base::ifstream stlIn(stl, std::ios::in | std::ios::binary); input.LoadBinarySTL(stlIn); stlIn.close(); kernel.harmonizeNormals(); Mesh::Feature* fea = d->mesh.get(); App::Document* doc = fea->getDocument(); doc->openTransaction("Remesh"); fea->Mesh.setValue(kernel.getKernel()); doc->commitTransaction(); stl.deleteFile(); geo.deleteFile(); return true; } // ------------------------------------------------- /* TRANSLATOR MeshGui::TaskRemeshGmsh */ TaskRemeshGmsh::TaskRemeshGmsh(Mesh::Feature* mesh) { widget = new RemeshGmsh(mesh); addTaskBox(widget, false); } void TaskRemeshGmsh::clicked(int id) { if (id == QDialogButtonBox::Apply) { widget->accept(); } else if (id == QDialogButtonBox::Close) { widget->reject(); } } #include "moc_RemeshGmsh.cpp"