// SPDX-License-Identifier: LGPL-2.1-or-later #include "MeshPartFlattenCommands.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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, int> edges; std::vector> 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 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 visited; std::vector 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(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 shapes; QJsonArray boundaries, flatPoints, triangles; if (faceMode) { auto* feature = dynamic_cast(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(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 points; ColMat facets; if (selected) { auto* mesh = dynamic_cast(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(value); } } } FaceUnwrapper unwrapper(points, facets); checkMesh(unwrapper); unwrapper.findFlatNodes(static_cast(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(facets(i, 0)), static_cast(facets(i, 1)), static_cast(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(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; } }