242 lines
14 KiB
C++
242 lines
14 KiB
C++
// SPDX-License-Identifier: LGPL-2.1-or-later
|
|
#include "MeshPartFlattenCommands.h"
|
|
#include <App/Application.h>
|
|
#include <App/Document.h>
|
|
#include <Mod/Mesh/App/MeshFeature.h>
|
|
#include <Mod/Part/App/PartFeature.h>
|
|
#include <Mod/MeshPart/App/MeshFlattening.h>
|
|
#include <BRepBuilderAPI_MakeFace.hxx>
|
|
#include <BRepBuilderAPI_MakePolygon.hxx>
|
|
#include <BRepBuilderAPI_NurbsConvert.hxx>
|
|
#include <BRepMesh_IncrementalMesh.hxx>
|
|
#include <BRepTools.hxx>
|
|
#include <BRep_Builder.hxx>
|
|
#include <BRep_Tool.hxx>
|
|
#include <Geom_BSplineSurface.hxx>
|
|
#include <GeomConvert_ApproxSurface.hxx>
|
|
#include <Precision.hxx>
|
|
#include <TopoDS.hxx>
|
|
#include <TopoDS_Wire.hxx>
|
|
#include <Poly_Triangulation.hxx>
|
|
#include <TopExp.hxx>
|
|
#include <TopTools_IndexedMapOfShape.hxx>
|
|
#include <QJsonArray>
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
#include <map>
|
|
#include <set>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
|
|
namespace {
|
|
void checkMesh(const FaceUnwrapper& mesh)
|
|
{
|
|
if (mesh.xyz_nodes.rows() < 3 || mesh.tris.rows() < 1 || !mesh.xyz_nodes.allFinite())
|
|
throw std::runtime_error("Unwrap requires finite vertices and triangles");
|
|
std::map<std::pair<long, long>, int> edges;
|
|
std::vector<std::set<long>> neighbours(mesh.xyz_nodes.rows());
|
|
for (Eigen::Index row = 0; row < mesh.tris.rows(); ++row) {
|
|
for (int col = 0; col < 3; ++col) {
|
|
long a = mesh.tris(row, col), b = mesh.tris(row, (col + 1) % 3);
|
|
if (a < 0 || b < 0 || a >= mesh.xyz_nodes.rows() || b >= mesh.xyz_nodes.rows() || a == b)
|
|
throw std::runtime_error("Triangle indices must reference three distinct vertices");
|
|
if (++edges[std::minmax(a, b)] > 2) throw std::runtime_error("Mesh has non-manifold edges");
|
|
neighbours[a].insert(b); neighbours[b].insert(a);
|
|
}
|
|
Eigen::Vector3d a = mesh.xyz_nodes.row(mesh.tris(row, 1)) - mesh.xyz_nodes.row(mesh.tris(row, 0));
|
|
Eigen::Vector3d b = mesh.xyz_nodes.row(mesh.tris(row, 2)) - mesh.xyz_nodes.row(mesh.tris(row, 0));
|
|
const double area = a.cross(b).squaredNorm();
|
|
if (!std::isfinite(area) || area <= 0.0) throw std::runtime_error("Mesh contains a degenerate or unbounded triangle");
|
|
}
|
|
std::map<long, int> boundary;
|
|
for (const auto& [edge, count] : edges) if (count == 1) { ++boundary[edge.first]; ++boundary[edge.second]; }
|
|
if (boundary.empty()) throw std::runtime_error("A closed mesh needs an open seam before unwrapping");
|
|
for (const auto& [vertex, count] : boundary)
|
|
if (count != 2) throw std::runtime_error("Mesh boundary is not a closed manifold loop");
|
|
std::set<long> visited;
|
|
std::vector<long> pending{0};
|
|
while (!pending.empty()) {
|
|
const long vertex = pending.back(); pending.pop_back();
|
|
if (!visited.insert(vertex).second) continue;
|
|
for (long next : neighbours[vertex]) if (!visited.count(next)) pending.push_back(next);
|
|
}
|
|
if (visited.size() != static_cast<size_t>(mesh.xyz_nodes.rows()))
|
|
throw std::runtime_error("Unwrap each connected mesh component separately and remove unused vertices");
|
|
}
|
|
|
|
TopoDS_Shape readBrep(const QString& value)
|
|
{
|
|
if (value.isEmpty()) throw std::runtime_error("A native face BREP is required");
|
|
std::istringstream stream(value.toStdString());
|
|
BRep_Builder builder;
|
|
TopoDS_Shape shape;
|
|
BRepTools::Read(shape, stream, builder);
|
|
if (shape.IsNull()) throw std::runtime_error("Invalid face BREP");
|
|
return shape;
|
|
}
|
|
}
|
|
|
|
QJsonObject executeMeshPartFlatten(const QString& command, const QJsonObject& args)
|
|
{
|
|
App::Document* document = nullptr;
|
|
bool transaction = false;
|
|
try {
|
|
const bool faceMode = command == QStringLiteral("MeshPart_CreateFlatFace");
|
|
if (args.value("activeTask").toBool()) throw std::runtime_error("Finish the active task before unwrapping");
|
|
if (args.contains("selection") && !args.value("selection").isArray()) throw std::runtime_error("selection must be an array");
|
|
const auto selection = args.value("selection").toArray();
|
|
App::DocumentObject* selected = nullptr;
|
|
QString subelement;
|
|
if (!selection.isEmpty()) {
|
|
if (selection.size() != 1 || !selection.first().isObject()) throw std::runtime_error("Select one source object");
|
|
const auto entry = selection.first().toObject();
|
|
const auto id = entry.value("documentId").toString();
|
|
if (id.isEmpty() || (args.contains("documentId") && args.value("documentId").toString() != id))
|
|
throw std::runtime_error("Selection must belong to the requested document");
|
|
document = App::GetApplication().getDocument(id.toUtf8().constData());
|
|
selected = document ? document->getObject(entry.value("object").toString().toUtf8().constData()) : nullptr;
|
|
if (!selected) throw std::runtime_error("Selected native object does not exist in this runtime");
|
|
const auto subs = entry.value("subelements").toArray();
|
|
if (subs.size() > 1) throw std::runtime_error("Select one complete face");
|
|
subelement = subs.isEmpty() ? entry.value("subelement").toString() : subs.first().toString();
|
|
}
|
|
const double stepsValue = args.contains("steps") ? args.value("steps").toDouble(-1) : 5;
|
|
const double relaxation = args.contains("relaxation") ? args.value("relaxation").toDouble(-1) : faceMode ? 0.99 : 0.95;
|
|
if (!std::isfinite(stepsValue) || std::floor(stepsValue) != stepsValue || stepsValue < 1 || stepsValue > 100
|
|
|| !std::isfinite(relaxation) || relaxation <= 0 || relaxation >= 1)
|
|
throw std::runtime_error("steps must be an integer 1..100 and relaxation must be between 0 and 1");
|
|
std::vector<TopoDS_Shape> shapes;
|
|
QJsonArray boundaries, flatPoints, triangles;
|
|
if (faceMode) {
|
|
auto* feature = dynamic_cast<Part::Feature*>(selected);
|
|
if (selected && !feature) throw std::runtime_error("Selected object has no native Part shape");
|
|
TopoDS_Shape source = feature ? feature->Shape.getValue() : readBrep(args.value("brep").toString());
|
|
TopoDS_Face face;
|
|
if (subelement.isEmpty() && source.ShapeType() == TopAbs_FACE) face = TopoDS::Face(source);
|
|
else {
|
|
bool valid = false;
|
|
const int index = subelement.startsWith("Face") ? subelement.mid(4).toInt(&valid) : 0;
|
|
TopTools_IndexedMapOfShape faces;
|
|
TopExp::MapShapes(source, TopAbs_FACE, faces);
|
|
if (!valid || index < 1 || index > faces.Extent()) throw std::runtime_error("Select a native Face reference");
|
|
face = TopoDS::Face(faces(index));
|
|
}
|
|
BRepBuilderAPI_NurbsConvert conversion(face, true);
|
|
auto nurbsFace = TopoDS::Face(conversion.Shape());
|
|
auto nurbs = Handle(Geom_BSplineSurface)::DownCast(BRep_Tool::Surface(nurbsFace));
|
|
if (nurbs.IsNull()) throw std::runtime_error("Face conversion produced no B-spline surface");
|
|
nurbs->SetUNotPeriodic(); nurbs->SetVNotPeriodic();
|
|
// Match MeshFlatteningCommand.py's toBSpline(1, C0, C0, 3, 3, 10).
|
|
GeomConvert_ApproxSurface approximation(nurbs, 1.0, GeomAbs_C0, GeomAbs_C0, 3, 3, 10, 0);
|
|
if (!approximation.IsDone() || !approximation.HasResult()) throw std::runtime_error("B-spline approximation failed");
|
|
auto surface = approximation.Surface();
|
|
BRepBuilderAPI_MakeFace makeFace(surface, Precision::Confusion());
|
|
if (!makeFace.IsDone()) throw std::runtime_error("Could not construct the source B-spline face");
|
|
face = makeFace.Face();
|
|
BRepMesh_IncrementalMesh mesher(face, 0.01, false, 0.5, false);
|
|
FaceUnwrapper unwrapper(face);
|
|
checkMesh(unwrapper);
|
|
unwrapper.findFlatNodes(static_cast<int>(stepsValue), relaxation);
|
|
auto poles = unwrapper.interpolateFlatFace(face);
|
|
if (!poles.allFinite() || poles.rows() != surface->NbUPoles() * surface->NbVPoles())
|
|
throw std::runtime_error("Native unwrap produced invalid poles");
|
|
Eigen::Index row = 0;
|
|
for (int u = 1; u <= surface->NbUPoles(); ++u)
|
|
for (int v = 1; v <= surface->NbVPoles(); ++v, ++row)
|
|
surface->SetPole(u, v, gp_Pnt(poles(row, 0), poles(row, 1), poles(row, 2)));
|
|
BRepBuilderAPI_MakeFace flattened(surface, Precision::Confusion());
|
|
if (!flattened.IsDone()) throw std::runtime_error("Could not construct the flattened face");
|
|
shapes.push_back(flattened.Face());
|
|
BRepMesh_IncrementalMesh displayMesh(shapes.back(), 0.01, false, 0.5, false);
|
|
TopLoc_Location location;
|
|
auto mesh = BRep_Tool::Triangulation(TopoDS::Face(shapes.back()), location);
|
|
if (mesh.IsNull()) throw std::runtime_error("Flattened face has no display triangulation");
|
|
for (int i = 1; i <= mesh->NbNodes(); ++i) {
|
|
auto point = mesh->Node(i).Transformed(location.Transformation());
|
|
flatPoints.append(QJsonArray{point.X(), point.Y(), point.Z()});
|
|
}
|
|
for (int i = 1; i <= mesh->NbTriangles(); ++i) {
|
|
int a, b, c; mesh->Triangle(i).Get(a, b, c);
|
|
triangles.append(QJsonArray{a - 1, b - 1, c - 1});
|
|
}
|
|
} else {
|
|
ColMat<double, 3> points;
|
|
ColMat<long, 3> facets;
|
|
if (selected) {
|
|
auto* mesh = dynamic_cast<Mesh::Feature*>(selected);
|
|
if (!mesh || !subelement.isEmpty()) throw std::runtime_error("Select a whole native mesh object");
|
|
const auto& kernel = mesh->Mesh.getValue().getKernel();
|
|
const auto& vertices = kernel.GetPoints();
|
|
const auto& faces = kernel.GetFacets();
|
|
points.resize(vertices.size(), 3); facets.resize(faces.size(), 3);
|
|
for (size_t i = 0; i < vertices.size(); ++i) points.row(i) << vertices[i].x, vertices[i].y, vertices[i].z;
|
|
for (size_t i = 0; i < faces.size(); ++i)
|
|
for (int j = 0; j < 3; ++j) facets(i, j) = faces[i]._aulPoints[j];
|
|
} else {
|
|
const auto vertices = args.value("points").toArray();
|
|
const auto faces = args.value("triangles").toArray();
|
|
points.resize(vertices.size(), 3); facets.resize(faces.size(), 3);
|
|
for (int i = 0; i < vertices.size(); ++i) {
|
|
auto point = vertices[i].toArray();
|
|
if (point.size() != 3) throw std::runtime_error("Each point requires three coordinates");
|
|
for (int j = 0; j < 3; ++j) {
|
|
if (!point[j].isDouble()) throw std::runtime_error("Point coordinates must be numbers");
|
|
points(i, j) = point[j].toDouble();
|
|
}
|
|
}
|
|
for (int i = 0; i < faces.size(); ++i) {
|
|
auto face = faces[i].toArray();
|
|
if (face.size() != 3) throw std::runtime_error("Each triangle requires three vertex indices");
|
|
for (int j = 0; j < 3; ++j) {
|
|
const double value = face[j].toDouble(-1);
|
|
if (!std::isfinite(value) || std::floor(value) != value || value < 0 || value >= vertices.size())
|
|
throw std::runtime_error("Triangle index is outside the supplied points");
|
|
facets(i, j) = static_cast<long>(value);
|
|
}
|
|
}
|
|
}
|
|
FaceUnwrapper unwrapper(points, facets);
|
|
checkMesh(unwrapper);
|
|
unwrapper.findFlatNodes(static_cast<int>(stepsValue), relaxation);
|
|
if (!unwrapper.ze_nodes.allFinite()) throw std::runtime_error("Native unwrap produced non-finite coordinates");
|
|
for (Eigen::Index i = 0; i < unwrapper.ze_nodes.rows(); ++i)
|
|
flatPoints.append(QJsonArray{unwrapper.ze_nodes(i, 0), unwrapper.ze_nodes(i, 1), 0.0});
|
|
for (Eigen::Index i = 0; i < facets.rows(); ++i)
|
|
triangles.append(QJsonArray{static_cast<int>(facets(i, 0)), static_cast<int>(facets(i, 1)), static_cast<int>(facets(i, 2))});
|
|
for (const auto& edge : unwrapper.getFlatBoundaryNodes()) {
|
|
BRepBuilderAPI_MakePolygon polygon;
|
|
QJsonArray boundary;
|
|
for (Eigen::Index i = 0; i < edge.rows(); ++i) {
|
|
gp_Pnt point(edge(i, 0), edge(i, 1), edge(i, 2));
|
|
polygon.Add(point); boundary.append(QJsonArray{point.X(), point.Y(), point.Z()});
|
|
}
|
|
if (!polygon.IsDone()) throw std::runtime_error("Could not construct the unwrapped boundary wire");
|
|
shapes.push_back(polygon.Wire()); boundaries.append(boundary);
|
|
}
|
|
if (shapes.empty()) throw std::runtime_error("Native unwrap produced no boundary wires");
|
|
}
|
|
QJsonArray breps, created;
|
|
for (const auto& shape : shapes) { std::ostringstream stream; BRepTools::Write(shape, stream); breps.append(QString::fromStdString(stream.str())); }
|
|
if (document) {
|
|
document->openTransaction("Unwrap MeshPart geometry"); transaction = true;
|
|
for (const auto& shape : shapes) {
|
|
auto* feature = dynamic_cast<Part::Feature*>(document->addObject("Part::Feature", faceMode ? "UnwrappedFace" : "UnwrappedWire"));
|
|
if (!feature) throw std::runtime_error("Could not create unwrapped document object");
|
|
feature->Shape.setValue(shape); created.append(QString::fromUtf8(feature->getNameInDocument()));
|
|
}
|
|
bool failed = false; document->recompute({}, false, &failed);
|
|
if (failed) throw std::runtime_error("Unwrapped document recompute failed");
|
|
document->commitTransaction(); transaction = false;
|
|
}
|
|
QJsonObject result{{"nativeComputed", true}, {"desktopCommandImplemented", true},
|
|
{"nativeHandler", "FaceUnwrapper"}, {"breps", breps}, {"boundaries", boundaries},
|
|
{"flatPoints", flatPoints}, {"triangles", triangles}, {"createdObjects", created},
|
|
{"documentChanged", document != nullptr}, {"sourceEntry", "src/Mod/MeshPart/App/MeshFlattening.cpp"}};
|
|
if (document) result.insert("document", QString::fromUtf8(document->getName()));
|
|
return result;
|
|
} catch (...) {
|
|
if (transaction && document) document->abortTransaction();
|
|
throw;
|
|
}
|
|
}
|