mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-22 16:32:40 +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
@@ -30,6 +30,16 @@
|
||||
#include <Standard_Failure.hxx>
|
||||
#include <BRepExtrema_DistShapeShape.hxx>
|
||||
#include <BRepBuilderAPI_MakeVertex.hxx>
|
||||
#include <BRepBuilderAPI_MakeEdge.hxx>
|
||||
#include <BRepBuilderAPI_MakeWire.hxx>
|
||||
#include <BRepBuilderAPI_MakeFace.hxx>
|
||||
#include <BRepBuilderAPI_MakeSolid.hxx>
|
||||
#include <BRep_Builder.hxx>
|
||||
#include <TopoDS_Shell.hxx>
|
||||
#include <TopoDS_Vertex.hxx>
|
||||
#include <ShapeUpgrade_UnifySameDomain.hxx>
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <cmath>
|
||||
|
||||
namespace Slic3r {
|
||||
@@ -60,6 +70,158 @@ std::vector<TopoDS_Shape> GeometryEngine::read_step_solids(const std::string& pa
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---- Mesh -> B-rep (faceted, shared topology by construction) ----
|
||||
//
|
||||
// Port of mesh2step's brep_build.py. Two properties are load-bearing and easy to lose:
|
||||
//
|
||||
// 1. The edge cache is keyed on the UNORDERED vertex-index pair, and a triangle that walks
|
||||
// the edge backwards (i > j) gets edge.Reversed(). Consistently-wound meshes (STL/OBJ/3MF
|
||||
// all are) walk every shared edge in opposite directions from its two adjacent triangles,
|
||||
// so this reversal is exactly what leaves the faces coherently outward-oriented.
|
||||
// 2. Degeneracy is split in two, deliberately. A triangle is dropped as sub-resolution noise
|
||||
// only if its longest edge is below `tolerance` (an absolute floor), while sliver rejection
|
||||
// is scale-INDEPENDENT (area < 1e-9 * longest_edge^2). Folding the two together under one
|
||||
// `area < tolerance^2` test rejects legitimate thin CAD slivers whenever tolerance is coarse
|
||||
// relative to them, turning a watertight input into a falsely-open shell — a real regression
|
||||
// mesh2step hit on a 62k-triangle mechanical part.
|
||||
TopoDS_Shape GeometryEngine::mesh_to_brep(const indexed_triangle_set& its,
|
||||
double tolerance,
|
||||
double merge_angle_deg,
|
||||
MeshBrepStats& stats)
|
||||
{
|
||||
stats = MeshBrepStats{};
|
||||
stats.input_tris = int(its.indices.size());
|
||||
if (tolerance <= 0.0)
|
||||
throw std::runtime_error("mesh_to_brep: tolerance must be > 0");
|
||||
if (its.indices.empty())
|
||||
throw std::runtime_error("mesh_to_brep: mesh has no triangles");
|
||||
|
||||
// 1. Tolerance-quantized vertex dedup. A merged vertex keeps the exact coordinates of the
|
||||
// first input occurrence — vertices are grouped by a cell, never snapped onto its grid.
|
||||
std::map<std::array<long long, 3>, int> cell_to_new;
|
||||
std::vector<int> old_to_new(its.vertices.size(), -1);
|
||||
std::vector<Vec3d> verts;
|
||||
verts.reserve(its.vertices.size());
|
||||
for (size_t i = 0; i < its.vertices.size(); ++i) {
|
||||
const Vec3d p = its.vertices[i].cast<double>();
|
||||
const std::array<long long, 3> cell{ (long long) std::llround(p.x() / tolerance),
|
||||
(long long) std::llround(p.y() / tolerance),
|
||||
(long long) std::llround(p.z() / tolerance) };
|
||||
auto ins = cell_to_new.emplace(cell, int(verts.size()));
|
||||
if (ins.second)
|
||||
verts.push_back(p);
|
||||
old_to_new[i] = ins.first->second;
|
||||
}
|
||||
|
||||
// 2. Reject degenerate triangles (see the two-part rule in the comment above).
|
||||
std::vector<Vec3i32> tris;
|
||||
tris.reserve(its.indices.size());
|
||||
for (const Vec3i32& t : its.indices) {
|
||||
const int a = old_to_new[t(0)], b = old_to_new[t(1)], c = old_to_new[t(2)];
|
||||
if (a == b || b == c || a == c) { ++stats.degenerate_collapsed; continue; }
|
||||
const Vec3d& pa = verts[a]; const Vec3d& pb = verts[b]; const Vec3d& pc = verts[c];
|
||||
const double e0 = (pb - pa).norm(), e1 = (pc - pb).norm(), e2 = (pa - pc).norm();
|
||||
const double longest = std::max(e0, std::max(e1, e2));
|
||||
if (longest < tolerance) { ++stats.degenerate_collapsed; continue; }
|
||||
const double area = 0.5 * (pb - pa).cross(pc - pa).norm();
|
||||
if (area < 1e-9 * longest * longest) { ++stats.degenerate_sliver; continue; }
|
||||
tris.emplace_back(a, b, c);
|
||||
}
|
||||
stats.kept_tris = int(tris.size());
|
||||
if (tris.empty())
|
||||
throw std::runtime_error("mesh_to_brep: every triangle was rejected as degenerate "
|
||||
"(try a smaller tolerance)");
|
||||
|
||||
// 3. One face per triangle, sharing vertices and edges through the caches.
|
||||
std::vector<TopoDS_Vertex> vertex_cache(verts.size());
|
||||
std::vector<bool> vertex_made(verts.size(), false);
|
||||
auto get_vertex = [&](int i) -> const TopoDS_Vertex& {
|
||||
if (!vertex_made[i]) {
|
||||
const Vec3d& p = verts[i];
|
||||
vertex_cache[i] = BRepBuilderAPI_MakeVertex(gp_Pnt(p.x(), p.y(), p.z())).Vertex();
|
||||
vertex_made[i] = true;
|
||||
}
|
||||
return vertex_cache[i];
|
||||
};
|
||||
|
||||
std::map<std::pair<int, int>, TopoDS_Edge> edge_cache;
|
||||
std::map<std::pair<int, int>, int> edge_usage;
|
||||
auto get_edge = [&](int i, int j) -> TopoDS_Edge {
|
||||
const std::pair<int, int> key = (i < j) ? std::make_pair(i, j) : std::make_pair(j, i);
|
||||
++edge_usage[key];
|
||||
auto it = edge_cache.find(key);
|
||||
if (it == edge_cache.end())
|
||||
it = edge_cache.emplace(key,
|
||||
BRepBuilderAPI_MakeEdge(get_vertex(key.first), get_vertex(key.second)).Edge()).first;
|
||||
return (i > j) ? TopoDS::Edge(it->second.Reversed()) : it->second;
|
||||
};
|
||||
|
||||
BRep_Builder builder;
|
||||
TopoDS_Shell shell;
|
||||
builder.MakeShell(shell);
|
||||
|
||||
for (const Vec3i32& t : tris) {
|
||||
try {
|
||||
BRepBuilderAPI_MakeWire mk_wire(get_edge(t(0), t(1)), get_edge(t(1), t(2)), get_edge(t(2), t(0)));
|
||||
if (!mk_wire.IsDone()) { ++stats.faces_failed; continue; }
|
||||
BRepBuilderAPI_MakeFace mk_face(mk_wire.Wire());
|
||||
if (!mk_face.IsDone()) { ++stats.faces_failed; continue; }
|
||||
builder.Add(shell, mk_face.Face());
|
||||
++stats.faces_built;
|
||||
} catch (const Standard_Failure&) {
|
||||
++stats.faces_failed;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Watertightness falls straight out of the usage counts the cache already gathered.
|
||||
for (const auto& kv : edge_usage) {
|
||||
if (kv.second == 1) ++stats.boundary_edges;
|
||||
else if (kv.second >= 3) ++stats.nonmanifold_edges;
|
||||
}
|
||||
stats.unique_edges = int(edge_usage.size());
|
||||
stats.watertight = stats.boundary_edges == 0 && stats.nonmanifold_edges == 0 && stats.unique_edges > 0;
|
||||
|
||||
TopoDS_Shape shape = shell;
|
||||
if (stats.watertight && stats.faces_built > 0) {
|
||||
BRepBuilderAPI_MakeSolid mk_solid(shell);
|
||||
if (mk_solid.IsDone()) {
|
||||
TopoDS_Solid solid = mk_solid.Solid();
|
||||
GProp_GProps props;
|
||||
BRepGProp::VolumeProperties(solid, props);
|
||||
double vol = props.Mass();
|
||||
if (vol < 0.0) { // inward-wound input
|
||||
solid = TopoDS::Solid(solid.Reversed());
|
||||
vol = -vol;
|
||||
}
|
||||
if (vol > 0.0) {
|
||||
shape = solid;
|
||||
stats.is_solid = true;
|
||||
stats.volume = vol;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Optional coplanar merge. Faceted output is one planar face per triangle — exact, but
|
||||
// you cannot meaningfully fillet or extrude a face that IS a single triangle. Merging
|
||||
// coplanar neighbours is what turns the import into something the face/edge tools can
|
||||
// actually operate on (a 12-triangle cube collapses to its 6 real faces).
|
||||
if (merge_angle_deg > 0.0) {
|
||||
try {
|
||||
ShapeUpgrade_UnifySameDomain unifier(shape, true, true, true);
|
||||
unifier.SetAngularTolerance(merge_angle_deg * M_PI / 180.0);
|
||||
unifier.SetLinearTolerance(tolerance);
|
||||
unifier.Build();
|
||||
const TopoDS_Shape merged = unifier.Shape();
|
||||
if (!merged.IsNull())
|
||||
shape = merged;
|
||||
} catch (const Standard_Failure&) {
|
||||
// Merging is an optimisation, not a correctness step: keep the exact faceted shape.
|
||||
}
|
||||
}
|
||||
stats.faces_final = face_count(shape);
|
||||
return shape;
|
||||
}
|
||||
|
||||
// ---- Primitive creation ----
|
||||
|
||||
TopoDS_Solid GeometryEngine::make_primitive(const PrimitiveParams& params)
|
||||
|
||||
Reference in New Issue
Block a user