mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-27 12:52:07 +00:00
Merge branch 'main' into feature/automation
This commit is contained in:
@@ -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
|
||||
|
||||
209
tests/libslic3r/test_arachne_walls.cpp
Normal file
209
tests/libslic3r/test_arachne_walls.cpp
Normal file
@@ -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 <catch2/catch_all.hpp>
|
||||
|
||||
#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 <cmath>
|
||||
|
||||
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<Segment> extract_all_segments(const std::vector<VariableWidthLines>& toolpaths) {
|
||||
std::vector<Segment> 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<std::pair<Segment, Segment>> find_duplicate_segments(
|
||||
const std::vector<Segment>& segments,
|
||||
coord_t tolerance)
|
||||
{
|
||||
std::vector<std::pair<Segment, Segment>> 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<coord_t>(ext_perimeter_width_mm);
|
||||
coord_t ext_perimeter_spacing = scaled<coord_t>(ext_perimeter_spacing_mm);
|
||||
coord_t perimeter_spacing = scaled<coord_t>(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<coord_t>(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);
|
||||
}
|
||||
@@ -315,6 +315,54 @@ SCENARIO("update_non_diff_values_to_base_config preserves child vectors when chi
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("update_diff_values_to_child_config tolerates legacy machine-limit vector sizes",
|
||||
"[Config][Variant]") {
|
||||
// Regression: loading a user printer preset that inherits a non-BBL multi-extruder base and
|
||||
// overrides stride-2 machine limits used to throw in ConfigOptionVector::set_only_diff
|
||||
// ("invalid diff_index size"). The base's machine-limit vectors get length-extended by the
|
||||
// nozzle count while it carries no printer_extruder_variant, so the base length (nozzles*2)
|
||||
// no longer matches variant_index.size()*2. The throw was caught upstream and DELETED the
|
||||
// user's preset file. The merge must instead degrade gracefully.
|
||||
GIVEN("A 4-nozzle parent with stride-2 limits extended to nozzles*2 but no printer_extruder_variant") {
|
||||
Slic3r::DynamicPrintConfig parent;
|
||||
Slic3r::DynamicPrintConfig child;
|
||||
|
||||
parent.set_key_value("nozzle_diameter",
|
||||
new Slic3r::ConfigOptionFloats({0.4, 0.4, 0.4, 0.4}));
|
||||
parent.set_key_value("machine_max_acceleration_x",
|
||||
new Slic3r::ConfigOptionFloats({25000, 25000, 25000, 25000, 25000, 25000, 25000, 25000}));
|
||||
|
||||
// Child user preset declares 4 extruder variants and overrides the machine limit.
|
||||
child.set_key_value("printer_extruder_id",
|
||||
new Slic3r::ConfigOptionInts({1, 2, 3, 4}));
|
||||
child.set_key_value("printer_extruder_variant",
|
||||
new Slic3r::ConfigOptionStrings({"Direct Drive Standard", "Direct Drive Standard",
|
||||
"Direct Drive Standard", "Direct Drive Standard"}));
|
||||
child.set_key_value("machine_max_acceleration_x",
|
||||
new Slic3r::ConfigOptionFloats({8000, 8000, 8000, 8000, 8000, 8000, 8000, 8000}));
|
||||
|
||||
WHEN("update_diff_values_to_child_config merges the child overrides") {
|
||||
std::string id_name = "printer_extruder_id";
|
||||
std::string var_name = "printer_extruder_variant";
|
||||
|
||||
THEN("it does not throw on the legacy size mismatch") {
|
||||
REQUIRE_NOTHROW(parent.update_diff_values_to_child_config(
|
||||
child, id_name, var_name,
|
||||
Slic3r::printer_options_with_variant_1,
|
||||
Slic3r::printer_options_with_variant_2));
|
||||
|
||||
AND_THEN("the child's overridden machine limit is preserved") {
|
||||
auto* mx = parent.option<Slic3r::ConfigOptionFloats>("machine_max_acceleration_x");
|
||||
REQUIRE(mx != nullptr);
|
||||
REQUIRE(mx->values.size() >= 2);
|
||||
REQUIRE_THAT(mx->values[0], Catch::Matchers::WithinAbs(8000.0, 1e-6));
|
||||
REQUIRE_THAT(mx->values[1], Catch::Matchers::WithinAbs(8000.0, 1e-6));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SCENARIO("DynamicPrintConfig JSON serialization", "[Config]") {
|
||||
// WHEN("DynamicPrintConfig is serialized and deserialized") {
|
||||
// auto now = std::chrono::high_resolution_clock::now();
|
||||
|
||||
Reference in New Issue
Block a user