#include #include "libslic3r/TriangleSelector.hpp" #include "libslic3r/TriangleMesh.hpp" #include using namespace Slic3r; // A sphere gives well over ExtruderMax original facets, so every extruder state can be assigned // to a facet of its own without any splitting getting in the way. static TriangleMesh test_mesh() { return make_sphere(5., 2 * PI / 24); } // Read the nibble_idx-th 4-bit group of a serialized bitstream, least significant bit first. static int nibble_at(const std::vector &bitstream, size_t nibble_idx) { int n = 0; for (size_t bit = 0; bit < 4; ++bit) n |= int(bitstream[nibble_idx * 4 + bit]) << bit; return n; } TEST_CASE("Every extruder state survives a serialize/deserialize round trip", "[TriangleSelector]") { const TriangleMesh mesh = test_mesh(); const int max_state = int(EnforcerBlockerType::ExtruderMax); REQUIRE(int(mesh.its.indices.size()) >= max_state); TriangleSelector selector(mesh); for (int state = 1; state <= max_state; ++state) selector.set_facet(state - 1, EnforcerBlockerType(state)); TriangleSelector restored(mesh); restored.deserialize(selector.serialize()); for (int state = 1; state <= max_state; ++state) { INFO("Extruder " << state); REQUIRE(restored.has_facets(EnforcerBlockerType(state))); REQUIRE(restored.num_facets(EnforcerBlockerType(state)) == 1); } } TEST_CASE("Serialized data reports the extruder states it uses", "[TriangleSelector]") { const TriangleMesh mesh = test_mesh(); TriangleSelector selector(mesh); selector.set_facet(0, EnforcerBlockerType::Extruder16); selector.set_facet(1, EnforcerBlockerType::Extruder32); const TriangleSelector::TriangleSplittingData data = selector.serialize(); REQUIRE(data.used_states.size() == size_t(EnforcerBlockerType::ExtruderMax) + 1); REQUIRE(data.used_states[size_t(EnforcerBlockerType::Extruder16)]); REQUIRE(data.used_states[size_t(EnforcerBlockerType::Extruder32)]); REQUIRE_FALSE(data.used_states[size_t(EnforcerBlockerType::Extruder17)]); SECTION("used_states recomputed from the bitstream agrees") { TriangleSelector::TriangleSplittingData recomputed = data; recomputed.reset_used_states(); recomputed.update_used_states(0); REQUIRE(recomputed.used_states == data.used_states); } SECTION("has_facets on the raw data agrees") { REQUIRE(TriangleSelector::has_facets(data, EnforcerBlockerType::Extruder32)); REQUIRE_FALSE(TriangleSelector::has_facets(data, EnforcerBlockerType::Extruder17)); } } // States 3..17 must keep the pre-existing encoding ("11" prefix plus one nibble of state-3) so // projects written by older builds stay readable and newly written ones stay readable by them. TEST_CASE("Extruder states up to 17 keep the single-nibble encoding", "[TriangleSelector]") { const int state = GENERATE(3, 8, 16, 17); TriangleSelector selector(test_mesh()); selector.set_facet(0, EnforcerBlockerType(state)); const std::vector bitstream = selector.serialize().bitstream; INFO("Extruder " << state); // Two nibbles: the "11"-prefixed leaf code, then the state itself. REQUIRE(bitstream.size() == 8); REQUIRE(nibble_at(bitstream, 0) == 0b1100); REQUIRE(nibble_at(bitstream, 1) == state - 3); } // States 18 and above set the state nibble to 0b1111 and carry (state-18) in one more nibble. TEST_CASE("Extruder states above 17 are encoded in a second nibble", "[TriangleSelector]") { const int state = GENERATE(18, 25, 32); TriangleSelector selector(test_mesh()); selector.set_facet(0, EnforcerBlockerType(state)); const std::vector bitstream = selector.serialize().bitstream; INFO("Extruder " << state); REQUIRE(bitstream.size() == 12); REQUIRE(nibble_at(bitstream, 0) == 0b1100); REQUIRE(nibble_at(bitstream, 1) == 0b1111); REQUIRE(nibble_at(bitstream, 2) == state - 18); } // Model.cpp writes these hex strings into the 3MF for colored mesh imports; the selector must // decode exactly the states CONST_FILAMENTS assigns to them. TEST_CASE("Extruder states match the CONST_FILAMENTS hex encoding", "[TriangleSelector]") { struct Case { const char *hex; int state; }; const auto c = GENERATE(values({ {"8", 2}, {"0C", 3}, {"DC", 16}, {"EC", 17}, {"0FC", 18}, {"EFC", 32}, })); // get_triangle_as_string emits the nibbles most significant first, so read the hex backwards. const std::string hex = c.hex; std::vector bitstream; for (auto it = hex.rbegin(); it != hex.rend(); ++it) { const int nibble = *it >= 'A' ? (*it - 'A' + 10) : (*it - '0'); for (int bit = 0; bit < 4; ++bit) bitstream.push_back((nibble >> bit) & 1); } TriangleSelector::TriangleSplittingData data; data.triangles_to_split.emplace_back(0, 0); data.bitstream = bitstream; INFO("Hex " << c.hex << " -> extruder " << c.state); REQUIRE(TriangleSelector::has_facets(data, EnforcerBlockerType(c.state))); } // Pack 4-bit codes into a bitstream, least significant bit first, in the order the decoder reads them. static std::vector pack_nibbles(const std::vector &nibbles) { std::vector bitstream; for (const int nibble : nibbles) for (int bit = 0; bit < 4; ++bit) bitstream.push_back((nibble >> bit) & 1); return bitstream; } TEST_CASE("A valid paint stream with nested splits round-trips bit for bit", "[TriangleSelector]") { const TriangleMesh mesh = test_mesh(); TriangleSelector::TriangleSplittingData data; data.triangles_to_split.emplace_back(0, 0); // A three-side split whose children, in stream order, are: a one-side split (side 2) into two // leaves, a two-side split (side 1) into leaves of states 20, 0 and 8, then two plain leaves. const std::vector triangle_0 = {0b0011, 0b1001, 0b1000, 0b0100, 0b0110, 0b1100, 0b1111, 20 - 18, 0b0000, 0b1100, 8 - 3, 0b1000, 0b0100}; data.bitstream = pack_nibbles(triangle_0); data.triangles_to_split.emplace_back(5, int(data.bitstream.size())); const std::vector triangle_5 = pack_nibbles({0b1100, 3 - 3}); data.bitstream.insert(data.bitstream.end(), triangle_5.begin(), triangle_5.end()); data.reset_used_states(); REQUIRE(data.update_used_states(0)); TriangleSelector restored(mesh); restored.deserialize(data); REQUIRE(restored.num_facets(EnforcerBlockerType::Extruder20) == 1); REQUIRE(restored.num_facets(EnforcerBlockerType::Extruder3) == 1); REQUIRE(restored.serialize() == data); } TEST_CASE("A truncated or malformed paint stream drops only the damaged triangle", "[TriangleSelector][Regression]") { struct Case { const char *name; std::vector nibbles; }; const auto c = GENERATE(values({ {"three-side split missing two children", {0b0011, 0b1000, 0b1000}}, {"leaf missing its state nibble", {0b1100}}, {"leaf missing its second state nibble", {0b1100, 0b1111}}, {"splits nested past the end", {0b0011, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF, 0xF}}, {"one-side split of the nonexistent side 3", {0b1101, 0b1000, 0b1000}}, })); INFO(c.name); const TriangleMesh mesh = test_mesh(); TriangleSelector intact(mesh); intact.set_facet(0, EnforcerBlockerType::Extruder2); // Triangle 0 stays intact, triangle 1 carries the damaged stream. TriangleSelector::TriangleSplittingData data = intact.serialize(); data.triangles_to_split.emplace_back(1, int(data.bitstream.size())); const std::vector damaged = pack_nibbles(c.nibbles); data.bitstream.insert(data.bitstream.end(), damaged.begin(), damaged.end()); TriangleSelector restored(mesh); REQUIRE_NOTHROW(restored.deserialize(data)); // Triangle 1 unwinds completely, so the selector holds exactly the intact paint. REQUIRE(restored.serialize() == intact.serialize()); REQUIRE_NOTHROW(TriangleSelector::has_facets(data, EnforcerBlockerType::Extruder3)); TriangleSelector::TriangleSplittingData recomputed = data; recomputed.reset_used_states(); REQUIRE_FALSE(recomputed.update_used_states(0)); REQUIRE(std::none_of(recomputed.used_states.begin(), recomputed.used_states.end(), [](bool used) { return used; })); }