diff --git a/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.cpp b/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.cpp index 4c6dbb2706..5bef586d66 100644 --- a/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.cpp +++ b/src/libslic3r/Arachne/BeadingStrategy/WideningBeadingStrategy.cpp @@ -26,7 +26,13 @@ std::string WideningBeadingStrategy::toString() const WideningBeadingStrategy::Beading WideningBeadingStrategy::compute(coord_t thickness, coord_t bead_count) const { - if (thickness < optimal_width) { + // Use getTransitionThickness(1) to determine if this is a thin wall that should produce + // a single bead. This ensures consistency with getOptimalBeadCount() which uses the same + // threshold (via RedistributeBeadingStrategy) to decide between 1 and 2 beads. + // Previously used optimal_width which could differ from the outer wall width used in + // bead count calculations, causing inconsistency where bead_count=2 was requested but + // only 1 bead was produced. + if (thickness < getTransitionThickness(1)) { Beading ret; ret.total_thickness = thickness; if (thickness >= min_input_width) { diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index fa48ae928c..1824df8dc0 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -5,6 +5,7 @@ add_executable(${_TEST_NAME}_tests test_3mf.cpp test_aabbindirect.cpp test_appconfig.cpp + test_arachne_walls.cpp test_bambu_networking.cpp test_clipper_offset.cpp test_clipper_utils.cpp diff --git a/tests/libslic3r/test_arachne_walls.cpp b/tests/libslic3r/test_arachne_walls.cpp new file mode 100644 index 0000000000..dc5d7143f6 --- /dev/null +++ b/tests/libslic3r/test_arachne_walls.cpp @@ -0,0 +1,209 @@ +// 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); +}