mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 10:51:22 +00:00
Import a triangle mesh as an editable B-rep body (mesh2step port)
Opening an STL/OBJ in the Design pane now rebuilds it into a real OCCT B-rep
solid that the face/edge feature tools can operate on, instead of a print mesh.
GeometryEngine::mesh_to_brep is a native C++ port of mesh2step
(github.com/tommasobbianchi/mesh2step): vertices and edges are shared across
triangles at construction time (vertex cache by deduped index, edge cache by
unordered index pair), so no BRepBuilderAPI_Sewing pass is needed to rebuild the
topology afterwards, and watertightness falls out of the edge-usage counts for
free. An open mesh is returned as a shell and reported as such — never dressed up
as a fake solid.
It runs in-process on the OCCT kernel libslic3r already links, so no STEP file is
written or re-read. That is not an optimisation but the whole point: a faceted
STEP of a 62k-triangle mesh is ~149 MB and OCCT's STEPControl_Reader takes >300 s
to parse it back, so routing this through a file would hang the GUI.
Coplanar neighbours are merged (ShapeUpgrade_UnifySameDomain, 5° default) so the
body arrives with pickable CAD faces rather than one face per triangle — on the
20,656-triangle test part that is 20,614 faces down to 4,784. Without it the
import is technically a solid but nothing you can meaningfully fillet or extrude.
- Design pane: "Import mesh" button + Shift+M; warns above 50k triangles.
- MCP: import_mesh {path, tolerance, merge_angle_deg}, returning the full
conversion stats so a caller can tell an honest solid from an open shell.
- Catch2: cube round-trip (exact volume, 12 faceted faces, 6 after merge), open
mesh stays a shell, and the scale-independent sliver rule that a naive
area < tolerance^2 test would get wrong.
Verified end-to-end on the real 20,656-triangle ir3v2 hotend STL: reproduces
mesh2step's Python run exactly (20,614 kept, 42 degenerate, 0 boundary edges,
2 non-manifold edges, not watertight) and the resulting body's bbox matches the
one FreeCAD reports for the same part.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BVzKmX6Y1aEteit1HTXG4Q
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c618965a4c
commit
343a0439f1
@@ -29,6 +29,9 @@
|
||||
#include "libslic3r/SketchEngine.hpp"
|
||||
#include "libslic3r/GeometryEngine.hpp"
|
||||
#include "libslic3r/TriangleMesh.hpp"
|
||||
#include "libslic3r/Format/OBJ.hpp"
|
||||
#include <boost/algorithm/string/case_conv.hpp>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
|
||||
#include <gp_Pln.hxx>
|
||||
@@ -194,6 +197,12 @@ json describe_tools()
|
||||
{"params", json::array({
|
||||
json{{"name", "path"}, {"type", "string"}},
|
||||
})}},
|
||||
json{{"name", "import_mesh"}, {"summary", "Convert a triangle mesh (STL/OBJ) into an editable B-rep body. Reports whether the result is a real solid or an open shell, and why."},
|
||||
{"params", json::array({
|
||||
json{{"name", "path"}, {"type", "string"}},
|
||||
json{{"name", "tolerance"}, {"type", "number"}, {"default", 0.01}},
|
||||
json{{"name", "merge_angle_deg"}, {"type", "number"}, {"default", 5.0}},
|
||||
})}},
|
||||
json{{"name", "validate_against"}, {"summary", "Volume + bbox/centroid + surface deviation (max/mean/rms mm) of a body vs a reference {step:path|body:id} (the RE acceptance metric)."},
|
||||
{"params", json::array({
|
||||
json{{"name", "body"}, {"type", "integer"}, {"default", 0}},
|
||||
@@ -449,6 +458,59 @@ json import_step(DesignPanel* panel, const json& params)
|
||||
{"bodies", int(doc.bodies.size())}, {"error", doc.error}};
|
||||
}
|
||||
|
||||
// --- Import a triangle mesh as a B-rep body (GeometryEngine::mesh_to_brep) ---
|
||||
// Same destination as import_step: a CadFeatureType::Import body every feature tool can edit.
|
||||
// The full conversion stats come back so a caller can tell an honest solid from an open shell
|
||||
// instead of discovering it later when a boolean silently fails.
|
||||
json import_mesh(DesignPanel* panel, const json& params)
|
||||
{
|
||||
if (!params.contains("path")) throw std::runtime_error("import_mesh needs 'path'");
|
||||
const std::string path = params["path"].get<std::string>();
|
||||
const double tolerance = params.value("tolerance", 0.01);
|
||||
const double merge_angle_deg = params.value("merge_angle_deg", 5.0);
|
||||
|
||||
TriangleMesh mesh;
|
||||
const std::string ext = boost::algorithm::to_lower_copy(
|
||||
boost::filesystem::path(path).extension().string());
|
||||
if (ext == ".stl") {
|
||||
if (!mesh.ReadSTLFile(path.c_str())) throw std::runtime_error("could not read STL: " + path);
|
||||
} else if (ext == ".obj") {
|
||||
ObjInfo obj_info; std::string obj_err;
|
||||
if (!load_obj(path.c_str(), &mesh, obj_info, obj_err))
|
||||
throw std::runtime_error("could not read OBJ: " + obj_err);
|
||||
} else {
|
||||
throw std::runtime_error("unsupported mesh format (want .stl or .obj): " + ext);
|
||||
}
|
||||
|
||||
GeometryEngine::MeshBrepStats st;
|
||||
const TopoDS_Shape shape = GeometryEngine::mesh_to_brep(mesh.its, tolerance, merge_angle_deg, st);
|
||||
if (shape.IsNull()) throw std::runtime_error("mesh conversion produced no geometry");
|
||||
|
||||
CadDocument& doc = panel->mcp_doc();
|
||||
doc.checkpoint();
|
||||
const int first = int(doc.features.size());
|
||||
CadFeature f;
|
||||
f.type = CadFeatureType::Import;
|
||||
f.name = "Mesh" + std::to_string(first + 1);
|
||||
f.imported_solid = shape;
|
||||
f.mode = BooleanMode::New;
|
||||
doc.features.push_back(f);
|
||||
|
||||
const bool ok = doc.recompute();
|
||||
if (!ok) doc.undo();
|
||||
panel->mcp_after_change();
|
||||
return json{{"ok", ok}, {"first_feature", first}, {"bodies", int(doc.bodies.size())},
|
||||
{"input_triangles", st.input_tris}, {"kept_triangles", st.kept_tris},
|
||||
{"degenerate_collapsed", st.degenerate_collapsed},
|
||||
{"degenerate_sliver", st.degenerate_sliver},
|
||||
{"faces_built", st.faces_built}, {"faces_failed", st.faces_failed},
|
||||
{"faces_final", st.faces_final}, {"unique_edges", st.unique_edges},
|
||||
{"boundary_edges", st.boundary_edges},
|
||||
{"nonmanifold_edges", st.nonmanifold_edges},
|
||||
{"watertight", st.watertight}, {"is_solid", st.is_solid},
|
||||
{"volume", st.volume}, {"error", doc.error}};
|
||||
}
|
||||
|
||||
// --- Validate: volume + bbox deviation of a body vs a reference (the "scarto %") --
|
||||
// ponytail: volume delta + bbox/centroid offset (the RE skill's actual acceptance metric).
|
||||
// Surface-deviation heat-map is the upgrade path (per-vertex BRepExtrema), add when needed.
|
||||
@@ -719,6 +781,7 @@ std::string handle_on_main(const std::string& method, const json& params, const
|
||||
if (method == "measure") return rpc_result(id, measure(panel, params));
|
||||
if (method == "slice_body") return rpc_result(id, slice_body(panel, params));
|
||||
if (method == "import_step") return rpc_result(id, import_step(panel, params));
|
||||
if (method == "import_mesh") return rpc_result(id, import_mesh(panel, params));
|
||||
if (method == "validate_against") return rpc_result(id, validate_against(panel, params));
|
||||
if (method == "extrude") return rpc_result(id, action_extrude(panel, params));
|
||||
if (method == "revolve") return rpc_result(id, action_revolve(panel, params));
|
||||
|
||||
Reference in New Issue
Block a user