429 lines
13 KiB
C++
429 lines
13 KiB
C++
// SPDX-License-Identifier: LGPL-2.1-or-later
|
|
|
|
/***************************************************************************
|
|
* Copyright (c) 2020 Werner Mayer <wmayer[at]users.sourceforge.net> *
|
|
* *
|
|
* 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 <QElapsedTimer>
|
|
#include <QMessageBox>
|
|
#include <QPointer>
|
|
#include <QTextCursor>
|
|
|
|
|
|
#include <App/Application.h>
|
|
#include <App/Document.h>
|
|
#include <App/DocumentObserver.h>
|
|
#include <Base/Console.h>
|
|
#include <Base/FileInfo.h>
|
|
#include <Base/Stream.h>
|
|
#include <Gui/ReportView.h>
|
|
#include <Mod/Mesh/App/MeshFeature.h>
|
|
|
|
#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<Gui::StatusWidget> label;
|
|
QPointer<Gui::DockWnd::ReportHighlighter> 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<int>(Automatic));
|
|
d->ui.method->addItem(tr("Adaptive"), static_cast<int>(MeshAdapt));
|
|
d->ui.method->addItem(QStringLiteral("Delaunay"), static_cast<int>(Delaunay));
|
|
d->ui.method->addItem(tr("Frontal"), static_cast<int>(FrontalDelaunay));
|
|
d->ui.method->addItem(QStringLiteral("BAMG"), static_cast<int>(BAMG));
|
|
d->ui.method->addItem(tr("Frontal quad"), static_cast<int>(FrontalDelaunayForQuads));
|
|
d->ui.method->addItem(tr("Parallelograms"), static_cast<int>(PackingOfParallelograms));
|
|
d->ui.method->addItem(tr("Quasi-structured quad"), static_cast<int>(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<int, QProcess::ExitStatus>(&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<Mesh::Feature>();
|
|
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"
|