// Test file for Arachne wall generation // // Tests for duplicate/coinciding wall segment detection in Arachne output. // // This test reproduces an issue where Arachne generates duplicate extrusion // segments at certain min_bead_width settings. The test uses a polygon with // an outer rectangle (0,0)-(20,20) and an inner cutout (0.5,0.5)-(19.5,19.5). // // With precise_outer_wall enabled and min_bead_width at 50% (0.20mm), Arachne // generates two separate closed contours that share a coinciding edge at y=19.75. // At 60% (0.24mm), Arachne handles this differently and avoids the duplicate. // // Parameters are based on "0.28mm Extra Draft @BBL X1C" profile with: // - 0.4mm nozzle, 0.28mm layer height // - outer_wall_line_width: 0.42mm, inner_wall_line_width: 0.45mm // - wall_loops: 2, precise_outer_wall: enabled #include #include "libslic3r/Arachne/WallToolPaths.hpp" #include "libslic3r/Arachne/utils/ExtrusionLine.hpp" #include "libslic3r/Polygon.hpp" #include "libslic3r/ExPolygon.hpp" #include "libslic3r/ClipperUtils.hpp" #include "libslic3r/Point.hpp" #include using namespace Slic3r; using namespace Slic3r::Arachne; namespace { // Represents a segment with direction-independent comparison struct Segment { Point from; Point to; size_t inset_idx; // Normalize segment so that the "smaller" point comes first // This allows direction-independent comparison Segment normalized() const { if (from < to || (from.x() == to.x() && from.y() < to.y())) { return *this; } return {to, from, inset_idx}; } bool operator<(const Segment& other) const { auto a = normalized(); auto b = other.normalized(); if (a.inset_idx != b.inset_idx) return a.inset_idx < b.inset_idx; if (a.from != b.from) return a.from < b.from; return a.to < b.to; } bool operator==(const Segment& other) const { auto a = normalized(); auto b = other.normalized(); return a.inset_idx == b.inset_idx && a.from == b.from && a.to == b.to; } }; // Check if two points are approximately equal within tolerance bool points_approx_equal(const Point& a, const Point& b, coord_t tolerance) { return std::abs(a.x() - b.x()) <= tolerance && std::abs(a.y() - b.y()) <= tolerance; } // Check if two segments are approximately equal (direction-independent) bool segments_approx_equal(const Segment& a, const Segment& b, coord_t tolerance) { if (a.inset_idx != b.inset_idx) return false; // Check both directions bool same_dir = points_approx_equal(a.from, b.from, tolerance) && points_approx_equal(a.to, b.to, tolerance); bool reverse_dir = points_approx_equal(a.from, b.to, tolerance) && points_approx_equal(a.to, b.from, tolerance); return same_dir || reverse_dir; } // Extract all segments from toolpaths (all inset indices) std::vector extract_all_segments(const std::vector& toolpaths) { std::vector segments; for (const auto& inset : toolpaths) { for (const auto& line : inset) { if (line.junctions.size() < 2) continue; for (size_t i = 0; i + 1 < line.junctions.size(); ++i) { segments.push_back({ line.junctions[i].p, line.junctions[i + 1].p, line.inset_idx }); } } } return segments; } // Find duplicate segments within tolerance std::vector> find_duplicate_segments( const std::vector& segments, coord_t tolerance) { std::vector> duplicates; for (size_t i = 0; i < segments.size(); ++i) { for (size_t j = i + 1; j < segments.size(); ++j) { if (segments_approx_equal(segments[i], segments[j], tolerance)) { duplicates.emplace_back(segments[i], segments[j]); } } } return duplicates; } // Create params matching "0.28mm Extra Draft @BBL X1C" profile WallToolPathsParams make_bbl_x1c_028_params(int min_bead_width_percent) { constexpr double nozzle_diameter = 0.4; WallToolPathsParams params; params.min_bead_width = float(min_bead_width_percent / 100.0 * nozzle_diameter); params.min_feature_size = float(0.25 * nozzle_diameter); params.wall_transition_filter_deviation = float(0.25 * nozzle_diameter); params.wall_transition_length = float(1.0 * nozzle_diameter); params.wall_transition_angle = 10.0f; params.wall_distribution_count = 1; params.min_length_factor = 0.5f; params.is_top_or_bottom_layer = false; return params; } // Run Arachne wall generation test with specified min_bead_width percentage // Returns the number of duplicate segments found size_t run_arachne_test(int min_bead_width_percent) { constexpr double layer_height = 0.28; constexpr double ext_perimeter_width_mm = 0.42; constexpr double perimeter_width_mm = 0.45; // Spacing calculation: width - height * (1 - PI/4) constexpr double spacing_factor = 1.0 - 0.25 * M_PI; double ext_perimeter_spacing_mm = ext_perimeter_width_mm - layer_height * spacing_factor; double perimeter_spacing_mm = perimeter_width_mm - layer_height * spacing_factor; coord_t ext_perimeter_width = scaled(ext_perimeter_width_mm); coord_t ext_perimeter_spacing = scaled(ext_perimeter_spacing_mm); coord_t perimeter_spacing = scaled(perimeter_spacing_mm); coord_t bead_width_0 = ext_perimeter_spacing; coord_t bead_width_x = perimeter_spacing; size_t inset_count = 2; // precise_outer_wall enabled float precise_offset = -float(ext_perimeter_width - ext_perimeter_spacing); coord_t wall_0_inset = -coord_t(ext_perimeter_width / 2 - ext_perimeter_spacing / 2); auto params = make_bbl_x1c_028_params(min_bead_width_percent); // Test polygon: outer rectangle with inner cutout creating 0.5mm frame Polygon outer_raw; outer_raw.points.emplace_back(Point::new_scale(0.0, 0.0)); outer_raw.points.emplace_back(Point::new_scale(20.0, 0.0)); outer_raw.points.emplace_back(Point::new_scale(20.0, 20.0)); outer_raw.points.emplace_back(Point::new_scale(0.0, 20.0)); Polygon inner_raw; inner_raw.points.emplace_back(Point::new_scale(0.5, 0.5)); inner_raw.points.emplace_back(Point::new_scale(0.5, 19.5)); inner_raw.points.emplace_back(Point::new_scale(19.5, 19.5)); inner_raw.points.emplace_back(Point::new_scale(19.5, 0.5)); ExPolygon input_expolygon; input_expolygon.contour = outer_raw; input_expolygon.holes.push_back(inner_raw); ExPolygons offset_result = offset_ex(input_expolygon, precise_offset); Polygons outline; for (const auto& expoly : offset_result) { outline.push_back(expoly.contour); for (const auto& hole : expoly.holes) { outline.push_back(hole); } } WallToolPaths wallToolPaths(outline, bead_width_0, bead_width_x, inset_count, wall_0_inset, layer_height, params); auto toolpaths = wallToolPaths.getToolPaths(); auto all_segments = extract_all_segments(toolpaths); auto duplicates = find_duplicate_segments(all_segments, scaled(0.1)); return duplicates.size(); } } // anonymous namespace TEST_CASE("Arachne wall generation - 50% min_bead_width", "[Arachne]") { size_t duplicates = run_arachne_test(50); REQUIRE(duplicates == 0); } TEST_CASE("Arachne wall generation - 60% min_bead_width", "[Arachne]") { size_t duplicates = run_arachne_test(60); REQUIRE(duplicates == 0); }