mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-13 19:01:21 +00:00
* 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.
153 lines
6.0 KiB
C++
153 lines
6.0 KiB
C++
#include <catch2/catch_all.hpp>
|
|
|
|
#include <boost/nowide/fstream.hpp>
|
|
#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"
|
|
#include "libslic3r/Format/STEP.hpp"
|
|
#include "libslic3r/TriangleMesh.hpp"
|
|
#include "test_utils.hpp"
|
|
|
|
using namespace Slic3r;
|
|
|
|
static void write_step_line(const std::string &path, const std::string &line)
|
|
{
|
|
boost::nowide::ofstream file(path, std::ios::binary);
|
|
file << "ISO-10303-21;\n" << line << "\nEND-ISO-10303-21;\n";
|
|
}
|
|
|
|
// preprocess() hands back the input path unless it transcoded into a temporary.
|
|
static std::string preprocess_result(const std::string &line)
|
|
{
|
|
ScopedSlic3rTemporaryDir scratch;
|
|
|
|
ScopedTemporaryFile step(".step");
|
|
write_step_line(step.string(), line);
|
|
|
|
std::string output_path;
|
|
StepPreProcessor preprocessor;
|
|
REQUIRE(preprocessor.preprocess(step.string().c_str(), output_path));
|
|
|
|
return output_path == step.string() ? "untouched" : "transcoded";
|
|
}
|
|
|
|
// data/utf8_part_names.step is three boxes written by OCCT's own STEP writer, whose
|
|
// PRODUCT names were then patched to raw UTF-8. Most CAD exporters write non-ASCII names
|
|
// that way rather than in the \X2\ escape form. The third part is ASCII, as a control.
|
|
TEST_CASE("Part names with multi-byte UTF-8 survive import", "[Step]")
|
|
{
|
|
// getNamedSolids() replaces a name that isUtf8() rejects with a running number.
|
|
const std::string path = TEST_DATA_DIR PATH_SEPARATOR "utf8_part_names.step";
|
|
|
|
Model model;
|
|
bool cancel = false;
|
|
Step step(path); // no isUtf8Fn, matching how Model::read_from_step builds it
|
|
|
|
REQUIRE(step.load() == Step::Step_Status::LOAD_SUCCESS);
|
|
REQUIRE(step.mesh(&model, cancel, false) == Step::Step_Status::MESH_SUCCESS);
|
|
|
|
REQUIRE(model.objects.size() == 1);
|
|
const ModelObject *object = model.objects.front();
|
|
REQUIRE(object->volumes.size() == 3);
|
|
// "ce" is split off, or the hex escape would swallow it as further hex digits.
|
|
CHECK(object->volumes[0]->name == "pi\xC3\xA8" "ce");
|
|
CHECK(object->volumes[1]->name == "Geh\xC3\xA4use");
|
|
CHECK(object->volumes[2]->name == "bracket");
|
|
}
|
|
|
|
TEST_CASE("A security classification assignment does not crash import", "[Step]")
|
|
{
|
|
const std::string path = std::string(TEST_DATA_DIR) + PATH_SEPARATOR "security_classification.step";
|
|
|
|
Model model;
|
|
bool cancel = false;
|
|
Step step(path);
|
|
|
|
REQUIRE(step.load() == Step::Step_Status::LOAD_SUCCESS);
|
|
REQUIRE(step.mesh(&model, cancel, false) == 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() == 4); // a tetrahedron
|
|
}
|
|
|
|
// The fixture is a truncated cone whose seam pcurves have the direction (2.1e-16, -1).
|
|
TEST_CASE("A cone with a slightly tilted seam imports as a closed mesh", "[Step]")
|
|
{
|
|
const std::string path = std::string(TEST_DATA_DIR) + PATH_SEPARATOR "cone_tilted_seam_pcurve.step";
|
|
|
|
Model model;
|
|
bool cancel = false;
|
|
Step step(path);
|
|
|
|
REQUIRE(step.load() == Step::Step_Status::LOAD_SUCCESS);
|
|
REQUIRE(step.mesh(&model, cancel, false) == Step::Step_Status::MESH_SUCCESS);
|
|
|
|
REQUIRE(model.objects.size() == 1);
|
|
REQUIRE(model.objects.front()->volumes.size() == 1);
|
|
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
|
|
CHECK(StepPreProcessor::isUtf8("\xE4\xB8\xAD")); // U+4E2D
|
|
CHECK(StepPreProcessor::isUtf8("\xF0\x9F\x94\xA9")); // U+1F529
|
|
CHECK_FALSE(StepPreProcessor::isUtf8("\x81\x30")); // 0x81 is not a lead byte
|
|
CHECK_FALSE(StepPreProcessor::isUtf8("\xC3")); // truncated sequence
|
|
}
|
|
|
|
// The only caller of isGBK is preprocess(), which nothing calls today.
|
|
TEST_CASE("Encoding detection decides whether a step file is transcoded", "[Step]")
|
|
{
|
|
SECTION("UTF-8, so left alone")
|
|
{
|
|
// A two byte sequence also satisfies every GBK range, so misdetecting it as
|
|
// not-UTF-8 sends it to be transcoded.
|
|
const std::string sequence = GENERATE(std::string("\xC3\xA9"), // U+00E9
|
|
std::string("\xE4\xB8\xAD"), // U+4E2D
|
|
std::string("\xF0\x9F\x94\xA9")); // U+1F529
|
|
|
|
CHECK(preprocess_result("NAME('" + sequence + "');") == "untouched");
|
|
}
|
|
|
|
SECTION("neither UTF-8 nor GBK, so left alone")
|
|
{
|
|
// 0x81 is not a UTF-8 lead byte, and 0x30 is below the 0x40 floor for a GBK trail.
|
|
CHECK(preprocess_result("NAME('\x81\x30');") == "untouched");
|
|
}
|
|
|
|
SECTION("GBK, so transcoded")
|
|
{
|
|
// U+554A in GBK, whose lead byte is not valid UTF-8. Pins the other direction,
|
|
// since a detector that never reports GBK would pass every case above.
|
|
CHECK(preprocess_result("NAME('\xB0\xA1');") == "transcoded");
|
|
}
|
|
|
|
SECTION("plain ASCII, so left alone") { CHECK(preprocess_result("NAME('bracket');") == "untouched"); }
|
|
}
|