// SPDX-License-Identifier: LGPL-2.1-or-later /*************************************************************************** * Copyright (c) 2010 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 #include #include #include #include #include #include "Tessellation.h" #include "ui_Tessellation.h" using namespace MeshPartGui; /* TRANSLATOR MeshPartGui::Tessellation */ Tessellation::Tessellation(QWidget* parent) : QWidget(parent) , ui(new Ui_Tessellation) { ui->setupUi(this); gmsh = new Mesh2ShapeGmsh(this); setupConnections(); ui->stackedWidget->addTab(gmsh, tr("Gmsh")); ParameterGrp::handle handle = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/Mod/Mesh/Meshing/Standard" ); double value = ui->spinSurfaceDeviation->value().getValue(); value = handle->GetFloat("LinearDeflection", value); double angle = ui->spinAngularDeviation->value().getValue(); angle = handle->GetFloat("AngularDeflection", angle); bool relative = ui->relativeDeviation->isChecked(); relative = handle->GetBool("RelativeLinearDeflection", relative); ui->relativeDeviation->setChecked(relative); ui->spinSurfaceDeviation->setMaximum(std::numeric_limits::max()); ui->spinSurfaceDeviation->setValue(value); ui->spinAngularDeviation->setValue(angle); ui->spinMaximumEdgeLength->setRange(0, std::numeric_limits::max()); ui->comboFineness->setCurrentIndex(2); onComboFinenessCurrentIndexChanged(2); #if !defined(HAVE_MEFISTO) ui->stackedWidget->setTabEnabled(Mefisto, false); #endif #if !defined(HAVE_NETGEN) ui->stackedWidget->setTabEnabled(Netgen, false); #endif Gui::Command::doCommand(Gui::Command::Doc, "import Mesh, Part, PartGui"); try { Gui::Command::doCommand(Gui::Command::Doc, "import MeshPart"); } catch (...) { ui->stackedWidget->setTabEnabled(Mefisto, false); ui->stackedWidget->setTabEnabled(Netgen, false); } } Tessellation::~Tessellation() = default; void Tessellation::setupConnections() { connect(gmsh, &Mesh2ShapeGmsh::processed, this, &Tessellation::gmshProcessed); connect( ui->estimateMaximumEdgeLength, &QPushButton::clicked, this, &Tessellation::onEstimateMaximumEdgeLengthClicked ); connect( ui->comboFineness, qOverload(&QComboBox::currentIndexChanged), this, &Tessellation::onComboFinenessCurrentIndexChanged ); connect(ui->checkSecondOrder, &QCheckBox::toggled, this, &Tessellation::onCheckSecondOrderToggled); connect(ui->checkQuadDominated, &QCheckBox::toggled, this, &Tessellation::onCheckQuadDominatedToggled); } void Tessellation::meshingMethod(int id) { ui->stackedWidget->setCurrentIndex(id); } void Tessellation::onComboFinenessCurrentIndexChanged(int index) { // NOLINTBEGIN if (index == 5) { ui->doubleGrading->setEnabled(true); ui->spinEdgeElements->setEnabled(true); ui->spinCurvatureElements->setEnabled(true); } else { ui->doubleGrading->setEnabled(false); ui->spinEdgeElements->setEnabled(false); ui->spinCurvatureElements->setEnabled(false); } switch (index) { case VeryCoarse: ui->doubleGrading->setValue(0.7); ui->spinEdgeElements->setValue(0.3); ui->spinCurvatureElements->setValue(1.0); break; case Coarse: ui->doubleGrading->setValue(0.5); ui->spinEdgeElements->setValue(0.5); ui->spinCurvatureElements->setValue(1.5); break; case Moderate: ui->doubleGrading->setValue(0.3); ui->spinEdgeElements->setValue(1.0); ui->spinCurvatureElements->setValue(2.0); break; case Fine: ui->doubleGrading->setValue(0.2); ui->spinEdgeElements->setValue(2.0); ui->spinCurvatureElements->setValue(3.0); break; case VeryFine: ui->doubleGrading->setValue(0.1); ui->spinEdgeElements->setValue(3.0); ui->spinCurvatureElements->setValue(5.0); break; default: break; } // NOLINTEND } void Tessellation::onCheckSecondOrderToggled(bool on) { if (on) { ui->checkQuadDominated->setChecked(false); } } void Tessellation::onCheckQuadDominatedToggled(bool on) { if (on) { ui->checkSecondOrder->setChecked(false); } } void Tessellation::gmshProcessed() { bool doClose = !ui->checkBoxDontQuit->isChecked(); if (doClose) { Gui::Control().reject(); } } void Tessellation::changeEvent(QEvent* e) { if (e->type() == QEvent::LanguageChange) { int index = ui->comboFineness->currentIndex(); ui->retranslateUi(this); ui->comboFineness->setCurrentIndex(index); } QWidget::changeEvent(e); } void Tessellation::onEstimateMaximumEdgeLengthClicked() { App::Document* activeDoc = App::GetApplication().getActiveDocument(); if (!activeDoc) { return; } Gui::Document* activeGui = Gui::Application::Instance->getDocument(activeDoc); if (!activeGui) { return; } double edgeLen = 0; for (auto& sel : Gui::Selection().getSelection("*", Gui::ResolveMode::NoResolve)) { auto shape = Part::Feature::getTopoShape( sel.pObject, Part::ShapeOption::ResolveLink | Part::ShapeOption::Transform, sel.SubName ); if (shape.hasSubShape(TopAbs_FACE)) { Base::BoundBox3d bbox = shape.getBoundBox(); edgeLen = std::max(edgeLen, bbox.LengthX()); edgeLen = std::max(edgeLen, bbox.LengthY()); edgeLen = std::max(edgeLen, bbox.LengthZ()); } } ui->spinMaximumEdgeLength->setValue(edgeLen / 10); // NOLINT } bool Tessellation::accept() { std::list shapeObjects; App::Document* activeDoc = App::GetApplication().getActiveDocument(); if (!activeDoc) { QMessageBox::critical(this, windowTitle(), tr("No active document")); return false; } Gui::Document* activeGui = Gui::Application::Instance->getDocument(activeDoc); if (!activeGui) { QMessageBox::critical(this, windowTitle(), tr("No active document")); return false; } this->document = QString::fromLatin1(activeDoc->getName()); bool bodyWithNoTip = false; bool partWithNoFace = false; for (auto& sel : Gui::Selection().getSelection("*", Gui::ResolveMode::NoResolve)) { auto shape = Part::Feature::getTopoShape( sel.pObject, Part::ShapeOption::ResolveLink | Part::ShapeOption::Transform, sel.SubName ); if (shape.hasSubShape(TopAbs_FACE)) { shapeObjects.emplace_back(sel.pObject, sel.SubName); } else if (sel.pObject) { if (sel.pObject->isDerivedFrom()) { partWithNoFace = true; } if (auto body = dynamic_cast(sel.pObject)) { if (!body->Tip.getValue()) { bodyWithNoTip = true; } } } } if (shapeObjects.empty()) { if (bodyWithNoTip) { QMessageBox::critical( this, windowTitle(), tr("Error: body without a tip selected.\n" "Either set the tip of the body or select a different shape.") ); } else if (partWithNoFace) { QMessageBox::critical( this, windowTitle(), tr("Error: shape without faces selected.\n" "Select a different shape.") ); } else { QMessageBox::critical(this, windowTitle(), tr("Select a shape for meshing, first.")); } return false; } bool doClose = !ui->checkBoxDontQuit->isChecked(); int method = ui->stackedWidget->currentIndex(); // For Gmsh the workflow is very different because it uses an executable // and therefore things are asynchronous if (method == Gmsh) { gmsh->process(activeDoc, shapeObjects); return false; } process(method, activeDoc, shapeObjects); return doClose; } void Tessellation::process(int method, App::Document* doc, const std::list& shapeObjects) { try { Gui::WaitCursor wc; saveParameters(method); doc->openTransaction("Meshing"); for (auto& info : shapeObjects) { QString subname = QString::fromLatin1(info.getSubName().c_str()); QString objname = QString::fromLatin1(info.getObjectName().c_str()); auto obj = info.getObject(); if (!obj) { continue; } auto sobj = obj->getSubObject(info.getSubName().c_str()); if (!sobj) { continue; } sobj = sobj->getLinkedObject(true); if (!sobj) { continue; } QString label = QString::fromUtf8(sobj->Label.getValue()); QString param = getMeshingParameters(method, sobj); QString cmd = QStringLiteral( "__doc__=FreeCAD.getDocument(\"%1\")\n" "__mesh__=__doc__.addObject(\"Mesh::Feature\",\"Mesh\")\n" "__part__=__doc__.getObject(\"%2\")\n" "__shape__=Part.getShape(__part__,\"%3\")\n" "__mesh__.Mesh=MeshPart.meshFromShape(%4)\n" "__mesh__.Label=\"%5 (Meshed)\"\n" "del __doc__, __mesh__, __part__, __shape__\n" ) .arg(this->document, objname, subname, param, label); Gui::Command::runCommand(Gui::Command::Doc, cmd.toUtf8()); setFaceColors(method, doc, sobj); } doc->commitTransaction(); } catch (const Base::Exception& e) { doc->abortTransaction(); Base::Console().error(e.what()); } } void Tessellation::saveParameters(int method) { if (method == Standard) { ParameterGrp::handle handle = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/Mod/Mesh/Meshing/Standard" ); double value = ui->spinSurfaceDeviation->value().getValue(); handle->SetFloat("LinearDeflection", value); double angle = ui->spinAngularDeviation->value().getValue(); handle->SetFloat("AngularDeflection", angle); bool relative = ui->relativeDeviation->isChecked(); handle->SetBool("RelativeLinearDeflection", relative); } } void Tessellation::setFaceColors(int method, App::Document* doc, App::DocumentObject* obj) { // if Standard mesher is used and face colors should be applied if (method == Standard) { if (ui->meshShapeColors->isChecked()) { Gui::ViewProvider* vpm = Gui::Application::Instance->getViewProvider( doc->getActiveObject() ); auto vpmesh = dynamic_cast(vpm); auto svp = freecad_cast( Gui::Application::Instance->getViewProvider(obj) ); if (vpmesh && svp) { std::vector diff_col = svp->ShapeAppearance.getDiffuseColors(); if (ui->groupsFaceColors->isChecked()) { diff_col = getUniqueColors(diff_col); } vpmesh->highlightSegments(diff_col); addFaceColors(vpmesh->getObject(), diff_col); } } } } void Tessellation::addFaceColors(Mesh::Feature* mesh, const std::vector& colorPerSegm) { const Mesh::MeshObject& kernel = mesh->Mesh.getValue(); unsigned long numSegm = kernel.countSegments(); if (numSegm > 0 && numSegm == colorPerSegm.size()) { unsigned long uCtFacets = kernel.countFacets(); std::vector colorPerFace(uCtFacets); for (unsigned long i = 0; i < numSegm; i++) { Base::Color segmColor = colorPerSegm[i]; std::vector segm = kernel.getSegment(i).getIndices(); for (Mesh::FacetIndex it : segm) { colorPerFace[it] = segmColor; } } auto typeId = App::PropertyColorList::getClassTypeId(); if (auto prop = dynamic_cast( mesh->addDynamicProperty(typeId.getName(), "FaceColors") )) { prop->setValues(colorPerFace); } } } std::vector Tessellation::getUniqueColors(const std::vector& colors) const { // unique colors std::set col_set; for (const auto& it : colors) { col_set.insert(it.getPackedValue()); } std::vector unique; unique.reserve(col_set.size()); for (const auto& it : col_set) { unique.emplace_back(it); } return unique; } QString Tessellation::getMeshingParameters(int method, App::DocumentObject* obj) const { QString param; if (method == Standard) { param = getStandardParameters(obj); } else if (method == Mefisto) { param = getMefistoParameters(); } else if (method == Netgen) { param = getNetgenParameters(); } return param; } QString Tessellation::getStandardParameters(App::DocumentObject* obj) const { double devFace = ui->spinSurfaceDeviation->value().getValue(); double devAngle = ui->spinAngularDeviation->value().getValue(); devAngle = Base::toRadians(devAngle); bool relative = ui->relativeDeviation->isChecked(); QString param; param = QStringLiteral( "Shape=__shape__, " "LinearDeflection=%1, " "AngularDeflection=%2, " "Relative=%3" ) .arg(devFace) .arg(devAngle) .arg(relative ? QStringLiteral("True") : QStringLiteral("False")); if (ui->meshShapeColors->isChecked()) { param += QStringLiteral(",Segments=True"); } auto svp = freecad_cast( Gui::Application::Instance->getViewProvider(obj) ); if (ui->groupsFaceColors->isChecked() && svp) { // TODO: currently, we can only retrieve part feature // color. The problem is that if the feature is linked, // there are potentially many places where the color can // get overridden. // // With topo naming feature merged, it will be possible to // infer more accurate colors from just the shape names, // with static function, // // PartGui::ViewProviderPartExt::getShapeColors(). // param += QStringLiteral(",GroupColors=Gui.getDocument('%1').getObject('%2').DiffuseColor") .arg( QString::fromLatin1(obj->getDocument()->getName()), QString::fromLatin1(obj->getNameInDocument()) ); } return param; } QString Tessellation::getMefistoParameters() const { double maxEdge = ui->spinMaximumEdgeLength->value().getValue(); if (!ui->spinMaximumEdgeLength->isEnabled()) { maxEdge = 0; } return QStringLiteral("Shape=__shape__,MaxLength=%1").arg(maxEdge); } QString Tessellation::getNetgenParameters() const { QString param; int fineness = ui->comboFineness->currentIndex(); double growthRate = ui->doubleGrading->value(); double nbSegPerEdge = ui->spinEdgeElements->value(); double nbSegPerRadius = ui->spinCurvatureElements->value(); bool secondOrder = ui->checkSecondOrder->isChecked(); bool optimize = ui->checkOptimizeSurface->isChecked(); bool allowquad = ui->checkQuadDominated->isChecked(); if (fineness <= int(VeryFine)) { param = QStringLiteral( "Shape=__shape__," "Fineness=%1,SecondOrder=%2,Optimize=%3,AllowQuad=%4" ) .arg(fineness) .arg(secondOrder ? 1 : 0) .arg(optimize ? 1 : 0) .arg(allowquad ? 1 : 0); } else { param = QStringLiteral( "Shape=__shape__," "GrowthRate=%1,SegPerEdge=%2,SegPerRadius=%3,SecondOrder=%4," "Optimize=%5,AllowQuad=%6" ) .arg(growthRate) .arg(nbSegPerEdge) .arg(nbSegPerRadius) .arg(secondOrder ? 1 : 0) .arg(optimize ? 1 : 0) .arg(allowquad ? 1 : 0); } return param; } // --------------------------------------- class Mesh2ShapeGmsh::Private { public: std::string label; std::list shapes; App::DocumentT doc; std::string cadFile; std::string stlFile; std::string geoFile; }; Mesh2ShapeGmsh::Mesh2ShapeGmsh(QWidget* parent, Qt::WindowFlags fl) : GmshWidget(parent, fl) , d(new Private()) { d->cadFile = App::Application::getTempFileName() + "mesh.brep"; d->stlFile = App::Application::getTempFileName() + "mesh.stl"; d->geoFile = App::Application::getTempFileName() + "mesh.geo"; } Mesh2ShapeGmsh::~Mesh2ShapeGmsh() = default; void Mesh2ShapeGmsh::process(App::Document* doc, const std::list& objs) { d->doc = doc; d->shapes = objs; doc->openTransaction("Meshing"); accept(); } bool Mesh2ShapeGmsh::writeProject(QString& inpFile, QString& outFile) { if (!d->shapes.empty()) { App::SubObjectT sub = d->shapes.front(); d->shapes.pop_front(); App::DocumentObject* part = sub.getObject(); if (part) { Part::TopoShape shape = Part::Feature::getTopoShape( part, Part::ShapeOption::ResolveLink | Part::ShapeOption::Transform, sub.getSubName().c_str() ); shape.exportBrep(d->cadFile.c_str()); d->label = part->Label.getStrValue() + " (Meshed)"; // Parameters int algorithm = meshingAlgorithm(); double maxSize = getMaxSize(); if (maxSize == 0.0) { maxSize = 1.0e22; } double minSize = getMinSize(); // Gmsh geo file Base::FileInfo geo(d->geoFile); Base::ofstream geoOut(geo, std::ios::out); geoOut << "// geo file for meshing with Gmsh meshing software created by FreeCAD\n" << "// open brep geometry\n" << "Merge \"" << d->cadFile << "\";\n\n" << "// Characteristic Length\n" << "// no boundary layer settings for this mesh\n" << "// min, max Characteristic Length\n" << "Mesh.CharacteristicLengthMax = " << maxSize << ";\n" << "Mesh.CharacteristicLengthMin = " << minSize << ";\n\n" << "// optimize the mesh\n" << "Mesh.Optimize = 1;\n" << "Mesh.OptimizeNetgen = 0;\n" << "// High-order meshes optimization (0=none, 1=optimization, " "2=elastic+optimization, 3=elastic, 4=fast curving)\n" << "Mesh.HighOrderOptimize = 0;\n\n" << "// mesh order\n" << "Mesh.ElementOrder = 2;\n" << "// Second order nodes are created by linear interpolation instead by " "curvilinear\n" << "Mesh.SecondOrderLinear = 1;\n\n" << "// mesh algorithm, only a few algorithms are usable with 3D boundary layer " "generation\n" << "// 2D mesh algorithm (1=MeshAdapt, 2=Automatic, 5=Delaunay, 6=Frontal, " "7=BAMG, 8=DelQuad, 9=Packing of Parallelograms, 11=Quasi-structured Quad)\n" << "Mesh.Algorithm = " << algorithm << ";\n" << "// 3D mesh algorithm (1=Delaunay, 2=New Delaunay, 4=Frontal, 7=MMG3D, " "9=R-tree, 10=HTX)\n" << "Mesh.Algorithm3D = 1;\n\n" << "// meshing\n" << "// set geometrical tolerance (also used for merging nodes)\n" << "Geometry.Tolerance = 1e-06;\n" << "Mesh 2;\n" << "Coherence Mesh; // Remove duplicate vertices\n"; geoOut.close(); inpFile = QString::fromUtf8(d->geoFile.c_str()); outFile = QString::fromUtf8(d->stlFile.c_str()); return true; } } else { App::Document* doc = d->doc.getDocument(); if (doc) { doc->commitTransaction(); } Q_EMIT processed(); } return false; } bool Mesh2ShapeGmsh::loadOutput() { App::Document* doc = d->doc.getDocument(); if (!doc) { return false; } // Now read-in the 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(); auto fea = doc->addObject("Mesh"); fea->Label.setValue(d->label); fea->Mesh.setValue(kernel.getKernel()); stl.deleteFile(); geo.deleteFile(); // process next object accept(); return true; } // --------------------------------------- TaskTessellation::TaskTessellation() { widget = new Tessellation(); addTaskBox(widget); } void TaskTessellation::open() {} void TaskTessellation::clicked(int id) { Q_UNUSED(id) } bool TaskTessellation::accept() { return widget->accept(); } bool TaskTessellation::reject() { return true; } #include "moc_Tessellation.cpp"