Files
FreeCADWasmDev/freecad-source/src/WebAssembly/FreeCADCoreProbe/MeshPartFlattenCommands.cpp
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

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;
}
}