mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-26 18:31:11 +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
@@ -1625,3 +1625,86 @@ TEST_CASE("surface_deviation: identical solids ~0, shifted solid ~shift", "[Devi
|
||||
REQUIRE_THAT(d1.max_mm, Catch::Matchers::WithinAbs(2.0, 0.05));
|
||||
REQUIRE(d1.mean_mm > 0.0);
|
||||
}
|
||||
|
||||
TEST_CASE("mesh_to_brep: watertight cube -> solid, coplanar merge gives 6 faces", "[design][mesh2brep]")
|
||||
{
|
||||
const indexed_triangle_set cube = its_make_cube(10.0, 20.0, 30.0);
|
||||
REQUIRE(cube.indices.size() == 12);
|
||||
|
||||
SECTION("faceted: one B-rep face per triangle, exact volume") {
|
||||
GeometryEngine::MeshBrepStats st;
|
||||
const TopoDS_Shape shape = GeometryEngine::mesh_to_brep(cube, 0.01, /*no merge*/0.0, st);
|
||||
REQUIRE_FALSE(shape.IsNull());
|
||||
CHECK(st.kept_tris == 12);
|
||||
CHECK(st.faces_built == 12);
|
||||
CHECK(st.faces_final == 12);
|
||||
// Shared topology by construction: a cube has 8 vertices and 18 edges once the two
|
||||
// triangles of each face share their diagonal. Every edge used exactly twice.
|
||||
CHECK(st.unique_edges == 18);
|
||||
CHECK(st.boundary_edges == 0);
|
||||
CHECK(st.nonmanifold_edges == 0);
|
||||
CHECK(st.watertight);
|
||||
REQUIRE(st.is_solid);
|
||||
REQUIRE_THAT(st.volume, Catch::Matchers::WithinRel(10.0 * 20.0 * 30.0, 1e-9));
|
||||
}
|
||||
|
||||
SECTION("merge coplanar: 12 triangles collapse to the cube's 6 real faces") {
|
||||
GeometryEngine::MeshBrepStats st;
|
||||
const TopoDS_Shape shape = GeometryEngine::mesh_to_brep(cube, 0.01, 5.0, st);
|
||||
REQUIRE_FALSE(shape.IsNull());
|
||||
REQUIRE(st.is_solid);
|
||||
// This is the whole point of merging: the imported body must expose pickable CAD faces,
|
||||
// not one face per triangle, or the fillet/extrude tools have nothing meaningful to grab.
|
||||
REQUIRE(st.faces_final == 6);
|
||||
REQUIRE_THAT(st.volume, Catch::Matchers::WithinRel(10.0 * 20.0 * 30.0, 1e-9));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("mesh_to_brep: an open mesh is reported as a shell, never a fake solid", "[design][mesh2brep]")
|
||||
{
|
||||
indexed_triangle_set open_cube = its_make_cube(10.0, 10.0, 10.0);
|
||||
open_cube.indices.pop_back(); // punch a hole: drop one triangle
|
||||
open_cube.indices.pop_back(); // (and its coplanar partner -> a whole face missing)
|
||||
|
||||
GeometryEngine::MeshBrepStats st;
|
||||
const TopoDS_Shape shape = GeometryEngine::mesh_to_brep(open_cube, 0.01, 5.0, st);
|
||||
REQUIRE_FALSE(shape.IsNull());
|
||||
CHECK(st.kept_tris == 10);
|
||||
CHECK(st.boundary_edges > 0); // the hole's rim
|
||||
CHECK_FALSE(st.watertight);
|
||||
REQUIRE_FALSE(st.is_solid); // must NOT be dressed up as a solid
|
||||
CHECK(st.volume == 0.0);
|
||||
}
|
||||
|
||||
TEST_CASE("mesh_to_brep: degenerate triangles are rejected on a scale-independent test", "[design][mesh2brep]")
|
||||
{
|
||||
// A thin but perfectly legitimate CAD sliver. Every edge (1.0, ~0.5, ~0.5) is far above the
|
||||
// 0.01 dedup tolerance, so no vertex collapses — but its area (5e-5) is BELOW tolerance^2
|
||||
// (1e-4). A rule of "reject when area < tolerance^2" would therefore throw it away, which is
|
||||
// precisely the bug that turned a watertight 62k-triangle input into a falsely-open shell.
|
||||
// The scale-independent test (area < 1e-9 * longest_edge^2 = 1e-9) keeps it, as it must.
|
||||
indexed_triangle_set sliver;
|
||||
sliver.vertices = { {0.f, 0.f, 0.f}, {1.f, 0.f, 0.f}, {0.5f, 0.0001f, 0.f} };
|
||||
sliver.indices = { {0, 1, 2} };
|
||||
GeometryEngine::MeshBrepStats st;
|
||||
GeometryEngine::mesh_to_brep(sliver, 0.01, 0.0, st);
|
||||
CHECK(st.degenerate_sliver == 0);
|
||||
CHECK(st.degenerate_collapsed == 0);
|
||||
CHECK(st.kept_tris == 1);
|
||||
|
||||
// A truly collinear triangle has no area at any scale -> rejected as a sliver.
|
||||
indexed_triangle_set collinear;
|
||||
collinear.vertices = { {0.f, 0.f, 0.f}, {10.f, 0.f, 0.f}, {20.f, 0.f, 0.f} };
|
||||
collinear.indices = { {0, 1, 2} };
|
||||
GeometryEngine::MeshBrepStats st2;
|
||||
CHECK_THROWS(GeometryEngine::mesh_to_brep(collinear, 0.01, 0.0, st2)); // nothing left to build
|
||||
CHECK(st2.degenerate_sliver == 1);
|
||||
|
||||
// A triangle entirely inside one tolerance cell is sub-resolution noise -> collapsed.
|
||||
indexed_triangle_set tiny;
|
||||
tiny.vertices = { {0.f, 0.f, 0.f}, {0.001f, 0.f, 0.f}, {0.f, 0.001f, 0.f} };
|
||||
tiny.indices = { {0, 1, 2} };
|
||||
GeometryEngine::MeshBrepStats st3;
|
||||
CHECK_THROWS(GeometryEngine::mesh_to_brep(tiny, 0.1, 0.0, st3));
|
||||
CHECK(st3.degenerate_collapsed == 1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user