Rewrite Fix Model on CGAL 6.2, fix Split to parts, remove Win10 SDK leftovers (#16366)

* Add CGAL-based mesh repair, drop Netfabb remnants

Add repair_mesh() in MeshRepair.cpp, a CGAL-based repair that replaces MeshBoolean::cgal::repair(). It welds cracks, cancels coincident triangles, fixes orientation, fills holes (including planar annular regions), removes local self-intersections, and unites crossing shells with libigl's exact boolean while following the nonzero winding rule used by slicing.

FixModelByCgal now repairs each volume in place without splitting it into parts, and reports whether the mesh was modified so unchanged meshes keep their painting and ids. The texture import repair prompt no longer depends on the Windows SDK, and the FIX_THROUGH_CGAL_ALWAYS conditionals are removed. The commented-out Netfabb/WIN10SDK build code is deleted.

libslic3r_cgal now builds with CGAL_LINKED_WITH_TBB and links TBB. Add docs/HLSD/mesh-repair.md and unit tests in test_mesh_repair.cpp.

* Parallelize repair while preserving separate shells

Repair connected mesh units and shells independently in parallel, keep crossing shells separate, and limit self-intersection remeshing to local defects. Update mesh-repair documentation and tests for the revised behavior.

* Make mesh repair deterministic and report its progress

Flags written from parallel loops were std::vector<bool>, whose neighbouring
bits share a word, so shells could be kept or dropped at random; they are now
std::vector<char>. Holes of up to 100 edges are filled with the search over all
triangles: the search restricted to a 3D Delaunay triangulation picks between
equally good fills differently from run to run.

Shells of fewer than four faces are dropped, as the volumes of two triangles
back to back need not cancel exactly, and a shell that self-intersection removal
changed is split again so the pieces it cut off are dropped too.

repair_mesh() takes a progress callback in place of the cancel callback, and
Fix Model reports each stage. The object's bounding box is now invalidated even
when a cancel or an error stops the repair after some volumes were replaced.

The edge-use sort, union-find and box-overlap code shared by several stages is
factored out, the topology analysed for welding is reused to build the work
units, the orientation pass checks only the edges it cut, and the output is
written from the shells without joining them first.

* Fix Split to parts crashing on flat pieces and staying offered after a repair

ModelVolume::split() added every piece to the object and then deleted the
pieces without a convex hull. When the first piece was flat, as often in a
triangle soup, that deleted the volume being split while split() and its
caller kept using it. A part made only of flat pieces lost all its volumes,
and a soup added thousands of pieces, saving a backup of the object for each
one it deleted. Pieces without a convex hull are now dropped before any joins
the object, the volume being split keeps the first remaining piece, and a part
with no solid piece is left whole. Other parts of the object without a convex
hull are no longer deleted as a side effect.

ModelVolume caches whether its mesh can be split, and set_mesh() kept the
cached value, so after Fix Model, Simplify or Emboss replaced a mesh, Split to
parts could still be offered for a single piece. set_mesh() and reset_mesh()
now clear the cache.

* Import STEP surface models with Split compound enabled

Splitting a compound kept only its solids, so a surface model, which OCCT
reads as a compound of loose faces, gave nothing to mesh and the import failed
with "Meshing of a model file failed or no valid shape". Each shell outside a
solid now becomes a part too, and loose faces stay together as one part, as
they do without splitting.

* Add Fix All for plates and re-enable --repair on the command line

ModelVolume::repair() applies repair_mesh() to a part and replaces the mesh
only when the repair changed it. Fix Model, the new Fix All and Fix All Plates
items of the plate context menu, and the command line's --repair option, which
had been commented out since the Win10 SDK repair was dropped, all use it.

With Keep painted feature after mesh change off, Fix Model cleared the painting
of every part of an object before repairing. Now only a part whose mesh changes
loses its painting, so fixing a whole project does not wipe it. The
notification counts the models that needed no repair instead of listing them as
repaired, and objects that did not change are not refreshed.
This commit is contained in:
Ian Bassi
2026-10-12 21:59:17 -03:00
committed by GitHub
parent 075b03975f
commit 6d35ff6b9a
27 changed files with 2053 additions and 475 deletions
-4
View File
@@ -460,10 +460,6 @@ jobs:
- name: Build slicer Win
if: runner.os == 'Windows'
working-directory: ${{ github.workspace }}
# Orca: Removed Netfabb STL fixing service support in favor of CGAL.
# env:
# WindowsSdkDir: 'C:\Program Files (x86)\Windows Kits\10\'
# WindowsSDKVersion: '10.0.26100.0\'
# --tests builds the unit tests too; the unit_tests_windows_* jobs run them.
run: |
# cache-path is the install directory inside the deps build directory.
-41
View File
@@ -497,47 +497,6 @@ endif()
# Once we get rid of Perl and libslic3r is linked statically, we can get rid of -fPIC
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
# WIN10SDK_PATH is used to point CMake to the WIN10 SDK installation directory.
# We pick it from environment if it is not defined in another way
# ORCA: Removed Netfabb STL fixing service support in favor of CGAL.
# if(WIN32)
# if(NOT DEFINED WIN10SDK_PATH)
# if(DEFINED ENV{WIN10SDK_PATH})
# set(WIN10SDK_PATH "$ENV{WIN10SDK_PATH}")
# endif()
# endif()
# if(DEFINED WIN10SDK_PATH)
# #BBS: modify win10sdk_path
# if (EXISTS "${WIN10SDK_PATH}/winrt/windows.graphics.printing3d.h")
# set(WIN10SDK_INCLUDE_PATH "${WIN10SDK_PATH}")
# else()
# message("WIN10SDK_PATH is invalid: ${WIN10SDK_PATH}")
# message("${WIN10SDK_PATH}/winrt/windows.graphics.printing3d.h was not found")
# message("STL fixing by the Netfabb service will not be compiled")
# unset(WIN10SDK_PATH)
# endif()
# else()
# # Try to use the default Windows 10 SDK path.
# if (DEFINED ENV{WindowsSdkDir} AND DEFINED ENV{WindowsSDKVersion})
# set(WIN10SDK_INCLUDE_PATH "$ENV{WindowsSdkDir}/Include/$ENV{WindowsSDKVersion}")
# else ()
# set(WIN10SDK_INCLUDE_PATH "C:/Program Files (x86)/Windows Kits/10/Include/10.0.26100.0")
# endif ()
# if (NOT EXISTS "${WIN10SDK_INCLUDE_PATH}/winrt/windows.graphics.printing3d.h")
# message("${WIN10SDK_INCLUDE_PATH}/winrt/windows.graphics.printing3d.h was not found")
# message("STL fixing by the Netfabb service will not be compiled")
# unset(WIN10SDK_INCLUDE_PATH)
# endif()
# endif()
# if(WIN10SDK_INCLUDE_PATH)
# message("Building with Win10 Netfabb STL fixing service support")
# add_definitions(-DHAS_WIN10SDK)
# include_directories(SYSTEM "${WIN10SDK_INCLUDE_PATH}")
# else()
# message("Building without Win10 Netfabb STL fixing service support")
# endif()
# endif()
if (APPLE)
message("OS X SDK Path: ${CMAKE_OSX_SYSROOT}")
if (CMAKE_OSX_DEPLOYMENT_TARGET)
+135
View File
@@ -0,0 +1,135 @@
# Mesh repair — High Level Design
## Purpose and scope
Mesh repair turns a broken triangle mesh into closed, outward oriented shells that bound a solid, so that the
mesh slices predictably and the other mesh tools (booleans, cut, painting) get valid input. It runs on demand:
the *Fix Model* command of the context menu of an object or part and of the warning icon in the object list, *Fix
All* and *Fix All Plates* in the context menu of a plate, the prompt the Cut tool shows when a cut leaves open edges,
the *Repair and import* choice of texture import, and the `--repair` option of the command line. It is built on CGAL
and is the only repair the application has, on every platform.
`repair_mesh()` in `src/libslic3r/MeshRepair.cpp` does the work on one `TriangleMesh`, and `ModelVolume::repair()`
applies it to a part: it replaces the mesh only when the repair changed it, and remaps or drops the painting of a
replaced mesh. The GUI side, `fix_model_with_cgal_gui()` in `src/slic3r/Utils/FixModelByCgal.cpp`, runs it on a
worker thread for each volume of an object, or for one volume, behind a progress dialog. `repair_mesh()` reports its
progress between stages through a callback, which cancels the repair by throwing.
## What the repaired mesh is
The repair keeps what the slicer would print from the input. The slicer fills a layer where the winding number of
the input surface is not zero (`MeshSlicingParams::SlicingMode::Regular` uses Clipper's nonzero fill rule), so:
- Shells crossing each other print as their union. The repair keeps them as separate shells, as the parts of an
object are kept, and makes each of them an outward oriented solid.
- A shell inside a solid that is oriented like it adds nothing, and the repair removes it.
- A shell inside a solid that is oriented inward is a cavity, and stays one.
- A whole mesh turned inside out prints as a solid, and the repair turns it outward.
On top of that, it closes holes and cracks, gives inconsistently oriented triangles the orientation of their
neighbours, drops degenerate, duplicate and stray flat geometry, and removes small self-intersections inside a shell,
such as slivers and folds. Where none of this applies, it reports the mesh as unmodified and the caller keeps the
original mesh, its painting and its object id.
A volume is repaired in place and is never split into several parts: splitting by connectivity would turn a cavity
into a separate solid part and fill it. Each solid of the result is a shell of its own, so *Split to parts* still
separates them. Open pieces of the input whose borders meet exactly, which are pieces of one surface rather than
solids, come out as the one shell they form.
## Pipeline
The input is treated as a triangle soup. Every stage after the first only runs on what the previous ones could
not settle, so a valid mesh passes through quickly.
### Valid parts first
Parts of the input that are connected by vertex indices and are already closed, consistently oriented 2-manifolds
skip the soup stages and go straight to the shell stages. Merging their points with coincident points of other
parts would only make non-manifold junctions out of solids that touch, for example parts of an assembly exported
together or voxels written as separate cubes.
### Soup clean-up
The rest of the soup has its coincident points merged. Triangles with repeated corners are dropped. Coincident
triangles of opposite orientation are a wall between two solids touching face to face, so they cancel in pairs;
of the remaining copies of a triangle one is kept. A connected region that would vanish entirely through
cancelling is a two-sided export instead (every face written in both orientations), and keeps one copy of each
triangle.
Open edges then get their vertices welded within a tolerance of 1e-4 of the bounding box diagonal, which closes the
cracks some exporters leave between triangles that should share corners. A vertex is never welded further than a
quarter of its shortest edge, so welding cannot collapse fine detail. A soup that stays mostly open after welding has
cracks wider than that tolerance, and is welded again with ten and then a hundred times the tolerance.
### Work units
The soup is cut into units repaired independently and in parallel: its connected parts, except that open parts whose
bounding boxes touch share a unit, so that cracks and T-junctions between separately exported patches still close.
### Building the mesh
A unit whose edges are each shared by at most two triangles, running in opposite directions, and whose vertices each
have a single fan of triangles, becomes a `Surface_mesh` directly. Any other unit is oriented first. Triangles are
flipped so that neighbours across an edge shared by two of them agree, each part keeping the orientation the
majority of its area had, which keeps inward oriented cavities inward. Edges where that is impossible or that more
than two triangles share are cut, and each fan of triangles around a vertex gets its own copy of the vertex. If that
still leaves an invalid mesh, CGAL's `orient_polygon_soup()` orients the unit instead, and the parts it flipped
against their majority are reversed back. The linear pass exists because `orient_polygon_soup()` is several times
slower on large soups that need only a few fixes.
### Closing
Compatible borders are stitched, parts that can be stitched once reversed are merged, and borders still open are
snapped onto each other within the weld tolerance, which closes T-junctions. Exactly degenerate faces are removed.
Holes are filled last. Planar border loops that face each other in one plane, one inside the other, bound a single
region, such as a missing annulus at the end of a tube or a cap with features standing on it. Such a group is
triangulated at once with a 2D constrained Delaunay triangulation that fills only the faces at odd nesting depth,
so the openings inside the region stay open. The patch is used only if it closes every loop with the orientation
of the triangles around it. Every other loop is filled on its own with `triangulate_hole()`, which uses a 2D
constrained Delaunay triangulation when the loop is planar. Otherwise it searches the best triangulation among all
triangles over the loop for loops of up to 100 edges, and among the triangles of a 3D Delaunay triangulation for
longer ones, as the full search is cubic in the length of the loop. Beyond 200 edges the full search is not even
used as a fallback, and a fan around the loop's centroid closes a loop nothing else could.
The closed unit is split into its shells. The largest stays in place and the others are copied out, as converting
the whole mesh again costs seconds on large models.
### Shells
The shells of all units, together with the valid parts set aside at the start, are handled independently and in
parallel. Shells enclosing no volume, which hole filling makes out of stray flat patches, are dropped.
Self-intersections inside a shell are removed locally with `experimental::remove_self_intersections()`, which
remeshes the region around them. It is used only for local defects: a shell whose intersections involve more than
5 % of its faces is left as it is, and a result that changes the enclosed volume by more than 0.1 % is undone. On
heavily overlapping ornamental models the remeshing otherwise cuts parts down to slivers, while on genuine defects
it changes the volume by thousandths of a percent. Overlaps left in place slice correctly. Remeshing can cut
small pieces off a shell, so a shell it changed is split again and its pieces enclosing no volume are dropped.
Shells crossing others are found through their bounding boxes and an AABB tree per shell, and are made outward
oriented solids. For every other shell the winding number just outside it is summed from the orientation of the
shells containing it, tested from a point on its largest face. The shell bounds the solid only if the winding
number is zero on exactly one of its sides; it is then oriented so that the solid is inside, or dropped otherwise.
Crossing shells are not united. Slicing unites them anyway, uniting them exactly costs tens of seconds on models
made of many overlapping pieces, and it would fuse parts that *Split to parts* should keep apart.
## Robustness
The repair avoids `experimental::autorefine_and_remove_self_intersections()` and corefinement, which can crash on
messy input, and uses only steps whose failures it can detect. A step that fails keeps its input. CGAL precondition
failures surface as `repair_mesh()` returning false with the message as the error. The output is written in float,
as every mesh in the application is.
The result does not depend on thread scheduling. `libslic3r_cgal` is built with `CGAL_LINKED_WITH_TBB`. Units and
shells are processed in parallel, but each on its own data, and their results are gathered in a fixed order;
sorts compare complete keys, and the detected self-intersections are put in order before they are used. Snapping
stays sequential, as its parallel version gives different results from run to run. Holes are filled with the full
search wherever it is affordable, because the search among Delaunay triangles can choose between equally good fills
differently from run to run, so only non-planar holes of more than 100 edges may still vary. On some inputs
`experimental::remove_self_intersections()` itself places a few vertices differently from run to run.
## Verification
`tests/libslic3r/test_mesh_repair.cpp` covers each kind of defect above on small meshes with known volumes.
+14 -3
View File
@@ -99,6 +99,7 @@
#include "libslic3r/Preset.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/GCode.hpp"
#include "libslic3r/MeshRepair.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/ModelArrange.hpp"
#include "libslic3r/Platform.hpp"
@@ -5130,9 +5131,19 @@ int CLI::run(int argc, char **argv)
}
}
} else if (opt_key == "repair") {
// Models are repaired by default.
//for (auto &model : m_models)
// model.repair();
if (m_config.opt_bool(opt_key))
for (Model &model : m_models)
for (ModelObject *object : model.objects) {
for (ModelVolume *volume : object->volumes) {
MeshRepairStats stats;
std::string error;
if (!volume->repair(true, &stats, &error))
BOOST_LOG_TRIVIAL(warning) << "repair of " << object->name << " / " << volume->name << " failed: " << error;
else if (stats.modified)
BOOST_LOG_TRIVIAL(info) << "repaired " << object->name << " / " << volume->name;
}
object->invalidate_bounding_box();
}
} else {
boost::nowide::cerr << "error: option not implemented yet: " << opt_key << std::endl;
record_exit_reson(outfile_dir, CLI_UNSUPPORTED_OPERATION, 0, cli_errors[CLI_UNSUPPORTED_OPERATION], sliced_info);
+4 -1
View File
@@ -625,6 +625,7 @@ add_library(libslic3r_cgal STATIC
CutSurface.hpp CutSurface.cpp
IntersectionPoints.hpp IntersectionPoints.cpp
MeshBoolean.hpp MeshBoolean.cpp
MeshRepair.hpp MeshRepair.cpp
TryCatchSignal.hpp TryCatchSignal.cpp
Triangulation.hpp Triangulation.cpp
)
@@ -652,7 +653,9 @@ if (IS_CLANG_CL)
target_compile_options(libslic3r_cgal PRIVATE -Wno-overriding-option)
endif ()
target_link_libraries(libslic3r_cgal PRIVATE ${_cgal_tgt} admesh libigl mcut boost_libs)
# Lets CGAL algorithms given CGAL::Parallel_if_available_tag run on TBB.
target_compile_definitions(libslic3r_cgal PRIVATE CGAL_LINKED_WITH_TBB)
target_link_libraries(libslic3r_cgal PRIVATE ${_cgal_tgt} admesh libigl mcut boost_libs TBB::tbb)
if (MSVC AND "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") # 32 bit MSVC workaround
target_compile_definitions(libslic3r_cgal PRIVATE CGAL_DO_NOT_USE_MPZF)
+15 -5
View File
@@ -236,19 +236,29 @@ static void getNamedSolids(const TopLoc_Location& location,
int i = 0;
switch (shape_type) {
case TopAbs_COMPOUND:
if (!isSplitCompound) {
namedSolids.emplace_back(TopoDS::Compound(transform.Shape()), fullName);
break;
}
case TopAbs_COMPSOLID:
if (!isSplitCompound) {
namedSolids.emplace_back(TopoDS::CompSolid(transform.Shape()), fullName);
namedSolids.emplace_back(transform.Shape(), fullName);
} else {
for (explorer.Init(transform.Shape(), TopAbs_SOLID); explorer.More(); explorer.Next()) {
i++;
const TopoDS_Shape& currentShape = explorer.Current();
namedSolids.emplace_back(TopoDS::Solid(currentShape), fullName + "-SOLID-" + std::to_string(i));
}
// Surface models have no solids: each shell outside a solid is a part, and loose faces stay together.
i = 0;
for (explorer.Init(transform.Shape(), TopAbs_SHELL, TopAbs_SOLID); explorer.More(); explorer.Next())
namedSolids.emplace_back(explorer.Current(), fullName + "-SHELL-" + std::to_string(++i));
TopoDS_Compound loose_faces;
TopoDS_Builder builder;
builder.MakeCompound(loose_faces);
bool has_loose_faces = false;
for (explorer.Init(transform.Shape(), TopAbs_FACE, TopAbs_SHELL); explorer.More(); explorer.Next()) {
builder.Add(loose_faces, explorer.Current());
has_loose_faces = true;
}
if (has_loose_faces)
namedSolids.emplace_back(loose_faces, fullName);
}
break;
case TopAbs_SOLID:
-88
View File
@@ -29,8 +29,6 @@
#include <CGAL/Polygon_mesh_processing/triangulate_hole.h>
#include <CGAL/Polygon_mesh_processing/stitch_borders.h>
#include <CGAL/Polygon_mesh_processing/polygon_soup_to_polygon_mesh.h>
#include <CGAL/Polygon_mesh_processing/repair_degeneracies.h>
#include <CGAL/Polygon_mesh_processing/manifoldness.h>
#include <cstdint>
#include <map>
#include <algorithm>
@@ -50,7 +48,6 @@
#include <CGAL/Polygon_mesh_processing/orient_polygon_soup.h>
#include <CGAL/Polygon_mesh_processing/repair.h>
#include <CGAL/Polygon_mesh_processing/remesh.h>
#include <CGAL/Polygon_mesh_processing/repair_polygon_soup.h>
#include <CGAL/Polygon_mesh_processing/orientation.h>
// BBS: for segment
#include <CGAL/mesh_segmentation.h>
@@ -658,91 +655,6 @@ bool empty(const CGALMesh &mesh)
return mesh.m.is_empty();
}
bool repair(TriangleMesh& mesh, RepairedMeshErrors* repaired_errors, std::string* error)
{
namespace PMP = CGAL::Polygon_mesh_processing;
if (mesh.empty())
return true;
try {
// 1) Convert to polygon soup
std::vector<_EpicMesh::Point> points;
std::vector<std::vector<std::size_t>> polygons;
points.reserve(mesh.its.vertices.size());
polygons.reserve(mesh.its.indices.size());
for (const auto& v : mesh.its.vertices)
points.emplace_back(v.x(), v.y(), v.z());
for (const auto& f : mesh.its.indices)
polygons.push_back({size_t(f[0]), size_t(f[1]), size_t(f[2])});
// 2) Aggressive soup cleanup
PMP::repair_polygon_soup(points, polygons);
// 3) Convert soup → mesh
_EpicMesh cgal_mesh;
PMP::polygon_soup_to_polygon_mesh(points, polygons, cgal_mesh);
// 4) Remove degenerate geometry
PMP::remove_degenerate_faces(cgal_mesh);
PMP::remove_isolated_vertices(cgal_mesh);
// 5) Fix remaining non-manifold vertices
PMP::duplicate_non_manifold_vertices(cgal_mesh);
// 6) Boolean union (keeps only outer shell)
_EpicMesh tmp;
if (PMP::corefine_and_compute_union(cgal_mesh, cgal_mesh, tmp)) {
cgal_mesh = std::move(tmp);
}
// If it fails, continue anyway with previous mesh
// 7) Fill holes
if (!CGAL::is_closed(cgal_mesh)) {
using halfedge_descriptor = boost::graph_traits<_EpicMesh>::halfedge_descriptor;
std::vector<halfedge_descriptor> borders;
CGAL::extract_boundary_cycles(cgal_mesh, std::back_inserter(borders));
for (halfedge_descriptor h : borders) {
PMP::triangulate_and_refine_hole(cgal_mesh, h);
}
}
// 8) Final validity check
if (!CGAL::is_closed(cgal_mesh)) {
if (error)
*error = "Repair failed: mesh still open after hole filling.";
return false;
}
// 9) Ensure outward orientation
if (!PMP::does_bound_a_volume(cgal_mesh))
PMP::orient_to_bound_a_volume(cgal_mesh);
// 10) Convert back
indexed_triangle_set its = cgal_to_indexed_triangle_set(cgal_mesh);
RepairedMeshErrors errs{};
errs.facets_removed = 0;
errs.edges_fixed = 0;
mesh = TriangleMesh(std::move(its), errs);
if (repaired_errors)
*repaired_errors = errs;
return true;
} catch (const std::exception& e) {
if (error)
*error = e.what();
return false;
}
}
CGALMeshPtr clone(const CGALMesh &m)
{
return CGALMeshPtr{new CGALMesh{m}};
-3
View File
@@ -77,9 +77,6 @@ TriangleMesh merge(std::vector<TriangleMesh> meshes);
bool does_bound_a_volume(const CGALMesh &mesh);
bool empty(const CGALMesh &mesh);
// Repair a mesh using CGAL. Returns true on success. Optionally returns a summary of repairs and an error string.
bool repair(TriangleMesh &mesh, RepairedMeshErrors *repaired_errors = nullptr, std::string *error = nullptr);
// Real UV unwrap of an open mesh patch via CGAL's LSCM (Least Squares Conformal Maps) surface
// parameterization. Returns one UV coordinate per input vertex (same indexing as `mesh.vertices`),
// or nullopt if `mesh` isn't a single topological disk -- LSCM needs exactly one connected
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <cstddef>
#include <functional>
#include <string>
namespace Slic3r {
class TriangleMesh;
struct MeshRepairStats
{
std::size_t welded_vertices = 0;
std::size_t removed_triangles = 0;
std::size_t reversed_shells = 0;
std::size_t filled_holes = 0;
std::size_t removed_shells = 0;
std::size_t self_intersections = 0;
// False when the input already was closed, consistently oriented and free of self-intersections.
bool modified = false;
};
// Returns false, leaving the mesh untouched, if no solid can be built. progress gets the percentage done and may
// throw to cancel. See docs/HLSD/mesh-repair.md.
bool repair_mesh(TriangleMesh &mesh, MeshRepairStats *stats = nullptr, std::string *error = nullptr,
const std::function<void(int)> &progress = {});
} // namespace Slic3r
+39 -22
View File
@@ -29,6 +29,7 @@
#include "TriangleMeshSlicer.hpp"
#include "TriangleSelector.hpp"
#include "MaterialType.hpp"
#include "MeshRepair.hpp"
#include "Format/AMF.hpp"
#include "Format/svg.hpp"
@@ -2946,6 +2947,18 @@ size_t ModelVolume::split(unsigned int max_extruders, bool remap_paint)
if (text_configuration.has_value())
text_configuration.reset();
// Parts without a convex hull enclose no volume; drop them here, as deleting volumes later could delete this one.
std::vector<std::unique_ptr<ModelVolume>> parts;
for (TriangleMesh &mesh : meshes)
if (!mesh.empty()) {
std::unique_ptr<ModelVolume> part(new ModelVolume(object, *this, std::move(mesh)));
if (const std::shared_ptr<const TriangleMesh> &hull = part->get_convex_hull_shared_ptr();
hull != nullptr && !hull->its.vertices.empty() && !hull->its.indices.empty())
parts.emplace_back(std::move(part));
}
if (parts.empty())
return 1;
std::optional<TriangleSelector::SavedPainting> saved_painting = remap_paint ? save_painting() :
std::optional<TriangleSelector::SavedPainting>{};
@@ -2956,26 +2969,22 @@ size_t ModelVolume::split(unsigned int max_extruders, bool remap_paint)
unsigned int extruder_counter = 0;
const Vec3d offset = this->get_offset();
for (TriangleMesh &mesh : meshes) {
if (mesh.empty())
// Repair may have removed unconnected triangles, thus emptying the mesh.
continue;
for (std::unique_ptr<ModelVolume> &part : parts) {
if (idx == 0) {
this->set_mesh(std::move(mesh));
this->calculate_convex_hull();
this->set_mesh(part->m_mesh);
m_convex_hull = part->m_convex_hull;
this->invalidate_convex_hull_2d();
// Assign a new unique ID, so that a new GLVolume will be generated.
this->set_new_unique_id();
// reset the source to disable reload from disk
this->source = ModelVolume::Source();
// BBS: reset facet annotations
this->reset_extra_facets();
}
else
this->object->volumes.insert(this->object->volumes.begin() + (++ivolume), new ModelVolume(object, *this, std::move(mesh)));
this->object->volumes.insert(this->object->volumes.begin() + (++ivolume), part.release());
// reset the source to disable reload from disk
this->object->volumes[ivolume]->source = ModelVolume::Source();
this->object->volumes[ivolume]->set_offset(Vec3d::Zero());
this->object->volumes[ivolume]->center_geometry_after_creation();
this->object->volumes[ivolume]->translate(offset);
@@ -2989,21 +2998,29 @@ size_t ModelVolume::split(unsigned int max_extruders, bool remap_paint)
++ idx;
}
// discard volumes for which the convex hull was not generated or is degenerate
size_t i = 0;
while (i < this->object->volumes.size()) {
const std::shared_ptr<const TriangleMesh> &hull = this->object->volumes[i]->get_convex_hull_shared_ptr();
if (hull == nullptr || hull->its.vertices.empty() || hull->its.indices.empty()) {
this->object->delete_volume(i);
--idx;
--i;
}
++i;
}
return idx;
}
bool ModelVolume::repair(bool keep_painting, MeshRepairStats *stats_out, std::string *error, const std::function<void(int)> &progress)
{
TriangleMesh mesh = this->mesh();
MeshRepairStats stats;
const bool ok = repair_mesh(mesh, &stats, error, progress);
if (stats_out)
*stats_out = stats;
if (!ok || !stats.modified)
return ok;
const std::optional<TriangleSelector::SavedPainting> saved_painting = keep_painting ? save_painting() : std::nullopt;
this->set_mesh(std::move(mesh));
this->calculate_convex_hull();
this->invalidate_convex_hull_2d();
// Assign a new unique ID, so that a new GLVolume will be generated.
this->set_new_unique_id();
this->restore_painting(saved_painting);
return true;
}
void ModelVolume::translate(const Vec3d& displacement)
{
set_offset(get_offset() + displacement);
+11 -7
View File
@@ -77,6 +77,7 @@ class Preset;
class BBLProject;
class KeyStore;
struct MeshRepairStats;
namespace UndoRedo {
class StackImpl;
@@ -887,13 +888,13 @@ public:
// The triangular model.
const TriangleMesh& mesh() const { return *m_mesh.get(); }
std::shared_ptr<const TriangleMesh> mesh_ptr() const { return m_mesh; }
void set_mesh(const TriangleMesh &mesh) { m_mesh = std::make_shared<const TriangleMesh>(mesh); }
void set_mesh(TriangleMesh &&mesh) { m_mesh = std::make_shared<const TriangleMesh>(std::move(mesh)); }
void set_mesh(const indexed_triangle_set &mesh) { m_mesh = std::make_shared<const TriangleMesh>(mesh); }
void set_mesh(indexed_triangle_set &&mesh) { m_mesh = std::make_shared<const TriangleMesh>(std::move(mesh)); }
void set_mesh(std::shared_ptr<const TriangleMesh> &mesh) { m_mesh = mesh; }
void set_mesh(std::unique_ptr<const TriangleMesh> &&mesh) { m_mesh = std::move(mesh); }
void reset_mesh() { m_mesh = std::make_shared<const TriangleMesh>(); }
void set_mesh(const TriangleMesh &mesh) { m_mesh = std::make_shared<const TriangleMesh>(mesh); m_is_splittable = -1; }
void set_mesh(TriangleMesh &&mesh) { m_mesh = std::make_shared<const TriangleMesh>(std::move(mesh)); m_is_splittable = -1; }
void set_mesh(const indexed_triangle_set &mesh) { m_mesh = std::make_shared<const TriangleMesh>(mesh); m_is_splittable = -1; }
void set_mesh(indexed_triangle_set &&mesh) { m_mesh = std::make_shared<const TriangleMesh>(std::move(mesh)); m_is_splittable = -1; }
void set_mesh(std::shared_ptr<const TriangleMesh> &mesh) { m_mesh = mesh; m_is_splittable = -1; }
void set_mesh(std::unique_ptr<const TriangleMesh> &&mesh) { m_mesh = std::move(mesh); m_is_splittable = -1; }
void reset_mesh() { m_mesh = std::make_shared<const TriangleMesh>(); m_is_splittable = -1; }
const std::shared_ptr<const TriangleMesh>& get_mesh_shared_ptr() const { return m_mesh; }
// Configuration parameters specific to an object model geometry or a modifier volume,
// overriding the global Slic3r settings and the ModelObject settings.
@@ -1043,6 +1044,9 @@ public:
// Return the number of volumes created from this one.
// This is useful to assign different materials to different volumes of an object.
size_t split(unsigned int max_extruders, bool remap_paint);
// Replaces the mesh by its repair_mesh() result if that changed it. False if no solid can be built.
bool repair(bool keep_painting, MeshRepairStats *stats = nullptr, std::string *error = nullptr,
const std::function<void(int)> &progress = {});
void translate(double x, double y, double z) { translate(Vec3d(x, y, z)); }
void translate(const Vec3d& displacement);
void scale(const Vec3d& scaling_factors);
+4 -3
View File
@@ -12866,10 +12866,11 @@ CLITransformConfigDef::CLITransformConfigDef()
//def->cli = "orient|o";
def->set_default_value(new ConfigOptionInt(0));
/*def = this->add("repair", coBool);
def = this->add("repair", coBool);
def->label = L("Repair");
def->tooltip = L("Repair the model's meshes if it is non-manifold mesh.");
def->set_default_value(new ConfigOptionBool(false));*/
def->tooltip = L("Repair the meshes of the models as Fix Model does: close holes and cracks, orient them outward and "
"remove self-intersections.");
def->set_default_value(new ConfigOptionBool(false));
def = this->add("rotate", coFloat);
def->label = L("Rotate");
@@ -673,14 +673,14 @@ static bool repair_cluster_smooth(
[&]() { return cancelled(); }, &repair_error);
if (repaired) {
if (cancelled()) return false;
BOOST_LOG_TRIVIAL(info) << log_prefix << ": Windows 3D mesh repair finished.";
BOOST_LOG_TRIVIAL(info) << log_prefix << ": mesh repair finished.";
if (!resample_face_colors(TriMesh(std::move(repaired_its))))
return false;
} else {
BOOST_LOG_TRIVIAL(warning) << log_prefix << ": Windows 3D mesh repair failed: " << repair_error;
BOOST_LOG_TRIVIAL(warning) << log_prefix << ": mesh repair failed: " << repair_error;
}
} else {
BOOST_LOG_TRIVIAL(info) << log_prefix << ": importing mesh without Windows 3D repair.";
BOOST_LOG_TRIVIAL(info) << log_prefix << ": importing mesh without repair.";
}
}
}
+10
View File
@@ -1952,6 +1952,16 @@ void MenuFactory::create_plate_menu()
},
"", nullptr, []() { return !plater()->get_partplate_list().get_selected_plate()->get_objects().empty(); }, m_parent);
// fix objects on current plate
append_menu_item(menu, wxID_ANY, _L("Fix All"), _L("Fix all models on the current plate"),
[](wxCommandEvent&) { obj_list()->fix_all_through_cgal(false); }, "", nullptr,
[]() { return !plater()->get_partplate_list().get_selected_plate()->empty(); }, m_parent);
// fix objects on all plates
append_menu_item(menu, wxID_ANY, _L("Fix All Plates"), _L("Fix all models on all plates"),
[](wxCommandEvent&) { obj_list()->fix_all_through_cgal(true); }, "", nullptr,
[]() { return !plater()->model().objects.empty(); }, m_parent);
// orient objects on current plate
append_menu_item(menu, wxID_ANY, _L("Auto Rotate"), _L("Auto rotate current plate"),
[](wxCommandEvent&) {
+65 -55
View File
@@ -6297,30 +6297,44 @@ void ObjectList::rename_item()
}
void ObjectList::fix_through_cgal()
{
std::vector<int> obj_idxs, vol_idxs;
get_selection_indexes(obj_idxs, vol_idxs);
fix_through_cgal(obj_idxs, vol_idxs);
}
void ObjectList::fix_all_through_cgal(bool all_plates)
{
std::vector<int> obj_idxs;
if (all_plates) {
for (int obj_idx = 0; obj_idx < int(m_objects->size()); ++obj_idx)
obj_idxs.push_back(obj_idx);
} else {
for (const ModelObject *mo : wxGetApp().plater()->get_partplate_list().get_selected_plate()->get_objects_on_this_plate())
obj_idxs.push_back(int(std::find(m_objects->begin(), m_objects->end(), mo) - m_objects->begin()));
std::sort(obj_idxs.begin(), obj_idxs.end());
obj_idxs.erase(std::unique(obj_idxs.begin(), obj_idxs.end()), obj_idxs.end());
}
fix_through_cgal(obj_idxs, {});
}
void ObjectList::fix_through_cgal(const std::vector<int> &obj_idxs, const std::vector<int> &vol_idxs)
{
// Do not fix anything when a gizmo is open. There might be issues with updates
// and what is worse, the snapshot time would refer to the internal stack.
if (!wxGetApp().plater()->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::Undefined))
if (obj_idxs.empty() || !wxGetApp().plater()->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::Undefined))
return;
// model_name
std::vector<std::string> succes_models;
// model_name failing reason
std::vector<std::pair<std::string, std::string>> failed_models;
std::vector<int> obj_idxs, vol_idxs;
get_selection_indexes(obj_idxs, vol_idxs);
size_t unchanged_models = 0;
bool painting_removed = false;
std::vector<std::string> model_names;
// clear selections from the non-broken models if any exists
// and than fill names of models to repairing
if (vol_idxs.empty()) {
#if !FIX_THROUGH_CGAL_ALWAYS
for (int i = int(obj_idxs.size())-1; i >= 0; --i)
if (object(obj_idxs[i])->get_repaired_errors_count() == 0)
obj_idxs.erase(obj_idxs.begin()+i);
#endif // FIX_THROUGH_CGAL_ALWAYS
for (int obj_idx : obj_idxs)
if (object(obj_idx))
model_names.push_back(object(obj_idx)->name);
@@ -6328,31 +6342,26 @@ void ObjectList::fix_through_cgal()
else {
ModelObject* obj = object(obj_idxs.front());
if (obj) {
#if !FIX_THROUGH_CGAL_ALWAYS
for (int i = int(vol_idxs.size()) - 1; i >= 0; --i)
if (obj->get_repaired_errors_count(vol_idxs[i]) == 0)
vol_idxs.erase(vol_idxs.begin() + i);
#endif // FIX_THROUGH_CGAL_ALWAYS
for (int vol_idx : vol_idxs)
model_names.push_back(obj->volumes[vol_idx]->name);
}
}
auto plater = wxGetApp().plater();
const bool keep_painting = GUI::wxGetApp().app_config->get_bool("keep_painting");
auto fix_and_update_progress = [this, plater, model_names](const int obj_idx, const int vol_idx,
int model_idx,
ProgressDialog& progress_dlg,
std::vector<std::string>& succes_models,
std::vector<std::pair<std::string, std::string>>& failed_models)
auto fix_and_update_progress = [&](const int obj_idx, const int vol_idx, int model_idx, ProgressDialog& progress_dlg)
{
if (!object(obj_idx))
ModelObject *mo = object(obj_idx);
if (!mo)
return false;
const std::string& model_name = model_names[model_idx];
wxString msg = _L("Repairing model object");
if (model_names.size() == 1)
msg += ": " + from_u8(model_name) + "\n";
else if (model_names.size() > 10)
msg += ": " + from_u8(model_name) + wxString::Format(" (%d/%d)", model_idx + 1, int(model_names.size())) + "\n";
else {
msg += ":\n";
for (int i = 0; i < int(model_names.size()); ++i)
@@ -6360,33 +6369,29 @@ void ObjectList::fix_through_cgal()
msg += "\n";
}
const bool keep_painting = GUI::wxGetApp().app_config->get_bool("keep_painting");
if (!keep_painting) {
plater->clear_before_change_mesh(obj_idx);
}
const size_t volumes_before = object(obj_idx)->volumes.size();
auto count_painted = [mo]() { return std::count_if(mo->volumes.begin(), mo->volumes.end(), [](const ModelVolume *mv) { return mv->is_any_painted(); }); };
const auto painted_before = count_painted();
std::string res;
if (!fix_model_with_cgal_gui(*(object(obj_idx)), vol_idx, progress_dlg, msg, res, keep_painting))
bool modified = false;
if (!fix_model_with_cgal_gui(*mo, vol_idx, progress_dlg, msg, res, keep_painting, &modified))
return false;
//wxGetApp().plater()->changed_mesh(obj_idx);
object(obj_idx)->ensure_on_bed();
plater->changed_mesh(obj_idx);
const size_t volumes_after = object(obj_idx)->volumes.size();
if (volumes_after != volumes_before)
add_volumes_to_object_in_list(obj_idx);
if (modified) {
painting_removed = painting_removed || count_painted() < painted_before;
mo->ensure_on_bed();
plater->changed_mesh(obj_idx);
plater->get_partplate_list().notify_instance_update(obj_idx, 0);
plater->sidebar().obj_list()->update_plate_values_for_items();
update_item_error_icon(obj_idx, vol_idx);
update_info_items(obj_idx);
}
plater->get_partplate_list().notify_instance_update(obj_idx, 0);
plater->sidebar().obj_list()->update_plate_values_for_items();
if (res.empty())
if (!res.empty())
failed_models.push_back({ model_name, res });
else if (modified)
succes_models.push_back(model_name);
else
failed_models.push_back({ model_name, res });
update_item_error_icon(obj_idx, vol_idx);
update_info_items(obj_idx);
++unchanged_models;
return true;
};
@@ -6399,11 +6404,7 @@ void ObjectList::fix_through_cgal()
if (vol_idxs.empty()) {
int vol_idx{ -1 };
for (int obj_idx : obj_idxs) {
#if !FIX_THROUGH_CGAL_ALWAYS
if (object(obj_idx)->get_repaired_errors_count(vol_idx) == 0)
continue;
#endif // FIX_THROUGH_CGAL_ALWAYS
if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg, succes_models, failed_models))
if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg))
break;
model_idx++;
}
@@ -6411,7 +6412,7 @@ void ObjectList::fix_through_cgal()
else {
int obj_idx{ obj_idxs.front() };
for (int vol_idx : vol_idxs) {
if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg, succes_models, failed_models))
if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg))
break;
model_idx++;
}
@@ -6419,20 +6420,29 @@ void ObjectList::fix_through_cgal()
// Close the progress dialog
progress_dlg.Update(100, "");
if (painting_removed)
plater->notify_painting_removed();
// Show info notification
wxString msg;
std::vector<wxString> blocks;
wxString bullet_suf = "\n - ";
if (!succes_models.empty()) {
msg = _L_PLURAL("The following model object has been repaired", "The following model objects have been repaired", succes_models.size()) + ":";
wxString block = _L_PLURAL("The following model object has been repaired", "The following model objects have been repaired", succes_models.size()) + ":";
for (auto& model : succes_models)
msg += bullet_suf + from_u8(model);
msg += "\n\n";
block += bullet_suf + from_u8(model);
blocks.push_back(block);
}
if (!failed_models.empty()) {
msg += _L_PLURAL("Failed to repair the following model object", "Failed to repair the following model objects", failed_models.size()) + ":\n";
wxString block = _L_PLURAL("Failed to repair the following model object", "Failed to repair the following model objects", failed_models.size()) + ":";
for (auto& model : failed_models)
msg += bullet_suf + from_u8(model.first) + ": " + _(model.second);
block += bullet_suf + from_u8(model.first) + ": " + _(model.second);
blocks.push_back(block);
}
if (unchanged_models > 0)
blocks.push_back(format_wxstr(_L_PLURAL("%1$d model object needed no repair.", "%1$d model objects needed no repair.", unchanged_models), unchanged_models));
wxString msg;
for (const wxString &block : blocks)
msg += (msg.IsEmpty() ? "" : "\n\n") + block;
if (msg.IsEmpty())
msg = _L("Repairing was canceled");
plater->get_notification_manager()->push_notification(NotificationType::CgalFinished, NotificationManager::NotificationLevel::PrintInfoShortNotificationLevel, into_u8(msg));
+3 -3
View File
@@ -47,9 +47,6 @@ typedef double coordf_t;
typedef std::pair<coordf_t, coordf_t> t_layer_height_range;
typedef std::map<t_layer_height_range, ModelConfig> t_layer_config_ranges;
// Manifold mesh may contain self-intersections, so we want to always allow fixing the mesh.
#define FIX_THROUGH_CGAL_ALWAYS 1
namespace GUI {
enum class Shortcut : uint8_t;
@@ -463,6 +460,9 @@ public:
void split_instances();
void rename_item();
void fix_through_cgal();
// Fixes every object on the selected plate, or on all plates.
void fix_all_through_cgal(bool all_plates);
void fix_through_cgal(const std::vector<int> &obj_idxs, const std::vector<int> &vol_idxs);
void simplify();
void smooth_mesh();
void update_item_error_icon(const int obj_idx, int vol_idx) const ;
+11 -24
View File
@@ -14984,24 +14984,8 @@ bool Plater::priv::can_fix_through_cgal() const
std::vector<int> obj_idxs, vol_idxs;
sidebar->obj_list()->get_selection_indexes(obj_idxs, vol_idxs);
#if FIX_THROUGH_CGAL_ALWAYS
// Fixing always.
// A closed mesh may still self-intersect, so fixing is always allowed.
return ! obj_idxs.empty() || ! vol_idxs.empty();
#else // FIX_THROUGH_CGAL_ALWAYS
// Fixing only if the model is not manifold.
if (vol_idxs.empty()) {
for (auto obj_idx : obj_idxs)
if (model.objects[obj_idx]->get_repaired_errors_count() > 0)
return true;
return false;
}
int obj_idx = obj_idxs.front();
for (auto vol_idx : vol_idxs)
if (model.objects[obj_idx]->get_repaired_errors_count(vol_idx) > 0)
return true;
return false;
#endif // FIX_THROUGH_CGAL_ALWAYS
}
bool Plater::priv::can_simplify() const
@@ -22041,13 +22025,16 @@ void Plater::clear_before_change_mesh(int obj_idx)
mv->mmu_segmentation_facets.reset();
mv->fuzzy_skin_facets.reset();
}
if (paint_removed) {
// snapshot_time is captured by copy so the lambda knows where to undo/redo to.
get_notification_manager()->push_notification(
NotificationType::CustomSupportsAndSeamRemovedAfterRepair,
NotificationManager::NotificationLevel::PrintInfoNotificationLevel,
_u8L("Custom supports and color painting were removed before repairing."));
}
if (paint_removed)
notify_painting_removed();
}
void Plater::notify_painting_removed()
{
get_notification_manager()->push_notification(
NotificationType::CustomSupportsAndSeamRemovedAfterRepair,
NotificationManager::NotificationLevel::PrintInfoNotificationLevel,
_u8L("Custom supports and color painting were removed before repairing."));
}
void Plater::changed_mesh(int obj_idx)
+1
View File
@@ -599,6 +599,7 @@ public:
void reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject &object, bool postpone_error_messages = false);
void clear_before_change_mesh(int obj_idx);
void notify_painting_removed();
void changed_mesh(int obj_idx);
void changed_object(ModelObject &object);
+17 -26
View File
@@ -14,7 +14,7 @@
#include "Widgets/StateColor.hpp"
#include "Widgets/StaticLine.hpp"
#include "libslic3r/ColorDecomposeRecipe.hpp"
#include "libslic3r/MeshBoolean.hpp"
#include "libslic3r/MeshRepair.hpp"
#include "libslic3r/TriangleSelector.hpp"
#include <utility>
@@ -2436,25 +2436,25 @@ void TextureImportDialog::start_computation(bool auto_color, bool initial)
settings.target_colors_num = auto_color ? 0 : (size_t)m_param_color_count;
settings.smooth_weight = m_param_smooth / 10.0;
settings.mesh_repair_decision = m_mesh_repair_decision;
// BBS repairs the mesh through the Windows 3D SDK, which is only available on Windows
// builds that ship the SDK. Orca's CGAL-based repair (MeshBoolean::cgal::repair) works
// on all three platforms, so use that instead.
settings.mesh_repair_callback = [](const indexed_triangle_set& mesh,
indexed_triangle_set& repaired_mesh,
std::function<void(const char*, unsigned)> progress_callback,
std::function<bool()> cancel_callback,
std::string* error_message) -> bool {
if (cancel_callback && cancel_callback())
return false;
if (progress_callback)
progress_callback(_u8L("Repairing mesh").c_str(), 0);
struct Canceled {};
TriangleMesh tm(mesh);
if (!MeshBoolean::cgal::repair(tm, nullptr, error_message))
return false;
if (cancel_callback && cancel_callback())
try {
if (!repair_mesh(tm, nullptr, error_message, [&cancel_callback](int) {
if (cancel_callback && cancel_callback())
throw Canceled();
}))
return false;
} catch (const Canceled &) {
return false;
}
repaired_mesh = tm.its;
if (progress_callback)
progress_callback(_u8L("Repairing mesh").c_str(), 100);
@@ -2683,12 +2683,12 @@ void TextureImportDialog::on_mesh_repair_decision_required(wxCommandEvent&)
m_progress_dlg = nullptr;
}
#ifdef HAS_WIN10SDK
Slic3r::GUI::MessageDialog dlg(initial ? GetParent() : this,
_L("The mesh has non-manifold geometry or open boundaries. You can import it as-is or repair it with Windows 3D repair service before importing."),
_L("Mesh repair"), wxYES_NO | wxICON_WARNING | wxYES_DEFAULT);
_L("The mesh has non-manifold geometry or open boundaries. You can import it as-is or repair it before importing."),
_L("Mesh repair"), wxYES_NO | wxCANCEL | wxICON_WARNING | wxYES_DEFAULT);
dlg.SetButtonLabel(wxID_YES, _L("Import without repair"));
dlg.SetButtonLabel(wxID_NO, _L("Repair and import"), true);
dlg.SetButtonLabel(wxID_CANCEL, _L("Cancel"));
// "Repair and import" is the recommended action here, so the accent moves off the default YES
// button onto NO. MsgDialog::add_button already styled both as ButtonType::Choice, so restyling
// with the same type swaps only the palette and leaves the geometry alone.
@@ -2704,17 +2704,7 @@ void TextureImportDialog::on_mesh_repair_decision_required(wxCommandEvent&)
dlg.Fit();
dlg.CenterOnParent();
int ret = dlg.ShowModal();
m_mesh_repair_decision = (ret == wxID_NO)
? Slic3r::TexturePaintingSettings::MeshRepairDecision::RepairAndImport
: Slic3r::TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair;
#else
Slic3r::GUI::MessageDialog dlg(initial ? GetParent() : this,
_L("Please note that the mesh has non-manifold geometry or open boundaries."),
_L("Mesh issue"), wxOK | wxCANCEL | wxICON_WARNING | wxOK_DEFAULT);
dlg.SetButtonLabel(wxID_OK, _L("Continue"), true);
dlg.SetButtonLabel(wxID_CANCEL, _L("Cancel"));
int ret = dlg.ShowModal();
if (ret != wxID_OK) {
if (ret == wxID_CANCEL) {
m_cancel_flag = true;
if (initial) {
m_initial_computation_cancelled = true;
@@ -2727,8 +2717,9 @@ void TextureImportDialog::on_mesh_repair_decision_required(wxCommandEvent&)
}
return;
}
m_mesh_repair_decision = Slic3r::TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair;
#endif
m_mesh_repair_decision = (ret == wxID_NO)
? Slic3r::TexturePaintingSettings::MeshRepairDecision::RepairAndImport
: Slic3r::TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair;
start_computation(auto_color, initial);
}
+60 -185
View File
@@ -3,240 +3,115 @@
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cmath>
#include <condition_variable>
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/libslic3r.h"
#include <exception>
#include <cstddef>
#include "libslic3r/Exception.hpp"
#include <limits>
#include <exception>
#include <mutex>
#include "slic3r/GUI/Widgets/ProgressDialog.hpp"
#include <optional>
#include <string>
#include <thread>
#include <utility>
#include <vector>
#include <wx/string.h>
#include "libslic3r/MeshBoolean.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Format/bbs_3mf.hpp"
#include "libslic3r/format.hpp"
#include "libslic3r/MeshRepair.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Thread.hpp"
#include "../GUI/I18N.hpp"
#include "libslic3r/TriangleSelector.hpp"
// Orca: This file provides utilities for repairing 3D model meshes using the CGAL library, handling mesh splitting, merging, and boolean operations.
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/Widgets/ProgressDialog.hpp"
namespace Slic3r {
namespace {
// Orca: Helper functions for analyzing mesh properties and transformations.
bool is_not_3dimensional_part(const TriangleMesh &mesh)
class RepairCanceledException : public std::exception
{
// Orca: Determines if a mesh is degenerate or represents a non-3dimensional part by checking volume and bounding box dimensions.
if (mesh.its.indices.empty())
return true;
indexed_triangle_set tmp = mesh.its;
its_remove_degenerate_faces(tmp, true);
if (tmp.indices.empty())
return true;
const BoundingBoxf3 bbox = mesh.bounding_box();
const Vec3d size = bbox.size();
const double min_dim = std::min(size.x(), std::min(size.y(), size.z()));
const double max_dim = std::max(size.x(), std::max(size.y(), size.z()));
if (min_dim <= EPSILON)
return true;
const double volume = std::abs(its_volume(mesh.its));
const double bbox_volume = size.x() * size.y() * size.z();
if (volume <= EPSILON)
return true;
const double min_relative_thickness = 1e-6;
const double min_volume_ratio = 1e-6;
if (min_dim / max_dim <= min_relative_thickness)
return true;
if (bbox_volume > 0.0 && volume / bbox_volume <= min_volume_ratio)
return true;
return false;
}
public:
const char *what() const noexcept override { return "Model repair has been canceled"; }
};
} // namespace
// Orca: Exception class for handling user-initiated cancellation of model repair operations.
class RepairCanceledException : public std::exception {
public:
const char* what() const noexcept override { return "Model repair has been canceled"; }
};
// Orca: Main function to repair model objects using CGAL, with progress dialog and cancellation support.
// Returns false if fixing was canceled. fix_result contains error message if failed.
bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::ProgressDialog &progress_dialog, const wxString &msg_header, std::string &fix_result, bool keep_painting)
bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::ProgressDialog &progress_dialog, const wxString &msg_header, std::string &fix_result, bool keep_painting,
bool *modified)
{
// Hold SaveObjectGaurd to prevent backup manager from racing concurrent mesh mutations (use-after-free).
// Keeps the backup manager from reading a mesh while the worker replaces it.
SaveObjectGaurd backup_gaurd(model_object);
// Orca: Synchronization primitives for progress updates between worker thread and GUI.
std::mutex mtx;
std::condition_variable condition;
struct Progress {
std::string message;
int percent = 0;
bool updated = false;
} progress;
std::vector<ModelVolume *> volumes;
if (volume_idx < 0)
volumes = model_object.volumes;
else if (std::size_t(volume_idx) < model_object.volumes.size())
volumes.push_back(model_object.volumes[volume_idx]);
std::mutex mtx;
std::condition_variable condition;
struct Progress
{
std::string message;
int percent = 0;
bool updated = false;
} progress;
std::atomic<bool> canceled = false;
std::atomic<bool> finished = false;
bool any_modified = false;
std::string errors;
bool success = false;
size_t ivolume = 0;
// Orca: Lambda for updating progress from worker thread.
auto on_progress = [&mtx, &condition, &ivolume, &model_object, &progress](const std::string &msg, unsigned prcnt) {
std::unique_lock<std::mutex> lock(mtx);
auto on_progress = [&mtx, &condition, &progress](const std::string &msg, int percent) {
std::lock_guard<std::mutex> lock(mtx);
progress.message = msg;
const size_t total = std::max<size_t>(1, model_object.volumes.size());
progress.percent = int(std::floor((float(prcnt) + float(ivolume) * 100.f) / float(total)));
progress.percent = percent;
progress.updated = true;
condition.notify_all();
};
// Orca: Worker thread that performs the actual model repair operations.
auto worker_thread = std::thread([&model_object, volume_idx, &ivolume, on_progress, &success, &canceled, &finished, &fix_result, keep_painting]() {
auto worker_thread = std::thread([&]() {
set_current_thread_name("cgal_fix_model");
try {
set_current_thread_name("cgal_fix_model");
for (std::size_t i = 0; i < volumes.size(); ++i) {
ModelVolume *volume = volumes[i];
const std::string name = volumes.size() > 1 ? volume->name : std::string();
const auto report = [&](int percent) {
if (canceled)
throw RepairCanceledException();
on_progress(name, int((100 * i + std::size_t(percent)) / volumes.size()));
};
report(0);
size_t start_volume = volume_idx == -1 ? 0 : size_t(volume_idx);
size_t end_volume = volume_idx == -1 ? std::numeric_limits<size_t>::max() : size_t(volume_idx);
for (ivolume = start_volume; ivolume < model_object.volumes.size(); ++ivolume) {
if (volume_idx != -1 && ivolume > end_volume)
break;
if (canceled)
throw RepairCanceledException();
on_progress(_u8L("Repairing model object"), 10);
ModelVolume *volume = model_object.volumes[ivolume];
// Orca: Split splittable volumes into parts for individual processing.
size_t parts_count = 1;
if (volume->is_splittable()) {
parts_count = volume->split(1, keep_painting);
if (parts_count > 1) {
const std::string msg = Slic3r::format(L("Split into %1% parts"), parts_count);
on_progress(msg, 10);
}
}
size_t part_end = std::min(ivolume + parts_count - 1, model_object.volumes.size() - 1);
if (volume_idx != -1)
end_volume = part_end;
size_t removed_parts = 0;
for (size_t idx = part_end + 1; idx > ivolume; --idx) {
const size_t part_idx = idx - 1;
const ModelVolume *part_volume = model_object.volumes[part_idx];
if (!is_not_3dimensional_part(part_volume->mesh()))
continue;
model_object.delete_volume(part_idx);
++removed_parts;
if (part_end > 0)
--part_end;
else
part_end = 0;
if (volume_idx != -1)
end_volume = part_end;
}
if (removed_parts >= parts_count) {
ivolume = part_end;
on_progress(_u8L("Repair finished"), 100);
continue;
}
for (size_t part_idx = ivolume; part_idx <= part_end && part_idx < model_object.volumes.size(); ++part_idx) {
ModelVolume *part_volume = model_object.volumes[part_idx];
TriangleMesh mesh = part_volume->mesh();
if (its_num_open_edges(mesh.its) != 0) {
// Save painting for later remap
const std::optional<TriangleSelector::SavedPainting> saved_painting = keep_painting ?
part_volume->save_painting() :
std::optional<TriangleSelector::SavedPainting>{};
std::string error;
if (!MeshBoolean::cgal::repair(mesh, nullptr, &error))
throw Slic3r::RuntimeError(error.empty() ? _u8L("Repair failed") : error);
part_volume->set_mesh(std::move(mesh));
part_volume->calculate_convex_hull();
part_volume->invalidate_convex_hull_2d();
part_volume->set_new_unique_id();
// Remap paint back
part_volume->restore_painting(saved_painting);
}
}
ivolume = part_end;
on_progress(_u8L("Repair finished"), 100);
MeshRepairStats stats;
std::string error;
if (!volume->repair(keep_painting, &stats, &error, report))
errors += (errors.empty() ? "" : "\n") + (volumes.size() > 1 ? volume->name + ": " : std::string()) + error;
any_modified = any_modified || stats.modified;
}
model_object.invalidate_bounding_box();
if (ivolume > 0)
--ivolume;
on_progress(_u8L("Repair finished"), 100);
success = true;
finished = true;
} catch (RepairCanceledException &) {
} catch (const RepairCanceledException &) {
canceled = true;
finished = true;
on_progress(_u8L("Repair canceled"), 100);
} catch (std::exception &ex) {
success = false;
finished = true;
fix_result = ex.what();
on_progress(ex.what(), 100);
} catch (const std::exception &ex) {
errors += (errors.empty() ? "" : "\n") + std::string(ex.what());
}
// Volumes repaired before a cancel or an error keep their new mesh.
model_object.invalidate_bounding_box();
finished = true;
on_progress(_u8L("Repair finished"), 100);
});
// Orca: Main GUI loop to update progress dialog and handle cancellation.
while (!finished) {
std::unique_lock<std::mutex> lock(mtx);
condition.wait_for(lock, std::chrono::milliseconds(250), [&progress]{ return progress.updated; });
// Decrease progress percent slightly to avoid auto-closing.
if (!progress_dialog.Update(progress.percent - 1, msg_header + _(progress.message)))
condition.wait_for(lock, std::chrono::milliseconds(250), [&progress] { return progress.updated; });
// One percent short of the maximum, so that the dialog does not close itself.
if (!progress_dialog.Update(std::max(0, progress.percent - 1), msg_header + wxString::FromUTF8(progress.message)))
canceled = true;
else
progress_dialog.Fit();
progress.updated = false;
}
if (canceled) {
// Nothing to show.
} else if (success) {
fix_result.clear();
}
if (worker_thread.joinable())
worker_thread.join();
fix_result = canceled ? std::string() : errors;
if (modified)
*modified = any_modified;
return !canceled;
}
+3 -2
View File
@@ -12,8 +12,9 @@ class Model;
class ModelObject;
class Print;
// Return false if fixing was canceled. fix_result is empty on success.
extern bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::ProgressDialog &progress_dlg, const wxString &msg_header, std::string &fix_result, bool keep_painting);
// Return false if fixing was canceled. fix_result is empty on success; modified tells whether any mesh changed.
extern bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::ProgressDialog &progress_dlg, const wxString &msg_header, std::string &fix_result, bool keep_painting,
bool *modified = nullptr);
} // namespace Slic3r
@@ -0,0 +1,35 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION((''),'2;1');
FILE_NAME('surface_model_geometric_set','2026-10-12T00:00:00',(''),(''),'','','');
FILE_SCHEMA(('CONFIG_CONTROL_DESIGN'));
ENDSEC;
DATA;
#1=APPLICATION_CONTEXT('configuration controlled 3d designs of mechanical parts and assemblies');
#2=APPLICATION_PROTOCOL_DEFINITION('international standard','config_control_design',1994,#1);
#3=MECHANICAL_CONTEXT('',#1,'mechanical');
#4=PRODUCT('surfaces','surfaces','',(#3));
#5=PRODUCT_DEFINITION_FORMATION_WITH_SPECIFIED_SOURCE('','',#4,.NOT_KNOWN.);
#6=DESIGN_CONTEXT('',#1,'design');
#7=PRODUCT_DEFINITION('','',#5,#6);
#8=PRODUCT_DEFINITION_SHAPE('','',#7);
#9=SHAPE_DEFINITION_REPRESENTATION(#8,#20);
#10=(LENGTH_UNIT()NAMED_UNIT(*)SI_UNIT(.MILLI.,.METRE.));
#11=(NAMED_UNIT(*)PLANE_ANGLE_UNIT()SI_UNIT($,.RADIAN.));
#12=(NAMED_UNIT(*)SI_UNIT($,.STERADIAN.)SOLID_ANGLE_UNIT());
#13=UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-6),#10,'distance_accuracy_value','');
#14=(GEOMETRIC_REPRESENTATION_CONTEXT(3)GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#13))GLOBAL_UNIT_ASSIGNED_CONTEXT((#10,#11,#12))REPRESENTATION_CONTEXT('',''));
#15=CARTESIAN_POINT('',(0.,0.,0.));
#16=DIRECTION('',(0.,0.,1.));
#17=DIRECTION('',(1.,0.,0.));
#18=AXIS2_PLACEMENT_3D('',#15,#16,#17);
#19=GEOMETRIC_SET('',(#30,#40));
#20=GEOMETRICALLY_BOUNDED_SURFACE_SHAPE_REPRESENTATION('',(#18,#19),#14);
#30=RECTANGULAR_TRIMMED_SURFACE('',#31,0.,10.,0.,10.,.T.,.T.);
#31=PLANE('',#18);
#40=RECTANGULAR_TRIMMED_SURFACE('',#41,0.,10.,0.,10.,.T.,.T.);
#41=PLANE('',#42);
#42=AXIS2_PLACEMENT_3D('',#43,#16,#17);
#43=CARTESIAN_POINT('',(0.,0.,10.));
ENDSEC;
END-ISO-10303-21;
+1
View File
@@ -53,6 +53,7 @@ add_executable(${_TEST_NAME}_tests
test_stl.cpp
test_triangle_selector.cpp
test_meshboolean.cpp
test_mesh_repair.cpp
test_trianglemesh_slicer.cpp
test_marchingsquares.cpp
test_lay_on_face.cpp
+255
View File
@@ -0,0 +1,255 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <string>
#include <utility>
#include <vector>
#include <libslic3r/MeshBoolean.hpp>
#include <libslic3r/MeshRepair.hpp>
#include <libslic3r/Point.hpp>
#include <libslic3r/libslic3r.h>
#include <libslic3r/TriangleMesh.hpp>
using namespace Slic3r;
using Catch::Matchers::WithinRel;
namespace {
indexed_triangle_set cube(double size, const Vec3d &offset = Vec3d::Zero())
{
TriangleMesh mesh = make_cube(size, size, size);
mesh.translate(offset.cast<float>());
return mesh.its;
}
indexed_triangle_set merged(const std::vector<indexed_triangle_set> &parts)
{
indexed_triangle_set out;
for (const indexed_triangle_set &part : parts)
its_merge(out, part);
return out;
}
// Every triangle with its own three vertices, as some exporters write meshes.
indexed_triangle_set soup(const indexed_triangle_set &its)
{
indexed_triangle_set out;
for (const stl_triangle_vertex_indices &f : its.indices) {
const int first = int(out.vertices.size());
for (int k = 0; k < 3; ++k)
out.vertices.push_back(its.vertices[f[k]]);
out.indices.emplace_back(first, first + 1, first + 2);
}
return out;
}
double regular_polygon_area(int sides, double radius) { return 0.5 * sides * radius * radius * std::sin(2. * PI / sides); }
// Closed by vertex indices, outward oriented and free of self-intersections.
void check_solid(const TriangleMesh &mesh, double volume)
{
CHECK(its_num_open_edges(mesh.its) == 0);
CHECK_THAT(its_volume(mesh.its), WithinRel(volume, 1e-4));
CHECK_FALSE(MeshBoolean::cgal::does_self_intersect(mesh));
}
} // namespace
TEST_CASE("A triangle soup repairs into the solid it describes", "[MeshRepair]")
{
TriangleMesh mesh(soup(cube(20.)));
MeshRepairStats stats;
REQUIRE(repair_mesh(mesh, &stats));
CHECK(stats.modified);
check_solid(mesh, 20. * 20. * 20.);
CHECK(mesh.its.vertices.size() == 8);
}
TEST_CASE("Flipped triangles take the orientation of their neighbours", "[MeshRepair]")
{
indexed_triangle_set its = cube(20.);
std::swap(its.indices[1][1], its.indices[1][2]);
std::swap(its.indices[5][1], its.indices[5][2]);
TriangleMesh mesh(std::move(its));
REQUIRE(repair_mesh(mesh));
check_solid(mesh, 20. * 20. * 20.);
}
TEST_CASE("An inside-out mesh is turned outward", "[MeshRepair]")
{
indexed_triangle_set its = cube(20.);
its_flip_triangles(its);
TriangleMesh mesh(std::move(its));
REQUIRE(repair_mesh(mesh));
check_solid(mesh, 20. * 20. * 20.);
}
TEST_CASE("Holes are filled", "[MeshRepair]")
{
indexed_triangle_set its = cube(20.);
its.indices.erase(its.indices.begin(), its.indices.begin() + 3);
TriangleMesh mesh(std::move(its));
MeshRepairStats stats;
REQUIRE(repair_mesh(mesh, &stats));
CHECK(stats.filled_holes > 0);
check_solid(mesh, 20. * 20. * 20.);
}
TEST_CASE("Cracks between triangles that should share corners are closed", "[MeshRepair]")
{
indexed_triangle_set its = soup(cube(20.));
// Shift the corners of two triangles by far less than a micron, as float rounding in an exporter would.
for (std::size_t v = 0; v < 6; ++v)
its.vertices[v] += Vec3f(1e-5f, -1e-5f, 1e-5f);
TriangleMesh mesh(std::move(its));
MeshRepairStats stats;
REQUIRE(repair_mesh(mesh, &stats));
CHECK(stats.welded_vertices > 0);
check_solid(mesh, 20. * 20. * 20.);
}
TEST_CASE("A T-junction is closed", "[MeshRepair]")
{
// The top of the cube is split at the middle of its front edge, which the front side does not share.
indexed_triangle_set its;
its.vertices = {{0, 0, 0}, {20, 0, 0}, {20, 20, 0}, {0, 20, 0}, {0, 0, 20}, {20, 0, 20}, {20, 20, 20}, {0, 20, 20}, {10, 0, 20}};
its.indices = {{0, 2, 1}, {0, 3, 2}, {4, 8, 7}, {8, 6, 7}, {8, 5, 6}, {0, 1, 5}, {0, 5, 4},
{1, 2, 6}, {1, 6, 5}, {2, 3, 7}, {2, 7, 6}, {3, 0, 4}, {3, 4, 7}};
TriangleMesh mesh(std::move(its));
REQUIRE(repair_mesh(mesh));
check_solid(mesh, 20. * 20. * 20.);
}
TEST_CASE("Crossing shells stay separate parts and are made solid", "[MeshRepair]")
{
indexed_triangle_set inside_out = cube(20., {10., 10., 10.});
its_flip_triangles(inside_out);
TriangleMesh mesh(merged({cube(20.), inside_out}));
MeshRepairStats stats;
REQUIRE(repair_mesh(mesh, &stats));
CHECK(stats.reversed_shells == 1);
CHECK(its_num_open_edges(mesh.its) == 0);
CHECK_THAT(its_volume(mesh.its), WithinRel(2 * 8000., 1e-4));
CHECK(mesh.split().size() == 2);
}
TEST_CASE("Solids touching face to face in a soup lose the wall between them", "[MeshRepair]")
{
// The mirrored copy shares the wall at x = 10 with exactly the same triangles, reversed.
TriangleMesh right = make_cube(10., 10., 10.);
right.mirror(X);
right.translate(20.f, 0.f, 0.f);
TriangleMesh mesh(soup(merged({cube(10.), right.its})));
REQUIRE(repair_mesh(mesh));
check_solid(mesh, 2 * 1000.);
CHECK(mesh.split().size() == 1);
}
TEST_CASE("A shell inside a solid follows the nonzero winding rule of slicing", "[MeshRepair]")
{
SECTION("Oriented like the outer shell, it adds nothing and is removed")
{
TriangleMesh mesh(merged({cube(20.), cube(10., {5., 5., 5.})}));
REQUIRE(repair_mesh(mesh));
check_solid(mesh, 8000.);
CHECK(mesh.its.indices.size() == 12);
}
SECTION("Oriented inward, it is a cavity and stays")
{
indexed_triangle_set cavity = cube(10., {5., 5., 5.});
its_flip_triangles(cavity);
TriangleMesh mesh(merged({cube(20.), cavity}));
MeshRepairStats stats;
REQUIRE(repair_mesh(mesh, &stats));
CHECK_FALSE(stats.modified);
CHECK_THAT(its_volume(mesh.its), WithinRel(8000. - 1000., 1e-4));
}
}
TEST_CASE("A pipe missing its end caps gets annular caps", "[MeshRepair]")
{
const int n = 64;
const double h = 20., r_out = 10., r_in = 7.;
indexed_triangle_set its;
for (const double r : {r_out, r_in})
for (int k = 0; k < n; ++k) {
const double a = 2. * PI * k / n;
its.vertices.emplace_back(float(r * std::cos(a)), float(r * std::sin(a)), 0.f);
its.vertices.emplace_back(float(r * std::cos(a)), float(r * std::sin(a)), float(h));
}
for (int k = 0; k < n; ++k) {
const int a0 = 2 * k, b0 = a0 + 1, a1 = 2 * ((k + 1) % n), b1 = a1 + 1;
its.indices.emplace_back(a0, a1, b1);
its.indices.emplace_back(a0, b1, b0);
its.indices.emplace_back(2 * n + a0, 2 * n + b1, 2 * n + a1);
its.indices.emplace_back(2 * n + a0, 2 * n + b0, 2 * n + b1);
}
TriangleMesh mesh(std::move(its));
REQUIRE(repair_mesh(mesh));
check_solid(mesh, h * (regular_polygon_area(n, r_out) - regular_polygon_area(n, r_in)));
}
TEST_CASE("A stray flat triangle is dropped", "[MeshRepair]")
{
indexed_triangle_set its = cube(20.);
its.vertices.insert(its.vertices.end(), {{30, 0, 0}, {40, 0, 0}, {30, 10, 0}});
its.indices.emplace_back(8, 9, 10);
TriangleMesh mesh(std::move(its));
MeshRepairStats stats;
REQUIRE(repair_mesh(mesh, &stats));
CHECK(stats.removed_shells == 1);
check_solid(mesh, 8000.);
}
TEST_CASE("A two-sided export keeps one side of each triangle", "[MeshRepair]")
{
indexed_triangle_set back = cube(20.);
its_flip_triangles(back);
TriangleMesh mesh(soup(merged({cube(20.), back})));
REQUIRE(repair_mesh(mesh));
check_solid(mesh, 8000.);
}
TEST_CASE("A valid mesh is left untouched", "[MeshRepair]")
{
TriangleMesh mesh = make_sphere(10., 2. * PI / 36.);
const TriangleMesh original = mesh;
MeshRepairStats stats;
REQUIRE(repair_mesh(mesh, &stats));
CHECK_FALSE(stats.modified);
CHECK(mesh.its.vertices == original.its.vertices);
CHECK(mesh.its.indices == original.its.indices);
}
TEST_CASE("A mesh enclosing no volume is reported and left untouched", "[MeshRepair]")
{
indexed_triangle_set its;
its.vertices = {{0, 0, 0}, {10, 0, 0}, {10, 10, 0}, {0, 10, 0}};
its.indices = {{0, 1, 2}, {0, 2, 3}};
TriangleMesh mesh(its);
std::string error;
CHECK_FALSE(repair_mesh(mesh, nullptr, &error));
CHECK_FALSE(error.empty());
CHECK(mesh.its.indices.size() == 2);
}
TEST_CASE("Progress is reported in order and throwing from it cancels the repair", "[MeshRepair]")
{
TriangleMesh mesh(soup(cube(20.)));
std::vector<int> reported;
REQUIRE(repair_mesh(mesh, nullptr, nullptr, [&reported](int percent) { reported.push_back(percent); }));
CHECK_FALSE(reported.empty());
CHECK(std::is_sorted(reported.begin(), reported.end()));
struct Canceled {};
TriangleMesh canceled(soup(cube(20.)));
const TriangleMesh original = canceled;
CHECK_THROWS_AS(repair_mesh(canceled, nullptr, nullptr, [](int) { throw Canceled(); }), Canceled);
CHECK(canceled.its.vertices == original.its.vertices);
CHECK(canceled.its.indices == original.its.indices);
}
+103
View File
@@ -12,7 +12,9 @@
#include <catch2/generators/catch_generators.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "libslic3r/MeshRepair.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/ObjectID.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/libslic3r.h"
@@ -97,3 +99,104 @@ TEST_CASE("An instance added from another's scale, rotation and mirror matches i
original->get_rotation(), original->get_mirror());
CHECK(copy->get_matrix().linear().isApprox(original->get_matrix().linear(), 1e-9));
}
namespace {
// A flat triangle first, then a cube for each offset, all in one mesh.
TriangleMesh flat_triangle_then_cubes(const std::vector<double> &cube_offsets)
{
indexed_triangle_set its;
its.vertices = {{-20.f, 0.f, 0.f}, {-10.f, 0.f, 0.f}, {-15.f, 5.f, 0.f}};
its.indices = {{0, 1, 2}};
for (double offset : cube_offsets) {
TriangleMesh cube = make_cube(10, 10, 10);
cube.translate(float(offset), 0.f, 0.f);
its_merge(its, cube.its);
}
return TriangleMesh(its);
}
} // namespace
TEST_CASE("Splitting a part drops pieces enclosing no volume and keeps the part itself", "[Model]")
{
Model model;
ModelObject *object = model.add_object();
ModelVolume *volume = object->add_volume(flat_triangle_then_cubes({0., 30.}), ModelVolumeType::MODEL_PART, false);
REQUIRE(volume->is_splittable());
CHECK(volume->split(1, false) == 2);
REQUIRE(object->volumes.size() == 2);
CHECK(std::find(object->volumes.begin(), object->volumes.end(), volume) != object->volumes.end());
for (const ModelVolume *part : object->volumes) {
CHECK(part->mesh().facets_count() == 12);
CHECK_FALSE(part->is_splittable());
}
}
TEST_CASE("Splitting a part with a single solid piece keeps just that piece", "[Model]")
{
Model model;
ModelObject *object = model.add_object();
ModelVolume *volume = object->add_volume(flat_triangle_then_cubes({0.}), ModelVolumeType::MODEL_PART, false);
CHECK(volume->split(1, false) == 1);
REQUIRE(object->volumes.size() == 1);
CHECK(object->volumes.front() == volume);
CHECK(volume->mesh().facets_count() == 12);
}
TEST_CASE("Splitting a part made only of pieces enclosing no volume leaves it whole", "[Model]")
{
Model model;
ModelObject *object = model.add_object();
indexed_triangle_set its;
its.vertices = {{0.f, 0.f, 0.f}, {10.f, 0.f, 0.f}, {5.f, 5.f, 0.f}, {20.f, 0.f, 0.f}, {30.f, 0.f, 0.f}, {25.f, 5.f, 0.f}};
its.indices = {{0, 1, 2}, {3, 4, 5}};
ModelVolume *volume = object->add_volume(TriangleMesh(its), ModelVolumeType::MODEL_PART, false);
CHECK(volume->split(1, false) == 1);
REQUIRE(object->volumes.size() == 1);
CHECK(object->volumes.front() == volume);
CHECK(volume->mesh().facets_count() == 2);
}
TEST_CASE("Replacing a part's mesh updates whether it can be split", "[Model]")
{
Model model;
ModelObject *object = model.add_object();
TriangleMesh two_cubes = make_cube(10, 10, 10);
TriangleMesh second = make_cube(10, 10, 10);
second.translate(30.f, 0.f, 0.f);
two_cubes.merge(second);
ModelVolume *volume = object->add_volume(std::move(two_cubes), ModelVolumeType::MODEL_PART, false);
REQUIRE(volume->is_splittable());
volume->set_mesh(make_cube(10, 10, 10));
CHECK_FALSE(volume->is_splittable());
}
TEST_CASE("Repairing a part replaces its mesh only when the repair changes it", "[Model]")
{
Model model;
ModelObject *object = model.add_object();
TriangleMesh flipped = make_cube(10, 10, 10);
std::swap(flipped.its.indices.front()[1], flipped.its.indices.front()[2]);
ModelVolume *broken = object->add_volume(std::move(flipped), ModelVolumeType::MODEL_PART, false);
TriangleMesh cube = make_cube(10, 10, 10);
cube.translate(30.f, 0.f, 0.f);
ModelVolume *valid = object->add_volume(std::move(cube), ModelVolumeType::MODEL_PART, false);
const ObjectID broken_id = broken->id();
const ObjectID valid_id = valid->id();
MeshRepairStats stats;
REQUIRE(broken->repair(false, &stats));
CHECK(stats.modified);
CHECK(broken->id() != broken_id);
CHECK_THAT(its_volume(broken->mesh().its), Catch::Matchers::WithinRel(1000., 1e-6));
CHECK_FALSE(broken->get_convex_hull().empty());
REQUIRE(valid->repair(false, &stats));
CHECK_FALSE(stats.modified);
CHECK(valid->id() == valid_id);
}
+21
View File
@@ -4,6 +4,7 @@
#include <string>
#include <ios>
#include <catch2/catch_message.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include "libslic3r/Model.hpp"
@@ -91,6 +92,26 @@ TEST_CASE("A cone with a slightly tilted seam imports as a closed mesh", "[Step]
CHECK(its_num_open_edges(model.objects.front()->volumes.front()->mesh().its) == 0);
}
// The fixture holds two trimmed planes in a geometric set and no shells or solids, as some CAD exporters write
// surface models, so OCCT reads it as a compound of loose faces.
TEST_CASE("A surface model imports whether or not compounds are split", "[Step]")
{
const std::string path = std::string(TEST_DATA_DIR) + PATH_SEPARATOR "surface_model_geometric_set.step";
const bool split = GENERATE(false, true);
CAPTURE(split);
Model model;
bool cancel = false;
Step step(path);
REQUIRE(step.load() == Step::Step_Status::LOAD_SUCCESS);
REQUIRE(step.mesh(&model, cancel, split) == Step::Step_Status::MESH_SUCCESS);
REQUIRE(model.objects.size() == 1);
REQUIRE(model.objects.front()->volumes.size() == 1);
CHECK(model.objects.front()->volumes.front()->mesh().facets_count() > 0);
}
TEST_CASE("isUtf8 recognises two, three and four byte sequences", "[Step]")
{
CHECK(StepPreProcessor::isUtf8("\xC3\xA9")); // U+00E9