diff --git a/tests/fff_print/test_cooling.cpp b/tests/fff_print/test_cooling.cpp index 1645d99a75..be8c29d2e3 100644 --- a/tests/fff_print/test_cooling.cpp +++ b/tests/fff_print/test_cooling.cpp @@ -1,6 +1,7 @@ #include #include +#include "catch2/catch_message.hpp" #include "test_helpers.hpp" #include @@ -17,15 +18,49 @@ using namespace Slic3r; using namespace Slic3r::Test; -// The fan is held off for the first close_fan_the_first_x_layers layers, so an explicit -// fan-off command is emitted. +// The fan stays off while printing the first close_fan_the_first_x_layers layers and runs afterwards. TEST_CASE("Fan is held off for the initial layers", "[Cooling]") { + const int fan_off_layers = 5; const std::string gcode = slice({ cube(20) }, { - { "cooling", true }, - { "close_fan_the_first_x_layers", 5 }, + { "close_fan_the_first_x_layers", fan_off_layers }, + { "full_fan_speed_layer", 0 }, + { "fan_min_speed", 50 }, + { "fan_max_speed", 100 }, + { "fan_cooling_layer_time", 1000 }, // every layer is short enough to run the fan + { "layer_height", 0.2 }, + { "initial_layer_print_height", 0.2 }, + { "layer_change_gcode", ";TEST_LAYER_CHANGE" }, }); - CHECK(gcode.find("M106 S0") != std::string::npos); + + // Record, for each layer, whether any extrusion was printed with the part fan running. + std::vector extruded_with_fan; + int layer = -1; + bool fan_on = false; + std::istringstream input(gcode); + std::string line; + while (std::getline(input, line)) { + if (line.rfind(";TEST_LAYER_CHANGE", 0) == 0) { + ++layer; + extruded_with_fan.push_back(false); + } else if (line.rfind("M107", 0) == 0) { + fan_on = false; + } else if (line.rfind("M106 ", 0) == 0 && line.find(" P") == std::string::npos) { + const size_t s = line.find(" S"); + fan_on = s != std::string::npos && std::stod(line.substr(s + 2)) > 0.; + } else if (layer >= 0 && fan_on && line.rfind("G1 ", 0) == 0 && line.find(" E") != std::string::npos && + (line.find(" X") != std::string::npos || line.find(" Y") != std::string::npos)) { + extruded_with_fan[layer] = true; + } + } + + // A 20 mm cube at 0.2 mm layers has 100 layers. + REQUIRE(extruded_with_fan.size() == 100); + for (int i = 0; i < fan_off_layers; ++i) { + CAPTURE(i); + CHECK_FALSE(extruded_with_fan[i]); + } + CHECK(extruded_with_fan[fan_off_layers]); } // The cooling pass resolves and strips its internal speed placeholders; none leak into diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp index 1fbedcda8f..160751e1db 100644 --- a/tests/fff_print/test_fill.cpp +++ b/tests/fff_print/test_fill.cpp @@ -86,10 +86,9 @@ TEST_CASE("Pattern path length", "[Fill]") { Slic3r::Polylines paths = test(Slic3r::ExPolygon(test_set)); REQUIRE(paths.size() == 1); // one continuous path - // TODO: determine what the "Expected length" should be for rectilinear fill of a 100x100 polygon. - // This check only checks that it's above scale(3*100 + 2*50) + scaled_epsilon. - // ok abs($paths->[0]->length - scale(3*100 + 2*50)) - scaled_epsilon, 'path has expected length'; - REQUIRE(std::abs(paths[0].length() - static_cast(scale_(3*100 + 2*50))) - SCALED_EPSILON > 0); // path has expected length + // Lines are 50 mm apart and the square is inset by half a spacing, so the lines on its edges drop out + // and a single line crosses the middle, ending within half a spacing of the edges. + CHECK_THAT(unscale(paths[0].length()), Catch::Matchers::WithinAbs(100., filler->spacing / 2.)); test_set.clear(); } @@ -148,10 +147,9 @@ TEST_CASE("Pattern path length", "[Fill]") { Slic3r::Polylines paths = test(Slic3r::ExPolygon(points)); REQUIRE(paths.size() == 1); // one continuous path - // TODO: determine what the "Expected length" should be for rectilinear fill of a 100x100 polygon. - // This check only checks that it's above scale(3*100 + 2*50) + scaled_epsilon. - // ok abs($paths->[0]->length - scale(3*100 + 2*50)) - scaled_epsilon, 'path has expected length'; - REQUIRE(std::abs(paths[0].length() - static_cast(scale_(3*100 + 2*50))) - SCALED_EPSILON > 0); // path has expected length + // A solid fill covers the area with lines one spacing apart, so a missing segment shortens the path. + const double expected_length = unscale(unscale(Slic3r::ExPolygon(points).area())) / filler->spacing; + CHECK_THAT(unscale(paths[0].length()), Catch::Matchers::WithinRel(expected_length, 0.01)); } SECTION("Rotated Square") { @@ -168,8 +166,9 @@ TEST_CASE("Pattern path length", "[Fill]") { fill_params.density = 1.0; filler->spacing = flow.spacing(); - for (auto angle : { 0.0, 45.0}) { + for (auto angle : { 0.0, PI / 4.0 }) { surface.expolygon.rotate(angle, Point(0,0)); + filler->bounding_box = get_extents(surface.expolygon.contour); Polylines paths = filler->fill_surface(&surface, fill_params); REQUIRE(paths.size() == 1); } @@ -222,7 +221,7 @@ TEST_CASE("Pattern path length", "[Fill]") { }; Slic3r::ExPolygon expolygon(points); - REQUIRE(test_if_solid_surface_filled(expolygon, 0.5, 45.0, 0.99) == true); + REQUIRE(test_if_solid_surface_filled(expolygon, 0.5, PI / 4.0, 0.99) == true); } } diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 670c2aa7c1..55f82894c1 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -123,38 +123,49 @@ SCENARIO("Changing the number of solid shell layers does not make all surfaces i config.set_deserialize_strict({ { "top_shell_layers", 2 }, { "bottom_shell_layers", 1 }, - { "layer_height", 0.25 }, // get a known number of layers + // Shell counts alone decide which layers are solid. + { "top_shell_thickness", 0 }, + { "bottom_shell_thickness", 0 }, + { "sparse_infill_density", "20%" }, + { "layer_height", 0.25 }, // 80 layers: 0..79 { "initial_layer_print_height", 0.25 } }); Slic3r::Print print; Slic3r::Model model; Slic3r::Test::init_print({cube(20)}, print, model, config); - // Precondition: Ensure that the model has 2 solid top layers (79, 78) - // and one solid bottom layer (0). - auto test_is_solid_infill = [&print](size_t obj_id, size_t layer_id) { + // True when the layer has fill surfaces and all of them are solid. + auto is_solid_infill = [&print](size_t obj_id, size_t layer_id) { const Layer &layer = *(print.objects().at(obj_id)->get_layer((int)layer_id)); - // iterate over all of the regions in the layer - for (const LayerRegion *region : layer.regions()) { - // for each region, iterate over the fill surfaces - for (const Surface &surface : region->fill_surfaces.surfaces) - CHECK(surface.is_solid()); - } + bool has_fill = false; + for (const LayerRegion *region : layer.regions()) + for (const Surface &surface : region->fill_surfaces.surfaces) { + if (! surface.is_solid()) + return false; + has_fill = true; + } + return has_fill; }; print.process(); - test_is_solid_infill(0, 0); // should be solid - test_is_solid_infill(0, 79); // should be solid - test_is_solid_infill(0, 78); // should be solid + // Precondition: 2 solid top layers (79, 78) and one solid bottom layer (0). + REQUIRE(is_solid_infill(0, 0)); + REQUIRE(is_solid_infill(0, 79)); + REQUIRE(is_solid_infill(0, 78)); + REQUIRE_FALSE(is_solid_infill(0, 77)); WHEN("Model is re-sliced with top_shell_layers == 3") { config.set("top_shell_layers", 3); print.apply(model, config); print.process(); THEN("Print object does not have 0 solid bottom layers.") { - test_is_solid_infill(0, 0); + CHECK(is_solid_infill(0, 0)); } AND_THEN("Print object has 3 top solid layers") { - test_is_solid_infill(0, 79); - test_is_solid_infill(0, 78); - test_is_solid_infill(0, 77); + CHECK(is_solid_infill(0, 79)); + CHECK(is_solid_infill(0, 78)); + CHECK(is_solid_infill(0, 77)); + CHECK_FALSE(is_solid_infill(0, 76)); + } + AND_THEN("Middle layers keep sparse infill") { + CHECK_FALSE(is_solid_infill(0, 40)); } } } @@ -672,14 +683,18 @@ TEST_CASE("gcode_skip_config_block omits the resolved-settings comment block", " { const std::string gcode = slice({ cube(20) }, { { "gcode_skip_config_block", true }, - { "gcode_comments", true }, }); CHECK(gcode.find("; CONFIG_BLOCK_START") == std::string::npos); CHECK(gcode.find("; CONFIG_BLOCK_END") == std::string::npos); CHECK(gcode.find("; layer_height =") == std::string::npos); - CHECK(gcode.find("; fill_density =") == std::string::npos); + CHECK(gcode.find("; sparse_infill_density =") == std::string::npos); CHECK(gcode.find("; HEADER_BLOCK_START") != std::string::npos); CHECK(gcode.find("; EXECUTABLE_BLOCK_START") != std::string::npos); + + const std::string with_block = slice({ cube(20) }, { { "gcode_skip_config_block", false } }); + CHECK(with_block.find("; CONFIG_BLOCK_START") != std::string::npos); + CHECK(with_block.find("; layer_height =") != std::string::npos); + CHECK(with_block.find("; sparse_infill_density =") != std::string::npos); } // Some firmwares only scan the last N lines of the file for "estimated printing time", so it diff --git a/tests/fff_print/test_trianglemesh.cpp b/tests/fff_print/test_trianglemesh.cpp index ab5c334d2e..e43185d2f6 100644 --- a/tests/fff_print/test_trianglemesh.cpp +++ b/tests/fff_print/test_trianglemesh.cpp @@ -1,6 +1,8 @@ #include #include +#include +#include "catch2/matchers/catch_matchers.hpp" #include "libslic3r/TriangleMesh.hpp" #include "libslic3r/TriangleMeshSlicer.hpp" #include "libslic3r/Point.hpp" @@ -162,21 +164,29 @@ SCENARIO("Slice behavior", "[TriangleMesh]") { const std::vector vertices {{0,0,0},{0,0,20},{0,5,0},{0,5,20},{50,0,0},{50,0,20},{15,5,0},{35,5,0},{15,20,0},{50,5,0},{35,20,0},{15,5,10},{50,5,20},{35,5,10},{35,20,10},{15,20,10}}; const std::vector facets {{0,1,2},{2,1,3},{1,0,4},{5,1,4},{0,2,4},{4,2,6},{7,6,8},{4,6,7},{9,4,7},{7,8,10},{2,3,6},{11,3,12},{7,12,9},{13,12,7},{6,3,11},{11,12,13},{3,1,5},{12,3,5},{5,4,9},{12,5,9},{13,7,10},{14,13,10},{8,15,10},{10,15,14},{6,11,8},{8,11,15},{15,11,13},{14,15,13}}; - auto cube = make_cube(); + TriangleMesh mesh(vertices, facets); + // A 50x5x20 wall with a 20x15x10 block against it, so the block's top is at Z = 10. + const double wall_area = 50. * 5.; + const double wall_block_area = wall_area + 20. * 15.; + const auto area_mm2 = [](const ExPolygons &slice) { return slice.at(0).area() * SCALING_FACTOR * SCALING_FACTOR; }; WHEN(" a top tangent plane is sliced") { - // At Z = 10 we have a top horizontal surface. - std::vector slices = cube.slice({5.0, 10.0}); + std::vector slices = mesh.slice({5.0, 10.0}); THEN( "its area is included") { - REQUIRE(slices.at(0).at(0).area() > 0); - REQUIRE(slices.at(1).at(0).area() > 0); + REQUIRE(slices.at(0).size() == 1); + REQUIRE(slices.at(1).size() == 1); + CHECK_THAT(area_mm2(slices.at(0)), Catch::Matchers::WithinRel(wall_block_area, 1e-6)); + CHECK_THAT(area_mm2(slices.at(1)), Catch::Matchers::WithinRel(wall_block_area, 1e-6)); } } WHEN(" a model that has been transformed is sliced") { - cube.mirror_z(); - std::vector slices = cube.slice({-5.0, -10.0}); - THEN( "it is sliced properly (mirrored bottom plane area is included)") { - REQUIRE(slices.at(0).at(0).area() > 0); - REQUIRE(slices.at(1).at(0).area() > 0); + mesh.mirror_z(); + std::vector slices = mesh.slice({-5.0, -10.0}); + THEN( "it is sliced like the original, and the mirrored block's bottom plane is not part of it") { + REQUIRE(slices.at(0).size() == 1); + REQUIRE(slices.at(1).size() == 1); + CHECK_THAT(area_mm2(slices.at(0)), Catch::Matchers::WithinRel(wall_block_area, 1e-6)); + // A triangle owns its top edge but not its bottom edge, so a bottom tangent plane is left out. + CHECK_THAT(area_mm2(slices.at(1)), Catch::Matchers::WithinRel(wall_area, 1e-6)); } } } diff --git a/tests/filament_group/filament_group_regression_main.cpp b/tests/filament_group/filament_group_regression_main.cpp index ec3264ba2c..ff39c7639e 100644 --- a/tests/filament_group/filament_group_regression_main.cpp +++ b/tests/filament_group/filament_group_regression_main.cpp @@ -319,9 +319,18 @@ TEST_CASE("FilamentGroup property checks", "[filament_group][property]") { // build (config_b/config_c cases evaluate the full per-layer nozzle-aware reorder for every // candidate grouping; this is a guard against hangs, not a micro-perf gate). REQUIRE(result.elapsed_ms < 40000.0); - REQUIRE(result.flush_cost >= 0); + // Every case, feasible or not, must yield a usable grouping: each used filament is + // assigned to an existing nozzle. auto used_filaments = collect_sorted_used_filaments(tc.context.model_info.layer_filaments); + const auto &nozzle_list = tc.context.nozzle_info.nozzle_list; + for (unsigned int fil : used_filaments) { + INFO("Filament: " << fil); + REQUIRE(fil < result.filament_map.size()); + CHECK(result.filament_map[fil] >= 0); + CHECK(result.filament_map[fil] < static_cast(nozzle_list.size())); + } + if (is_constraint_feasible(tc.context, used_filaments)) { if (!result.constraints_ok) { for (auto& v : result.violations) diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index f7b5049ebe..a02b07564b 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -1033,18 +1033,19 @@ SCENARIO("Non-standard nozzle diameter survives .3mf save on a single-nozzle pri } } -// A legacy / foreign project (no multi-nozzle metadata) must load crash-safe through the BBS -// importer and must not fabricate a filament_volume_map. -SCENARIO("Legacy project loads crash-safe via load_bbs_3mf", "[3mf][MultiNozzle]") { - GIVEN("a project without any multi-nozzle metadata") { - std::string path = std::string(TEST_DATA_DIR) + "/test_3mf/Geräte/Büchse.3mf"; - Model model; - DynamicPrintConfig config; - ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable }; - PlateDataPtrs plates; - std::vector project_presets; - bool is_bbl_3mf = false, is_orca_3mf = false; - Semver file_version; +// A project without multi-nozzle metadata must load through the BBS importer and must not +// fabricate a filament_volume_map. +SCENARIO("Projects without multi-nozzle metadata load without a per-filament volume map", "[3mf][MultiNozzle]") { + ConfigSubstitutionContext ctxt{ ForwardCompatibilitySubstitutionRule::Enable }; + std::vector project_presets; + bool is_bbl_3mf = false, is_orca_3mf = false; + Semver file_version; + + GIVEN("a legacy project with a bare model and no plate metadata") { + std::string path = std::string(TEST_DATA_DIR) + "/test_3mf/Geräte/Büchse.3mf"; + Model model; + DynamicPrintConfig config; + PlateDataPtrs plates; WHEN("loaded through the BBS importer") { bool loaded = false; @@ -1052,10 +1053,32 @@ SCENARIO("Legacy project loads crash-safe via load_bbs_3mf", "[3mf][MultiNozzle] &project_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, nullptr, LoadStrategy::LoadModel | LoadStrategy::LoadConfig)); - THEN("it does not crash and invents no per-filament volume map") { - for (PlateData* p : plates) { - REQUIRE(p->config.option("filament_volume_map") == nullptr); - } + THEN("the model loads") { + REQUIRE(loaded); + REQUIRE_FALSE(model.objects.empty()); + } + release_PlateData_list(plates); + } + } + + GIVEN("a project with plate metadata but no filament volume map") { + ScopedTemporaryFile temp(".3mf"); + store_painted_cube(temp.string()); + ScopedTemporaryDir backup_dir("orca_volume_map_dst"); + Model model; + model.set_backup_path(backup_dir.string()); + DynamicPrintConfig config; + PlateDataPtrs plates; + + WHEN("loaded through the BBS importer") { + const bool loaded = load_bbs_3mf(temp.string().c_str(), &config, &ctxt, &model, &plates, + &project_presets, &is_bbl_3mf, &is_orca_3mf, &file_version, + nullptr, LoadStrategy::LoadModel | LoadStrategy::LoadConfig); + THEN("every plate loads without a per-filament volume map") { + REQUIRE(loaded); + REQUIRE_FALSE(plates.empty()); + for (PlateData* p : plates) + CHECK(p->config.option("filament_volume_map") == nullptr); } release_PlateData_list(plates); } @@ -2026,8 +2049,8 @@ SCENARIO("Published 3MF round-trips the extended material metadata", "[3mf]") { REQUIRE(dst_model.model_info != nullptr); REQUIRE(dst_model.model_info->metadata_items[ORCA_PUBLISHED_MATERIAL_TAG] == material_keys_json); - // The value must parse back with every extended field intact. - nlohmann::json entries = nlohmann::json::parse(material_keys_json); + // The loaded value must parse back with every extended field intact. + nlohmann::json entries = nlohmann::json::parse(dst_model.model_info->metadata_items[ORCA_PUBLISHED_MATERIAL_TAG]); REQUIRE(entries.is_array()); REQUIRE(entries.size() == 1); REQUIRE(entries[0]["full"].get() == true); diff --git a/tests/libslic3r/test_caddocument.cpp b/tests/libslic3r/test_caddocument.cpp index 4ee1de587d..28133550b8 100644 --- a/tests/libslic3r/test_caddocument.cpp +++ b/tests/libslic3r/test_caddocument.cpp @@ -53,6 +53,7 @@ #include #include #include +#include #include #include #include @@ -772,16 +773,9 @@ TEST_CASE("entity constraints: tangent/midpoint/symmetric/angle", "[CadDocument] TEST_CASE("imported text glyphs all extrude without failing (charset sweep)", "[CadDocument]") { - std::string font = resources_dir().empty() - ? std::string("resources/fonts/HarmonyOS_Sans_SC_Regular.ttf") - : resources_dir() + "/fonts/HarmonyOS_Sans_SC_Regular.ttf"; - { - std::ifstream probe(font); - if (!probe.good()) { - SUCCEED("bundled font not reachable in this environment; covered live on :10"); - return; - } - } + // The bundled font, from the source tree: PROFILES_DIR is /resources/profiles. + const std::string font = std::string(PROFILES_DIR) + "/../fonts/HarmonyOS_Sans_SC_Regular.ttf"; + REQUIRE(std::ifstream(font).good()); const std::string charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" @@ -2853,9 +2847,10 @@ TEST_CASE("datum coordinate system: non-perpendicular inputs produce orthonormal using Catch::Matchers::WithinAbs; CadDocument doc; - // Build a body so we have a face to reference for FaceAndDirection. - int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 10, "Box"); - doc.add_extrude(sk, 10.0, false, BooleanMode::New, "BoxExt"); + // A cylinder's end cap is bounded by one closed circular edge, which offers no in-plane + // direction, so the frame has to fall back on coordsys_x_hint. + int sk = doc.add_sketch(SketchShape::Circle, SketchPlane::XY(), 0, 0, 5.0, "Circle"); + doc.add_extrude(sk, 10.0, false, BooleanMode::New, "Cyl"); REQUIRE(doc.recompute()); REQUIRE(doc.bodies.size() == 1); int n_faces = GeometryEngine::face_count(doc.bodies[0].shape); @@ -2865,34 +2860,33 @@ TEST_CASE("datum coordinate system: non-perpendicular inputs produce orthonormal if (fn.z() > 0.9) { top_face = i; break; } } REQUIRE(top_face >= 0); + const Vec3d Z = GeometryEngine::face_normal_world(GeometryEngine::face_by_index(doc.bodies[0].shape, top_face)); int cs = doc.add_coordsys(CoordSysType::PointWorld, Vec3d(0, 0, 0), "CS1"); REQUIRE(cs >= 0); doc.features[cs].coordsys_type = CoordSysType::FaceAndDirection; doc.features[cs].coordsys_body = 0; doc.features[cs].coordsys_face = top_face; - // Deliberately non-perpendicular X hint (NOT orthogonal to face normal ~+Z). + // Deliberately non-perpendicular X hint (NOT orthogonal to the face normal +Z). doc.features[cs].coordsys_x_hint = Vec3d(3.0, -1.0, 0.5); auto css = doc.resolve_datum_coordsys(); REQUIRE(css.size() == 1); REQUIRE(css[0].error.empty()); - Vec3d X = css[0].x, Y = css[0].y; - // Orthonormality: each axis has unit length + const Vec3d X = css[0].x, Y = css[0].y; + // X is the hint projected into the face plane. + const Vec3d expected_x = Vec3d(3.0, -1.0, 0.0).normalized(); + CHECK_THAT(X.x(), WithinAbs(expected_x.x(), 1e-9)); + CHECK_THAT(X.y(), WithinAbs(expected_x.y(), 1e-9)); + CHECK_THAT(X.z(), WithinAbs(expected_x.z(), 1e-9)); + // Orthonormal, with Z the face normal and the frame right-handed. CHECK_THAT(X.norm(), WithinAbs(1.0, 1e-9)); CHECK_THAT(Y.norm(), WithinAbs(1.0, 1e-9)); - // Pairwise dot products are ~0 - CHECK_THAT(std::abs(X.dot(Y)), WithinAbs(0.0, 1e-9)); - // Z = X x Y (derived), also unit and perpendicular - Vec3d Z = X.cross(Y); - CHECK_THAT(Z.norm(), WithinAbs(1.0, 1e-9)); - CHECK_THAT(std::abs(X.dot(Z)), WithinAbs(0.0, 1e-9)); - CHECK_THAT(std::abs(Y.dot(Z)), WithinAbs(0.0, 1e-9)); - // Right-handedness: X x Y == Z - CHECK_THAT(Z.x(), WithinAbs((X.cross(Y)).x(), 1e-9)); - CHECK_THAT(Z.y(), WithinAbs((X.cross(Y)).y(), 1e-9)); - CHECK_THAT(Z.z(), WithinAbs((X.cross(Y)).z(), 1e-9)); + CHECK_THAT(X.dot(Y), WithinAbs(0.0, 1e-9)); + CHECK_THAT(X.dot(Z), WithinAbs(0.0, 1e-9)); + CHECK_THAT(Y.dot(Z), WithinAbs(0.0, 1e-9)); + CHECK_THAT(X.cross(Y).dot(Z), WithinAbs(1.0, 1e-9)); } TEST_CASE("datum coordinate system: a face-only frame rotates with its body", "[CadDocument]") @@ -3917,17 +3911,23 @@ TEST_CASE("Use/Convert Entities: construction references never become solid", "[ } REQUIRE(top_face >= 0); - const size_t body_count_before = doc.bodies.size(); - const double volume_before = doc.body_mass_properties(0).volume; - int target = doc.add_sketch_entities({}, SketchPlane::XY(), "Use"); int appended = doc.project_edges_into_sketch(target, 0, {}, top_face); REQUIRE(appended == 4); + doc.add_extrude(target, 5.0, false, BooleanMode::New, "UseExt"); - REQUIRE(doc.recompute()); - REQUIRE(doc.error.empty()); - REQUIRE(doc.bodies.size() == body_count_before); - REQUIRE_THAT(doc.body_mass_properties(0).volume, WithinRel(volume_before, 1e-9)); + SECTION("construction edges form no profile, so the extrude fails") { + REQUIRE_FALSE(doc.recompute()); + REQUIRE_FALSE(doc.error.empty()); + } + SECTION("the same edges as solid geometry extrude a 20x20x5 body") { + for (auto& e : doc.features[target].entities) + e.construction = false; + REQUIRE(doc.recompute()); + REQUIRE(doc.error.empty()); + REQUIRE(doc.bodies.size() == 2); + REQUIRE_THAT(doc.body_mass_properties(1).volume, WithinRel(20.0 * 20.0 * 5.0, 1e-6)); + } } TEST_CASE("Use/Convert Entities: bad references return -1 and change nothing", "[CadDocument][project]") @@ -4252,7 +4252,7 @@ TEST_CASE("a previous-format recipe is refused, not silently misread", "[CadDocu REQUIRE(doc.features.empty()); // nothing half-read was left behind } -TEST_CASE("golden recipe v1 still deserialises", "[CadDocument]") +TEST_CASE("a v4 recipe keeps every field value", "[CadDocument]") { using Catch::Matchers::WithinRel; using Catch::Matchers::WithinAbs; @@ -4282,20 +4282,16 @@ TEST_CASE("golden recipe v1 still deserialises", "[CadDocument]") } CadDocument expected = make_golden_doc_v1(); - // Scoped tightly: this advice is ONLY valid for a count mismatch. It must not be in - // scope for the field-value assertions below, where "regenerate the fixture" is the - // one thing you must never do -- regenerating after a reorder bakes the corrupted - // layout in as the new golden and permanently disarms this test. { - INFO("Feature count changed - did you add/remove features in make_golden_doc_v1()?"); - INFO("If so: run libslic3r_tests \"[.regen]\" and re-run this test."); + INFO("Feature count changed - make_golden_doc_v1() must keep building exactly the tree " + "frozen in cad_recipe_v4.bin, so do not add or remove features there."); REQUIRE(features.size() == expected.features.size()); } // A failure BELOW this point means the on-disk serialization format changed: some field // in CadFeature::save/load was reordered, retyped, or removed. Fields may only ever be // APPENDED at the end of both lists. Do NOT regenerate the fixture to make this pass -- - // fix the field order instead. See scripts/CAD/run-kernel-tests.sh and the [.regen] case. + // fix the field order instead. // Field-by-field assertions against expected values. // Every field set to a distinctive non-default literal must be checked here. @@ -4550,35 +4546,47 @@ TEST_CASE("golden recipe v1 still deserialises", "[CadDocument]") } } - // --- Layer 2: geometry check (optional — only if the document recomputes) --- - // Build expected document and try to recompute it. - // ponytail: the field-value layer above is the real tripwire; - // this layer is a bonus sanity check on the full recompute path. + // --- Layer 2: the production v4 load rebuilds the same geometry as the in-memory tree --- + // The golden tree is serialization coverage and does not recompute as a whole, so the + // features are switched on one at a time and any the in-memory tree cannot build are left + // off in both. Every step that builds must give the loaded tree the same bodies. CadDocument exp_doc = make_golden_doc_v1(); - bool exp_ok = exp_doc.recompute(); - if (!exp_ok) { - INFO("Expected document from make_golden_doc_v1() could not recompute " - "(complex feature tree). Field-value checks above are sufficient."); + // Fields appended after v4 are not in the file, so a v4 project reads them as defaults + // (a v4 thread, for one, is not major-nominal). The reference has to match that. + const CadFeature def; + for (CadFeature& f : exp_doc.features) { + f.thread_major_nominal = def.thread_major_nominal; + f.pattern_inclusive = def.pattern_inclusive; + f.dressup_edges = def.dressup_edges; + f.text_string = def.text_string; + f.text_font = def.text_font; + f.text_height = def.text_height; + f.revolve_axis_entity = def.revolve_axis_entity; } - CadDocument doc; - bool ser_ok = doc.deserialize_recipe(blob); - - if (exp_ok && ser_ok) { - REQUIRE(doc.error.empty()); + doc.deserialize_recipe(blob); + REQUIRE(doc.features.size() == exp_doc.features.size()); + for (CadFeature& f : exp_doc.features) f.enabled = false; + for (CadFeature& f : doc.features) f.enabled = false; + size_t built_steps = 0; + for (size_t i = 0; i < exp_doc.features.size(); ++i) { + exp_doc.features[i].enabled = doc.features[i].enabled = true; + if (!exp_doc.recompute()) { + exp_doc.features[i].enabled = doc.features[i].enabled = false; + continue; + } + INFO("after feature " << i << " (" << exp_doc.features[i].name << ")"); + REQUIRE(doc.recompute()); REQUIRE(doc.bodies.size() == exp_doc.bodies.size()); - for (size_t i = 0; i < doc.bodies.size(); ++i) { - double v = double(SketchEngine::tessellate(doc.bodies[i].shape).volume()); - double ev = double(SketchEngine::tessellate(exp_doc.bodies[i].shape).volume()); + for (size_t b = 0; b < doc.bodies.size(); ++b) { + double v = double(SketchEngine::tessellate(doc.bodies[b].shape).volume()); + double ev = double(SketchEngine::tessellate(exp_doc.bodies[b].shape).volume()); REQUIRE_THAT(v, WithinRel(ev, 1e-6)); } - } else { - INFO("The golden recipe fixture was loaded and field-value checks passed."); - INFO("Recompute on the deserialized or expected document failed — this is"); - INFO("expected for the extended golden fixture (many feature types coexist"); - INFO("purely for serialization coverage). The field-value tripwire above is"); - INFO("the primary format check."); + ++built_steps; } + CHECK(built_steps > 0); + CHECK_FALSE(exp_doc.bodies.empty()); } // --- Rib tests (M5b) --- @@ -5781,8 +5789,9 @@ TEST_CASE("fastened mate with angle", "[CadDocument][mate]") REQUIRE(doc.recompute()); REQUIRE(doc.error.empty()); - int sk2 = doc.add_sketch(SketchShape::Circle, SketchPlane::XY(), 0, 0, 3, "Cyl"); - doc.add_extrude(sk2, 10.0, false, BooleanMode::New, "CylExt"); + // A 20x10 plate, so a quarter turn about the connector's Z swaps its X and Y extents. + int sk2 = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 10, 0, "Plate"); + doc.add_extrude(sk2, 10.0, false, BooleanMode::New, "PlateExt"); REQUIRE(doc.recompute()); REQUIRE(doc.error.empty()); @@ -5801,6 +5810,13 @@ TEST_CASE("fastened mate with angle", "[CadDocument][mate]") gp_Pnt com = props.CentreOfMass(); REQUIRE_THAT(double(com.X()), WithinAbs(5.0, 1e-4)); REQUIRE_THAT(double(com.Y()), WithinAbs(5.0, 1e-4)); + + Bnd_Box bb; + BRepBndLib::Add(doc.bodies[1].shape, bb); + double x0, y0, z0, x1, y1, z1; + bb.Get(x0, y0, z0, x1, y1, z1); + REQUIRE_THAT(x1 - x0, WithinAbs(10.0, 1e-3)); + REQUIRE_THAT(y1 - y0, WithinAbs(20.0, 1e-3)); } TEST_CASE("fastened mate with flip", "[CadDocument][mate]") @@ -5823,7 +5839,9 @@ TEST_CASE("fastened mate with flip", "[CadDocument][mate]") REQUIRE(doc.recompute()); REQUIRE(doc.error.empty()); - int cs_moving = doc.add_coordsys(CoordSysType::PointWorld, Vec3d(0, 0, 5), "CS_Moving"); + // The connector sits on the cylinder's base, so the flip decides which side of the + // fixed connector (z = 5) the 10 mm cylinder hangs: above it unflipped, below it flipped. + int cs_moving = doc.add_coordsys(CoordSysType::PointWorld, Vec3d(0, 0, 0), "CS_Moving"); doc.features[cs_moving].coordsys_body = 1; REQUIRE(doc.recompute()); REQUIRE(doc.error.empty()); @@ -5837,8 +5855,7 @@ TEST_CASE("fastened mate with flip", "[CadDocument][mate]") gp_Pnt com = props.CentreOfMass(); REQUIRE_THAT(double(com.X()), WithinAbs(5.0, 1e-4)); REQUIRE_THAT(double(com.Y()), WithinAbs(5.0, 1e-4)); - // Flip opposes Z axes: for a symmetric cylinder this is invisible in centroid, - // but the mate executed cleanly and the body moved to the target connector. + REQUIRE_THAT(double(com.Z()), WithinAbs(5.0 - 10.0 / 2, 1e-4)); } TEST_CASE("planar mate: normal distance becomes mate_offset", "[CadDocument][mate]") @@ -6525,23 +6542,19 @@ TEST_CASE("fastened mate with flip on asymmetric body", "[CadDocument][mate]") REQUIRE(doc.recompute()); REQUIRE(doc.error.empty()); - // Fastened, NO flip - doc.add_mate(0, cs_fixed, cs_moving, 0.0, 0.0, false, "MateNoFlip"); - REQUIRE(doc.recompute()); - REQUIRE(doc.error.empty()); - GProp_GProps nf_props; - BRepGProp::VolumeProperties(doc.bodies[1].shape, nf_props); - double nf_z = nf_props.CentreOfMass().Z(); - - doc.undo(); - - // Fastened, WITH flip - doc.add_mate(0, cs_fixed, cs_moving, 0.0, 0.0, true, "MateFlip"); - REQUIRE(doc.recompute()); - REQUIRE(doc.error.empty()); - GProp_GProps f_props; - BRepGProp::VolumeProperties(doc.bodies[1].shape, f_props); - double f_z = f_props.CentreOfMass().Z(); + // The same assembly mated once without and once with flip, each in its own copy. + auto mated_com_z = [&](bool flip) { + CadDocument d = doc; + d.add_mate(0, cs_fixed, cs_moving, 0.0, 0.0, flip, "Mate"); + REQUIRE(d.recompute()); + REQUIRE(d.error.empty()); + REQUIRE(d.mate_conflicts.empty()); + GProp_GProps props; + BRepGProp::VolumeProperties(d.bodies[1].shape, props); + return props.CentreOfMass().Z(); + }; + const double nf_z = mated_com_z(false); + const double f_z = mated_com_z(true); // Flip changes the centroid Z for an asymmetric body REQUIRE_THAT(f_z, !WithinAbs(nf_z, 1e-4)); @@ -7324,13 +7337,22 @@ TEST_CASE("interference: sheet bodies are skipped", "[CadDocument][interference] CadDocument doc; int sk_a = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 0, "BoxA"); doc.add_extrude(sk_a, 10.0, false, BooleanMode::New, "EA"); - // A sheet passing straight through the solid: it has no volume, so no interference. - int sk_s = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 40, 40, 0, "Sheet"); + // Four 10x5 walls standing inside the 20x20x10 solid: a sheet has no volume, so no interference. + int sk_s = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 10, 10, 0, "Sheet"); doc.add_surface_extrude(sk_s, 5.0, "SE"); REQUIRE(doc.recompute()); REQUIRE(doc.bodies.size() == 2); REQUIRE(CadDocument::is_sheet_shape(doc.bodies[1].shape)); + // The raw overlap of the walls with the solid does measure a volume, so only the sheet + // skip keeps it out of the report. + BRepAlgoAPI_Common common(doc.bodies[0].shape, doc.bodies[1].shape); + common.Build(); + REQUIRE(common.IsDone()); + GProp_GProps props; + BRepGProp::VolumeProperties(common.Shape(), props); + REQUIRE(std::abs(props.Mass()) > 1e-6); + REQUIRE(doc.check_interference().empty()); } @@ -7540,9 +7562,8 @@ TEST_CASE("A cut that removes no material is an error, not a silent success", "[ CHECK(doc.error.find("removed no material") != std::string::npos); // And the body is left as it was, not half-applied. - CadDocument again = box(); - REQUIRE(again.recompute()); - CHECK_THAT(again.body_mass_properties(0).volume, + REQUIRE(doc.bodies.size() == 1); + CHECK_THAT(doc.body_mass_properties(0).volume, Catch::Matchers::WithinAbs(solid, 1e-6)); } diff --git a/tests/libslic3r/test_config.cpp b/tests/libslic3r/test_config.cpp index 20177bcc07..213691ae2a 100644 --- a/tests/libslic3r/test_config.cpp +++ b/tests/libslic3r/test_config.cpp @@ -75,11 +75,10 @@ SCENARIO("Config accessor functions perform as expected.", "[Config]") { } } WHEN("A boolean option is set to a string value representing something other than 0 or 1") { - THEN("A BadOptionTypeException exception is thrown.") { + config.set("gcode_comments", true); + THEN("A BadOptionTypeException exception is thrown and the value is unchanged.") { REQUIRE_THROWS_AS(config.set("gcode_comments", "Z"), BadOptionTypeException); - } - AND_THEN("Value is unchanged.") { - REQUIRE(config.opt("gcode_comments")->getBool() == false); + REQUIRE(config.opt("gcode_comments")->getBool() == true); } } WHEN("A boolean option is set to an int value") { @@ -117,12 +116,10 @@ SCENARIO("Config accessor functions perform as expected.", "[Config]") { } } WHEN("A numeric option is set to a non-numeric value.") { - auto prev_value = config.opt("max_bridge_length")->getFloat(); - THEN("A BadOptionTypeException exception is thrown.") { + config.set("max_bridge_length", 7.5); + THEN("A BadOptionValueException exception is thrown and the value does not change.") { REQUIRE_THROWS_AS(config.set_deserialize_strict("max_bridge_length", "zzzz"), BadOptionValueException); - } - THEN("The value does not change.") { - REQUIRE(config.opt("max_bridge_length")->getFloat() == prev_value); + REQUIRE_THAT(config.opt("max_bridge_length")->getFloat(), Catch::Matchers::WithinAbs(7.5, 1e-9)); } } WHEN("A string option is set through the string interface") { @@ -198,7 +195,7 @@ SCENARIO("Config accessor functions perform as expected.", "[Config]") { } } WHEN("An invalid option is requested during set.") { - THEN("A BadOptionTypeException exception is thrown.") { + THEN("A UnknownOptionException exception is thrown.") { REQUIRE_THROWS_AS(config.set("deadbeef_invalid_option", 1), UnknownOptionException); REQUIRE_THROWS_AS(config.set("deadbeef_invalid_option", 1.0), UnknownOptionException); REQUIRE_THROWS_AS(config.set("deadbeef_invalid_option", "1"), UnknownOptionException); diff --git a/tests/libslic3r/test_filament_mixer.cpp b/tests/libslic3r/test_filament_mixer.cpp index 6da90950bf..3093078151 100644 --- a/tests/libslic3r/test_filament_mixer.cpp +++ b/tests/libslic3r/test_filament_mixer.cpp @@ -65,41 +65,25 @@ TEST_CASE("expand_mixed_filaments replaces mixed slots with their components", " } } -TEST_CASE("belt purge tower island count ignores virtual mixed slots", "[FilamentMixer][belt]") +TEST_CASE("A mixed slot whose components are already used adds no filaments", "[FilamentMixer][belt]") { - // Regression for the "extra purge tower" on a belt printer with a mixed - // filament (MCTEST5). The belt purge prism is sized as - // n_islands = used_filaments.size() - 1 - // and GUI::ensure_belt_purge_tower() collected those filaments straight off - // the model objects' extruder assignments. A mixed slot is VIRTUAL -- no - // nozzle carries it, and ToolOrdering::resolve_mixed_filaments() replaces it - // with its components before any G-code is emitted -- so counting it as a - // filament of its own provisions one island that can never be reached. - // - // MCTEST5: five cubes on extruders 1..5, where filament 5 is a 50/50 blend of - // filaments 2 and 4. The G-code uses only T0..T3 and reports - // "filament used [g] = 53.35, 141.11, 40.84, 107.23, 0.00" -- filament 5 - // consumes nothing, exactly as a virtual slot should. + // The belt purge tower sizes its islands from the expanded set. A mixed slot is virtual: no + // nozzle carries it and ToolOrdering::resolve_mixed_filaments() replaces it with its + // components, so it must not count as a filament of its own. const std::vector is_mixed = {0, 0, 0, 0, 1}; const std::vector comp_strs = {"", "", "", "", "2,4"}; - // The set the sizer used to see: slots 0..4 (filaments 1..5). const std::vector assigned = {0, 1, 2, 3, 4}; const auto physical = expand_mixed_filaments(assigned, is_mixed, comp_strs); // Slot 4 dissolves into 1 and 3, which are already present. REQUIRE(physical == std::vector({0, 1, 2, 3})); - // Four physical filaments => three transitions => three islands, not four. - REQUIRE(int(physical.size()) - 1 == 3); - REQUIRE(int(assigned.size()) - 1 == 4); // what it produced before the fix - SECTION("A mixed slot whose components are otherwise unused still counts them") { - // Only the mixed slot is assigned: it must still yield its two components, - // i.e. one island, rather than collapsing to zero. + // Only the mixed slot is assigned: it must still yield its two components rather than + // collapsing to nothing. const auto only_mixed = expand_mixed_filaments({4}, is_mixed, comp_strs); REQUIRE(only_mixed == std::vector({1, 3})); - REQUIRE(int(only_mixed.size()) - 1 == 1); } SECTION("No mixed filaments anywhere leaves the set untouched") { diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 1b4927f08c..9ff2a89b33 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -1334,12 +1334,18 @@ TEST_CASE("Renamed printer/process names are normalized into compatible lists on CHECK(compatible_list(bundle.filaments, "My Filament", "compatible_printers") == (std::vector{ "New Printer", "Unknown Printer" })); - // Normalizing rewrites config in place without flagging the preset dirty. - CHECK_FALSE(bundle.prints.find_preset("My Process", false, true)->is_dirty); + // Selecting the normalized preset copies the rewritten list into the edited preset, so it is + // not reported modified. + REQUIRE(bundle.prints.select_preset_by_name("My Process", true)); + CHECK(bundle.prints.get_edited_preset().config.option("compatible_printers")->values == + std::vector{ "New Printer" }); + CHECK_FALSE(bundle.prints.current_is_dirty()); - // A system preset that already references the current name is left untouched (idempotent no-op). + // A preset that already references the current name is left untouched (idempotent no-op), so + // the selection stays unmodified. bundle.normalize_compatible_presets(); CHECK(compatible_list(bundle.prints, "My Process", "compatible_printers") == std::vector{ "New Printer" }); + CHECK_FALSE(bundle.prints.current_is_dirty()); } TEST_CASE("Renamed names are normalized into a SYSTEM preset's compatible lists", "[Preset][Rename]") @@ -1369,11 +1375,14 @@ TEST_CASE("Renamed names are normalized into a SYSTEM preset's compatible lists" CHECK(compatible_list(bundle.filaments, "System Filament", "compatible_prints") == std::vector{ "New Process" }); - // The rewrite does not flag the system preset dirty, and is idempotent. - CHECK_FALSE(bundle.filaments.find_preset("System Filament", false, true)->is_dirty); + // Selecting the normalized system preset does not report it modified, and the rewrite is + // idempotent. + REQUIRE(bundle.filaments.select_preset_by_name("System Filament", true)); + CHECK_FALSE(bundle.filaments.current_is_dirty()); bundle.normalize_compatible_presets(); CHECK(compatible_list(bundle.filaments, "System Filament", "compatible_printers") == std::vector{ "New Printer" }); + CHECK_FALSE(bundle.filaments.current_is_dirty()); } TEST_CASE("compatible_prints on SLA materials resolves against sla_prints, not prints", "[Preset][Rename]") @@ -5566,16 +5575,6 @@ TEST_CASE("Sizing down to the nozzle count plus mixes is what eats the mixed tai CHECK(bundle.num_mixed_filaments() == 0); CHECK(bundle.num_physical_filaments() == target); } - - SECTION("applied as a floor, the mix is left alone") { - if (target > bundle.filament_presets.size()) - bundle.set_num_filaments(target); - - CHECK(bundle.filament_presets.size() == 6); - CHECK(bundle.num_mixed_filaments() == 1); - CHECK(bundle.is_mixed_filament(5)); - CHECK(bundle.project_config.option("filament_mixed_components")->values[5] == "1,2"); - } } // After a published-3MF import relocated mixed-filament definitions, the freshly loaded diff --git a/tests/libslic3r/test_sketchimport.cpp b/tests/libslic3r/test_sketchimport.cpp index 3e6636fd7d..cdbf0b8296 100644 --- a/tests/libslic3r/test_sketchimport.cpp +++ b/tests/libslic3r/test_sketchimport.cpp @@ -5,7 +5,6 @@ #include #include #include "libslic3r/CAD/SketchImport.hpp" -#include "libslic3r/Utils.hpp" // resources_dir #include "test_utils.hpp" // ScopedTemporaryFile #include @@ -78,18 +77,9 @@ TEST_CASE("transform_regions moves and scales independently", "[SketchImport]") TEST_CASE("text_to_regions vectorizes glyphs with counters", "[SketchImport]") { - // Locate the bundled font; resources_dir() may be unset under ctest, so - // fall back to a cwd-relative path (tests run from the repo root). - std::string font = resources_dir().empty() - ? std::string("resources/fonts/HarmonyOS_Sans_SC_Regular.ttf") - : resources_dir() + "/fonts/HarmonyOS_Sans_SC_Regular.ttf"; - { - std::ifstream probe(font); - if (!probe.good()) { - SUCCEED("bundled font not reachable in this environment; covered live on :10"); - return; - } - } + // The bundled font, from the source tree: PROFILES_DIR is /resources/profiles. + const std::string font = std::string(PROFILES_DIR) + "/../fonts/HarmonyOS_Sans_SC_Regular.ttf"; + REQUIRE(std::ifstream(font).good()); // Bad input is rejected without throwing. REQUIRE(text_to_regions("", 10.0, font).empty()); diff --git a/tests/libslic3r/test_sketchinference.cpp b/tests/libslic3r/test_sketchinference.cpp index 6ff9768b8f..c1224829fa 100644 --- a/tests/libslic3r/test_sketchinference.cpp +++ b/tests/libslic3r/test_sketchinference.cpp @@ -33,8 +33,9 @@ TEST_CASE("inference: cursor near a line endpoint snaps Coincident-able to it", TEST_CASE("inference: endpoint beats midpoint when both are in range", "[inference]") { std::vector ents = { line({0, 0}, {2, 0}) }; - // Query equidistant-ish but closer to the endpoint: endpoint tier wins regardless. - auto s = infer_point_snap(ents, {1.9, 0.0}, 5.0); + // Query 0.2 from the midpoint (1,0) and 0.8 from the endpoint (2,0): the endpoint tier + // wins even though the midpoint is nearer. + auto s = infer_point_snap(ents, {1.2, 0.0}, 5.0, /*include_origin=*/false); CHECK(s.kind == K::Endpoint); CHECK(s.role == SketchPointRole::P1); } diff --git a/tests/libslic3r/test_sketchprofile.cpp b/tests/libslic3r/test_sketchprofile.cpp index 6c33599847..263a5fc73f 100644 --- a/tests/libslic3r/test_sketchprofile.cpp +++ b/tests/libslic3r/test_sketchprofile.cpp @@ -115,15 +115,14 @@ TEST_CASE("profile: offsetting a closed rectangle keeps it closed", "[SketchProf TEST_CASE("profile: offset outward grows the enclosed area by the right amount", "[SketchProfile]") { - // A rectangle offset outward by d is (w+2d) x (h+2d) with the corners rounded at r=d, - // so its area is w*h + 2d(w+h) + pi*d^2 whichever way the corners are healed... except - // for a sharp-corner offset, which is exactly (w+2d)*(h+2d). Either healing is defensible; - // a set of four disconnected segments is not, and that is what this measures. + // Outward is -d for a CCW loop. The miter join keeps the corners sharp, so a rectangle + // offset outward by d is one closed (w+2d) x (h+2d) loop, not four disconnected segments. const double w = 40, h = 20, d = 5; - const auto out = SketchEngine::offset_entities(rect(w, h), d); + const auto out = SketchEngine::offset_entities(rect(w, h), -d); const auto ws = SketchEngine::entities_to_wires(out, xy_plane()); REQUIRE(ws.size() == 1); REQUIRE(ws[0].Closed()); + REQUIRE_THAT(profile_area(out), WithinAbs((w + 2 * d) * (h + 2 * d), 1e-6)); } TEST_CASE("profile: offset sign is left-of-travel, so +d shrinks a CCW rectangle", "[SketchProfile]") diff --git a/tests/libslic3r/test_toolordering_nozzle_group.cpp b/tests/libslic3r/test_toolordering_nozzle_group.cpp index 33f8083125..dde3e1ee22 100644 --- a/tests/libslic3r/test_toolordering_nozzle_group.cpp +++ b/tests/libslic3r/test_toolordering_nozzle_group.cpp @@ -60,9 +60,8 @@ std::vector single_nozzle_per_extruder(int extruder_count) } } // namespace -TEST_CASE("Multi-nozzle gate predicate mirrors BambuStudio", "[ToolOrdering][H2C]") +TEST_CASE("Multi-nozzle options exist and default to one nozzle per extruder", "[ToolOrdering][H2C]") { - // The multi-nozzle gate: std::any_of(extruder_max_nozzle_count > 1). DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); auto *opt = config.option("extruder_max_nozzle_count"); @@ -72,15 +71,11 @@ TEST_CASE("Multi-nozzle gate predicate mirrors BambuStudio", "[ToolOrdering][H2C // 3mf projects round-trip the per-extruder nozzle inventory (GUI producers wire it later). REQUIRE(config.option("extruder_nozzle_stats") != nullptr); - auto has_multiple_nozzle = [](const std::vector &values) { - return std::any_of(values.begin(), values.end(), [](int v) { return v > 1; }); - }; - - // Default for every existing printer: 1 nozzle per extruder => gate is closed. - REQUIRE_FALSE(has_multiple_nozzle(opt->values)); - - // Synthetic H2C-like machine: extruder 1 is a 6-nozzle cluster => gate opens. - REQUIRE(has_multiple_nozzle(std::vector{1, 6})); + // Default for every existing printer: 1 nozzle per extruder, so the multi-nozzle gate + // (any extruder_max_nozzle_count > 1) stays closed. + REQUIRE_FALSE(opt->values.empty()); + for (int count : opt->values) + CHECK(count == 1); } TEST_CASE("Single-nozzle grouping: every filament maps to its extruder nozzle", "[ToolOrdering][H2C]") diff --git a/tests/libslic3r/test_voronoi.cpp b/tests/libslic3r/test_voronoi.cpp index a08b84d3eb..46570440e0 100644 --- a/tests/libslic3r/test_voronoi.cpp +++ b/tests/libslic3r/test_voronoi.cpp @@ -198,8 +198,11 @@ TEST_CASE("Voronoi missing edges - Alessandro gapfill 12707", "[Voronoi]") REQUIRE(intersecting_edges({ poly }).empty()); Lines lines = to_lines(poly); - BoostVD vd; - construct_voronoi(lines.begin(), lines.end(), &vd); + VD vd; + vd.construct_voronoi(lines.begin(), lines.end()); + // Orca's detector finds nothing wrong with this input, so no repair is needed. + CHECK(VD::detect_known_issues(vd, lines.begin(), lines.end()) == VD::IssueType::NO_ISSUE_DETECTED); + CHECK(vd.get_state() == VD::State::REPAIR_NOT_NEEDED); #ifdef VORONOI_DEBUG_OUT dump_voronoi_to_svg(debug_out_path("voronoi-lines.svg").c_str(), @@ -305,9 +308,16 @@ TEST_CASE("Voronoi weirdness", "[Voronoi]") } #endif - BoostVD vd; + VD vd; Lines lines = to_lines(poly); - construct_voronoi(lines.begin(), lines.end(), &vd); + vd.construct_voronoi(lines.begin(), lines.end()); + // Unrepaired, the diagram is missing a Voronoi vertex. construct_voronoi detects it and repairs it + // by rotating the input. + VD raw_vd; + raw_vd.construct_voronoi(lines.begin(), lines.end(), false); + CHECK(VD::detect_known_issues(raw_vd, lines.begin(), lines.end()) == VD::IssueType::MISSING_VORONOI_VERTEX); + CHECK(vd.get_state() == VD::State::REPAIR_SUCCESSFUL); + CHECK(vd.get_issue_type() == VD::IssueType::NO_ISSUE_DETECTED); #ifdef VORONOI_DEBUG_OUT dump_voronoi_to_svg(debug_out_path("voronoi-weirdness.svg").c_str(), @@ -347,9 +357,8 @@ TEST_CASE("Voronoi division by zero 12903", "[Voronoi]") // https://svn.boost.org/trac10/ticket/12139 // Funny sample from a dental industry? -// Vojtech confirms this test fails and rightly so, because the input data contain self intersections. -// This test is suppressed. -TEST_CASE("Voronoi NaN coordinates 12139", "[Voronoi][.][!mayfail]") +// The input data contain self intersections, so no valid Voronoi diagram can be built from it. +TEST_CASE("Voronoi NaN coordinates 12139", "[Voronoi]") { Lines lines = { { { 260500,1564400 }, { 261040,1562960 } }, @@ -1327,8 +1336,12 @@ TEST_CASE("Voronoi NaN coordinates 12139", "[Voronoi][.][!mayfail]") } #endif - BoostVD vd; - construct_voronoi(lines.begin(), lines.end(), &vd); + VD vd; + vd.construct_voronoi(lines.begin(), lines.end()); + // The input self-intersects, so rotation cannot repair the diagram. It must be reported as invalid, + // which is what MedialAxis::build checks before falling back to a morphological closing. + CHECK(vd.get_state() == VD::State::REPAIR_UNSUCCESSFUL); + CHECK_FALSE(vd.is_valid()); for (const auto& edge : vd.edges()) if (edge.is_finite()) { @@ -1935,7 +1948,7 @@ TEST_CASE("Voronoi skeleton", "[VoronoiSkeleton]") } // Simple detection with complexity N^2 if there is any point in the input polygons that doesn't have Voronoi vertex. -[[maybe_unused]] static bool has_missing_voronoi_vertices(const Polygons &polygons, const VD &vd) +static bool has_missing_voronoi_vertices(const Polygons &polygons, const VD &vd) { auto are_equal = [](const VD::vertex_type v, const Point &p) { return (Vec2d(v.x(), v.y()) - p.cast()).norm() <= SCALED_EPSILON; }; @@ -1957,7 +1970,7 @@ TEST_CASE("Voronoi skeleton", "[VoronoiSkeleton]") // into two parts. Even we add more points to the edge, and then for these points, the Voronoin vertex is also missing. An // infinity-edge passes through the missing Voronoi vertex. Therefore, this missing Voronoi vertex and edge can be reconstructed // using the intersection between the infinity-edge with the input polygon. -// Rotation of the polygon solves this problem. +// VoronoiDiagram::construct_voronoi repairs it by rotating the input. TEST_CASE("Voronoi missing vertex 1", "[VoronoiMissingVertex1]") { Polygon poly = { @@ -1981,7 +1994,14 @@ TEST_CASE("Voronoi missing vertex 1", "[VoronoiMissingVertex1]") dump_voronoi_to_svg(debug_out_path("voronoi-missing-vertex1-out.svg").c_str(), vd, Points(), lines); #endif -// REQUIRE(!has_missing_voronoi_vertices({poly}, vd)); + // Unrepaired, the diagram shows the defect. construct_voronoi detects it and repairs it by + // rotating the input. + VD raw_vd; + raw_vd.construct_voronoi(lines.begin(), lines.end(), false); + CHECK(has_missing_voronoi_vertices({poly}, raw_vd)); + CHECK(vd.get_state() == VD::State::REPAIR_SUCCESSFUL); + CHECK(vd.get_issue_type() == VD::IssueType::NO_ISSUE_DETECTED); + CHECK_FALSE(has_missing_voronoi_vertices({poly}, vd)); } // This case is composed of two square polygons (contour and hole), and again one of the edges is divided into two parts by a @@ -1989,7 +2009,7 @@ TEST_CASE("Voronoi missing vertex 1", "[VoronoiMissingVertex1]") // that for this case, through the missing Voronoi vertex is passing a finite edge between two internal Voronoin vertices. // Therefore, this missing Voronoi vertex and edge can be reconstructed using the intersection between the finite edge with the // input polygon. -// Rotation of the polygons solves this problem. +// VoronoiDiagram::construct_voronoi repairs it by rotating the input. TEST_CASE("Voronoi missing vertex 2", "[VoronoiMissingVertex2]") { Polygons poly = { @@ -2021,13 +2041,20 @@ TEST_CASE("Voronoi missing vertex 2", "[VoronoiMissingVertex2]") dump_voronoi_to_svg(debug_out_path("voronoi-missing-vertex2-out.svg").c_str(), vd, Points(), lines); #endif -// REQUIRE(!has_missing_voronoi_vertices(poly, vd)); + // Unrepaired, the diagram shows the defect. construct_voronoi detects it and repairs it by + // rotating the input. + VD raw_vd; + raw_vd.construct_voronoi(lines.begin(), lines.end(), false); + CHECK(has_missing_voronoi_vertices(poly, raw_vd)); + CHECK(vd.get_state() == VD::State::REPAIR_SUCCESSFUL); + CHECK(vd.get_issue_type() == VD::IssueType::NO_ISSUE_DETECTED); + CHECK_FALSE(has_missing_voronoi_vertices(poly, vd)); } // This case is composed of two polygons, and again one of the edges is divided into two parts by a point that lies on this edge, // and for this point is Voronoi vertex missing. A difference between the previous cases and this case through the missing // Voronoi vertex is passing finite edge between one inner Voronoi vertex and one outer Voronoi vertex. -// Rotating the polygon also help solve this problem. +// VoronoiDiagram::construct_voronoi repairs it by rotating the input. TEST_CASE("Voronoi missing vertex 3", "[VoronoiMissingVertex3]") { Polygons poly = { @@ -2055,20 +2082,27 @@ TEST_CASE("Voronoi missing vertex 3", "[VoronoiMissingVertex3]") // polygons_rotate(poly, PI/180); // polygons_rotate(poly, PI/6); - BoostVD vd; + VD vd; Lines lines = to_lines(poly); - construct_voronoi(lines.begin(), lines.end(), &vd); + vd.construct_voronoi(lines.begin(), lines.end()); #ifdef VORONOI_DEBUG_OUT dump_voronoi_to_svg(debug_out_path("voronoi-missing-vertex3-out.svg").c_str(), vd, Points(), lines); #endif -// REQUIRE(!has_missing_voronoi_vertices(poly, vd)); + // Unrepaired, the diagram shows the defect. construct_voronoi detects it and repairs it by + // rotating the input. + VD raw_vd; + raw_vd.construct_voronoi(lines.begin(), lines.end(), false); + CHECK(has_missing_voronoi_vertices(poly, raw_vd)); + CHECK(vd.get_state() == VD::State::REPAIR_SUCCESSFUL); + CHECK(vd.get_issue_type() == VD::IssueType::NO_ISSUE_DETECTED); + CHECK_FALSE(has_missing_voronoi_vertices(poly, vd)); } // In this case, the Voronoi vertex (146873, -146873) is included twice. // Also, near to those duplicate Voronoi vertices is another Voronoi vertex (146872, -146872). // Rotating the polygon will help solve this problem, but then there arise three very close Voronoi vertices. -// Rotating of the input polygon will help solve this problem. +// VoronoiDiagram::construct_voronoi repairs it by rotating the input. TEST_CASE("Duplicate Voronoi vertices", "[Voronoi]") { Polygon poly = { @@ -2095,7 +2129,7 @@ TEST_CASE("Duplicate Voronoi vertices", "[Voronoi]") dump_voronoi_to_svg(debug_out_path("voronoi-duplicate-vertices-out.svg").c_str(), vd, Points(), lines); #endif - [[maybe_unused]] auto has_duplicate_vertices = [](const VD &vd) -> bool { + auto has_duplicate_vertices = [](const VD &vd) -> bool { std::vector vertices; for (const auto &vertex : vd.vertices()) vertices.emplace_back(Vec2d(vertex.x(), vertex.y())); @@ -2104,14 +2138,21 @@ TEST_CASE("Duplicate Voronoi vertices", "[Voronoi]") return std::unique(vertices.begin(), vertices.end()) != vertices.end(); }; -// REQUIRE(!has_duplicate_vertices(vd)); + // Unrepaired, the diagram shows the defect. construct_voronoi detects it and repairs it by + // rotating the input. + VD raw_vd; + raw_vd.construct_voronoi(lines.begin(), lines.end(), false); + CHECK(has_duplicate_vertices(raw_vd)); + CHECK(vd.get_state() == VD::State::REPAIR_SUCCESSFUL); + CHECK(vd.get_issue_type() == VD::IssueType::NO_ISSUE_DETECTED); + CHECK_FALSE(has_duplicate_vertices(vd)); } // In this case, there are three very close Voronoi vertices like in the previous test case after rotation. There is also one // missing Voronoi vertex. One infinity-edge (after clip [(146872, -70146871), (146872, -146871)]) passes through this missing // Voronoi vertex. This infinite edge [(146872, -70146871), (146872, -146871)] and edge [(146873, -146873), (0, 0)] are intersecting. // They intersect probably because the three points are very close to each other, with a combination of the missing Voronoi vertex. -// Rotating of the input polygon will help solve this problem. +// VoronoiDiagram::construct_voronoi repairs it by rotating the input. TEST_CASE("Intersecting Voronoi edges", "[Voronoi]") { Polygon poly = { @@ -2135,7 +2176,7 @@ TEST_CASE("Intersecting Voronoi edges", "[Voronoi]") dump_voronoi_to_svg(debug_out_path("voronoi-intersecting-edges-out.svg").c_str(), vd, Points(), lines); #endif - [[maybe_unused]] auto has_intersecting_edges = [](const Polygon &poly, const VD &vd) -> bool { + auto has_intersecting_edges = [](const Polygon &poly, const VD &vd) -> bool { BoundingBox bbox = get_extents(poly); const double bbox_dim_max = double(std::max(bbox.size().x(), bbox.size().y())); @@ -2166,5 +2207,12 @@ TEST_CASE("Intersecting Voronoi edges", "[Voronoi]") return false; }; -// REQUIRE(!has_intersecting_edges(poly, vd)); + // Unrepaired, the diagram shows the defect. construct_voronoi detects it and repairs it by + // rotating the input. + VD raw_vd; + raw_vd.construct_voronoi(lines.begin(), lines.end(), false); + CHECK(has_intersecting_edges(poly, raw_vd)); + CHECK(vd.get_state() == VD::State::REPAIR_SUCCESSFUL); + CHECK(vd.get_issue_type() == VD::IssueType::NO_ISSUE_DETECTED); + CHECK_FALSE(has_intersecting_edges(poly, vd)); } diff --git a/tests/slic3rutils/test_plugin_capability_config.cpp b/tests/slic3rutils/test_plugin_capability_config.cpp index bbd0368d26..b86eda8d22 100644 --- a/tests/slic3rutils/test_plugin_capability_config.cpp +++ b/tests/slic3rutils/test_plugin_capability_config.cpp @@ -8,6 +8,7 @@ #include #include +#include "catch2/catch_message.hpp" #include "plugin_test_utils.hpp" #include #include @@ -17,6 +18,12 @@ #include #include +#include +#include + +#include +#include +#include #include #include #include @@ -105,6 +112,72 @@ PluginCapabilityId capability_id(PluginCapabilityType type, const char* name, co return {type, name, plugin_key}; } +// restore_config_response looks the capability up in PluginManager, so the restore tests load a real +// plugin rather than building a detached capability. +const char* const RESTORE_PLUGIN_KEY = "Restore_Plugin"; + +const char* const RESTORE_PLUGIN_SOURCE = R"PY(# /// script +# requires-python = ">=3.12" +# +# [tool.orcaslicer.plugin] +# name = "Restore Plugin" +# description = "Restore defaults fixture" +# author = "OrcaSlicer" +# version = "1.0" +# type = "script" +# /// +import orca + +class Target(orca.script.ScriptPluginCapabilityBase): + def get_name(self): return "Target" + def execute(self, ctx): return orca.ExecutionResult.success() + def get_default_config(self): return {"speed": 1} + +class Bystander(orca.script.ScriptPluginCapabilityBase): + def get_name(self): return "Bystander" + def execute(self, ctx): return orca.ExecutionResult.success() + def get_default_config(self): return {"speed": 2} + +# Loading seeds defaults through get_default_config and fails the plugin if it raises, so this one +# answers the load and raises from then on. +class Raising(orca.script.ScriptPluginCapabilityBase): + calls = 0 + def get_name(self): return "Raising" + def execute(self, ctx): return orca.ExecutionResult.success() + def get_default_config(self): + Raising.calls += 1 + if Raising.calls > 1: + raise RuntimeError("boom") + return {} + +@orca.plugin +class RestorePackage(orca.base): + def register_capabilities(self): + orca.register_capability(Target) + orca.register_capability(Bystander) + orca.register_capability(Raising) +)PY"; + +// Writes the restore fixture into {data_dir}/orca_plugins and loads it. Call without holding the GIL: +// the load runs on a worker thread. +bool load_restore_plugin(const ScopedDataDir& data_dir_guard) +{ + const boost::filesystem::path plugin_dir = data_dir_guard.plugins_dir() / RESTORE_PLUGIN_KEY; + boost::filesystem::create_directories(plugin_dir); + { + std::ofstream out((plugin_dir / (std::string(RESTORE_PLUGIN_KEY) + ".py")).string(), std::ios::binary); + out << RESTORE_PLUGIN_SOURCE; + } + + PluginManager& manager = PluginManager::instance(); + manager.discover_plugins(/*async=*/false, /*clear=*/true); + manager.load_plugin(RESTORE_PLUGIN_KEY, /*skip_deps=*/true); + std::string error; + const bool loaded = manager.wait_for_plugin_load(RESTORE_PLUGIN_KEY, std::chrono::seconds(120), error); + INFO("load error: " << error); + return loaded && error.empty(); +} + } // namespace TEST_CASE("Capability config API is exposed on every Python capability", "[PluginConfig][Python]") @@ -357,26 +430,25 @@ TEST_CASE("Restoring defaults overwrites only the target capability", "[PluginCo ScopedPluginManager plugin_system; // declared first: destroyed last if (!plugin_system.initialized) SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error()); - py::gil_scoped_acquire gil; // released before plugin_system's destructor shuts Python down ScopedDataDir data_dir_guard("plugin-config-py-restore"); host_config().load(); + REQUIRE(load_restore_plugin(data_dir_guard)); - const std::string defaults_body = " def get_name(self): return 'cap'\n" - " def get_default_config(self): return {'speed': 1}\n"; - py::object target = make_capability("RestoreTargetCap", defaults_body, "plugin_a", "cap_a"); - py::object bystander = make_capability("RestoreBystanderCap", defaults_body, "plugin_b", "cap_a"); - + const PluginCapabilityId target = capability_id(PluginCapabilityType::Script, "Target", RESTORE_PLUGIN_KEY); + const PluginCapabilityId bystander = capability_id(PluginCapabilityType::Script, "Bystander", RESTORE_PLUGIN_KEY); const json edited = json{{"speed", 99}}; - REQUIRE(py_save_config(target, edited)); - REQUIRE(py_save_config(bystander, edited)); + REQUIRE(host_config().store_capability_config(target, edited)); + REQUIRE(host_config().store_capability_config(bystander, edited)); - // What PluginsDialog::restore_capability_config does: ask the capability, store the answer. - auto iface = as_interface(target); - REQUIRE(host_config().store_capability_config(capability_id(PluginCapabilityType::Script, "cap_a", "plugin_a"), iface->get_default_config())); + const json response = PluginConfig::restore_config_response(target); - CHECK(host_config().get_config(capability_id(PluginCapabilityType::Script, "cap_a", "plugin_a"))->config == json{{"speed", 1}}); - CHECK(host_config().get_config(capability_id(PluginCapabilityType::Script, "cap_a", "plugin_b"))->config == json{{"speed", 99}}); + CHECK(response["ok"] == true); + CHECK(response["config"] == json{{"speed", 1}}); + CHECK(host_config().get_config(target)->config == json{{"speed", 1}}); + CHECK(host_config().get_config(bystander)->config == edited); + + PluginManager::instance().unload_plugin(RESTORE_PLUGIN_KEY); } TEST_CASE("A raising get_default_config leaves the stored config untouched", "[PluginConfig][Python]") @@ -384,24 +456,21 @@ TEST_CASE("A raising get_default_config leaves the stored config untouched", "[P ScopedPluginManager plugin_system; // declared first: destroyed last if (!plugin_system.initialized) SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error()); - py::gil_scoped_acquire gil; // released before plugin_system's destructor shuts Python down ScopedDataDir data_dir_guard("plugin-config-py-restore-raise"); host_config().load(); + REQUIRE(load_restore_plugin(data_dir_guard)); - py::object cap = make_capability("RaisingDefaultsCap", - " def get_name(self): return 'cap_a'\n" - " def get_default_config(self): raise RuntimeError('boom')\n", - "plugin_a", "cap_a"); + const PluginCapabilityId raising = capability_id(PluginCapabilityType::Script, "Raising", RESTORE_PLUGIN_KEY); + REQUIRE(host_config().store_capability_config(raising, json{{"keep", "me"}})); - REQUIRE(py_save_config(cap, json{{"keep", "me"}})); + const json response = PluginConfig::restore_config_response(raising); - auto iface = as_interface(cap); - REQUIRE(iface); - CHECK_THROWS_AS(iface->get_default_config(), py::error_already_set); + CHECK(response["ok"] == false); + CHECK_FALSE(response["error"].get().empty()); + CHECK(host_config().get_config(raising)->config == json{{"keep", "me"}}); - // The dialog stores nothing when the hook throws: a broken plugin must not wipe user settings. - CHECK(host_config().get_config(capability_id(PluginCapabilityType::Script, "cap_a", "plugin_a"))->config == json{{"keep", "me"}}); + PluginManager::instance().unload_plugin(RESTORE_PLUGIN_KEY); } TEST_CASE("A raising config UI hook surfaces as an exception the host can catch", "[PluginConfig][Python]") diff --git a/tests/slic3rutils/test_plugin_cloud_metadata.cpp b/tests/slic3rutils/test_plugin_cloud_metadata.cpp index 95ae6fcd43..013e6e1e9e 100644 --- a/tests/slic3rutils/test_plugin_cloud_metadata.cpp +++ b/tests/slic3rutils/test_plugin_cloud_metadata.cpp @@ -76,12 +76,12 @@ TEST_CASE("plugin latest version uses the authoritative catalog field", "[Plugin { PluginDescriptor descriptor; descriptor.version = "1.3.0"; - descriptor.latest_version = "1.3.0"; + descriptor.latest_version = "1.4.0"; PluginChangelog changelog; changelog.version = "1.2.0"; descriptor.changelog.push_back(changelog); - CHECK(descriptor.latest_available_version() == "1.3.0"); + CHECK(descriptor.latest_available_version() == "1.4.0"); } TEST_CASE("plugin latest version falls back to the descriptor version", "[PluginDescriptor]") diff --git a/tests/slic3rutils/test_slicing_pipeline_bindings.cpp b/tests/slic3rutils/test_slicing_pipeline_bindings.cpp index d05589b552..795d66c2c5 100644 --- a/tests/slic3rutils/test_slicing_pipeline_bindings.cpp +++ b/tests/slic3rutils/test_slicing_pipeline_bindings.cpp @@ -615,12 +615,19 @@ TEST_CASE("orca.host: SurfaceCollection.set mutates geometry; lslices via make_s .attr("expolygon").attr("contour").attr("as_array")().cast(); CHECK(c.shape(0) == 4); - // lslices are derived: make_slices() re-derives them + refreshes the bbox cache. + // lslices are derived: make_slices() re-derives them from the region slices and refreshes the + // bbox cache. TestLayer layer; + layer.add_region(nullptr); py::object ly = py::cast(static_cast(&layer), py::return_value_policy::reference); - // (A hand-built layer has no regions, so make_slices() yields empty lslices — still null-safe.) + ly.attr("regions")().cast()[0].attr("slices").attr("set")(expolys, ST.attr("stInternalSolid")); + REQUIRE(layer.lslices.empty()); ly.attr("make_slices")(); - CHECK(layer.lslices_bboxes.size() == layer.lslices.size()); + REQUIRE(layer.lslices.size() == 1); + CHECK_THAT(layer.lslices.front().area(), WithinRel((double) s * (double) s, 1e-9)); + REQUIRE(layer.lslices_bboxes.size() == 1); + CHECK(layer.lslices_bboxes.front().min == Point(0, 0)); + CHECK(layer.lslices_bboxes.front().max == Point(s, s)); } TEST_CASE("orca.host ExPolygon in-place transforms + SurfaceCollection.append (sample ops)", "[slicing_pipeline]") {