Merge branch 'main' into feat/printer-agent-impl

This commit is contained in:
Ian Chua
2026-08-25 13:47:11 +08:00
committed by GitHub
76 changed files with 5075 additions and 859 deletions

View File

@@ -755,6 +755,9 @@ struct SparseInfillShape {
size_t sharp_turns { 0 };
size_t path_count { 0 };
double length { 0. };
// Digest of every point in the order it is printed. The counts above all survive the same
// extrusions being joined into different polylines, so only this tells two such fills apart.
uint64_t sequence { 14695981039346656037ull };
};
static SparseInfillShape sparse_infill_shape(const Print &print)
@@ -767,6 +770,9 @@ static SparseInfillShape sparse_infill_shape(const Print &print)
const Points3 &pts = path.polyline.points;
++shape.path_count;
shape.point_count += pts.size();
for (const auto &pt : pts)
for (const coord_t coordinate : {pt.x(), pt.y(), pt.z()})
shape.sequence = (shape.sequence ^ uint64_t(coordinate)) * 1099511628211ull;
for (size_t i = 1; i < pts.size(); ++i)
shape.length += (pts[i] - pts[i - 1]).head<2>().cast<double>().norm();
for (size_t i = 1; i + 1 < pts.size(); ++i) {
@@ -793,6 +799,33 @@ static SparseInfillShape sparse_infill_shape(const Print &print)
return shape;
}
TEST_CASE("Lightning infill slices the same model the same way twice", "[Fill][Regression]")
{
// Slicing twice in one process catches a generator that carries state from one slice to the
// next, or whose result depends on how the parallel layer fill interleaves.
auto shape = [] {
Print print;
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print,
{{"sparse_infill_pattern", "lightning"},
{"sparse_infill_density", "50%"},
{"layer_height", 0.2}});
return sparse_infill_shape(print);
};
const SparseInfillShape first = shape();
const SparseInfillShape second = shape();
REQUIRE(first.path_count > 0);
REQUIRE(second.path_count == first.path_count);
REQUIRE(second.point_count == first.point_count);
REQUIRE(second.sharp_turns == first.sharp_turns);
// No tolerance: the same extrusions in the same order add up to the very same number.
REQUIRE_THAT(second.length, Catch::Matchers::WithinAbs(first.length, 0.));
// All of the above agree when the same branches are joined into different polylines, so the
// point sequence is what actually decides whether the two slices produced the same infill.
REQUIRE(second.sequence == first.sequence);
}
TEST_CASE("Lightning infill rounds the turns of its branches with the smooth factor", "[Fill]")
{
auto shape_for = [](const std::string &smooth_factor) {

View File

@@ -468,22 +468,27 @@ FullPrintConfig make_junction_config(GCodeFlavor flavor, double corner_velocity,
constexpr double junction_x = 60.0;
constexpr double junction_y = 60.0;
// Two 40mm travels meeting at (junction_x, junction_y) with the given turn, rotated by `orientation`.
// Two 40mm moves meeting at (junction_x, junction_y) with the given turn, rotated by `orientation`.
// 40mm is long enough to reach the commanded 150mm/s and brake back to any corner speed these tests
// produce. Travels (no E) keep the junction vector purely geometric, as the formulas below assume.
std::string corner_gcode(double turn_deg, double orientation_deg)
// produce. `e_per_mm` of zero makes them travels, which keeps the junction vector purely geometric
// as the formulas below assume.
std::string corner_gcode(double turn_deg, double orientation_deg, double e_per_mm = 0.0)
{
const double len = 40.0;
const double a_in = orientation_deg * M_PI / 180.0;
const double a_out = (orientation_deg + turn_deg) * M_PI / 180.0;
std::ostringstream extrude;
if (e_per_mm > 0.0)
extrude << std::fixed << std::setprecision(4) << " E" << len * e_per_mm;
std::ostringstream os;
os << std::fixed << std::setprecision(4)
<< "M83\n"
<< "G1 Z0.2 F1200\n"
<< "G1 X" << junction_x - len * std::cos(a_in) << " Y" << junction_y - len * std::sin(a_in) << " F6000\n"
<< "G1 X" << junction_x << " Y" << junction_y << " F9000\n"
<< "G1 X" << junction_x + len * std::cos(a_out) << " Y" << junction_y + len * std::sin(a_out) << " F9000\n";
<< "G1 X" << junction_x << " Y" << junction_y << extrude.str() << " F9000\n"
<< "G1 X" << junction_x + len * std::cos(a_out) << " Y" << junction_y + len * std::sin(a_out)
<< extrude.str() << " F9000\n";
return os.str();
}
@@ -492,7 +497,7 @@ std::string corner_gcode(double turn_deg, double orientation_deg)
double corner_speed(const GCodeProcessorResult& r)
{
for (const auto& mv : r.moves)
if (mv.type == EMoveType::Travel &&
if ((mv.type == EMoveType::Travel || mv.type == EMoveType::Extrude) &&
std::abs(mv.position.x() - junction_x) < 1e-3 &&
std::abs(mv.position.y() - junction_y) < 1e-3)
return mv.actual_feedrate;
@@ -500,11 +505,11 @@ double corner_speed(const GCodeProcessorResult& r)
}
double planned_corner_speed(GCodeFlavor flavor, double corner_velocity, double junction_deviation,
double turn_deg, double orientation_deg = 0.0)
double turn_deg, double orientation_deg = 0.0, double e_per_mm = 0.0)
{
GCodeProcessor proc;
run_processor(proc, make_junction_config(flavor, corner_velocity, junction_deviation),
corner_gcode(turn_deg, orientation_deg).c_str());
corner_gcode(turn_deg, orientation_deg, e_per_mm).c_str());
return corner_speed(proc.get_result());
}
@@ -582,3 +587,30 @@ TEST_CASE("Junction deviation is only used where the firmware actually plans wit
Catch::Matchers::WithinRel(without, 1e-4));
}
}
TEST_CASE("How fast a corner is taken does not depend on how much is extruded through it",
"[GCodeTiming][JunctionDeviation]")
{
// The junction cosine is taken over XYZE, so the direction vectors have to be unit length or the
// E term makes the two paths look more parallel than they are and the corner comes out too fast,
// the more so the higher the flow. Marlin normalizes over XYZE on any extruding move
// (planner.cpp, esteps > 0) and Klipper leaves E out of the cosine altogether
// (toolhead.py::Move.calc_junction); on both, this corner is planned by its geometry alone.
const double scv = 5.0;
const double turn = 6.0;
const double geometric = planned_corner_speed(gcfKlipper, scv, 0.0, turn);
REQUIRE(geometric > 0.0);
// 0.029mm/mm is an ordinary 0.42 x 0.2 line on 1.75mm filament; 0.1 is a fat large-nozzle one.
// Unnormalized these came out at 94.4 and 150.0mm/s against a geometric 86.9.
for (double e_per_mm : {0.029, 0.1})
REQUIRE_THAT(planned_corner_speed(gcfKlipper, scv, 0.0, turn, 0.0, e_per_mm),
Catch::Matchers::WithinRel(geometric, 0.02));
SECTION("and the same holds on Marlin 2") {
const double marlin = planned_corner_speed(gcfMarlinFirmware, scv, 0.05, turn);
REQUIRE(marlin > 0.0);
REQUIRE_THAT(planned_corner_speed(gcfMarlinFirmware, scv, 0.05, turn, 0.0, 0.029),
Catch::Matchers::WithinRel(marlin, 0.02));
}
}

View File

@@ -17,6 +17,7 @@ add_executable(${_TEST_NAME}_tests
test_preset_bundle_loading.cpp
test_preset_setting_id.cpp
test_preset_diff.cpp
test_vendor_cache.cpp
test_elephant_foot_compensation.cpp
test_fill_corner_smoothing.cpp
test_fill_plane_path.cpp

View File

@@ -574,11 +574,6 @@ TEST_CASE("Convex polygon intersection on two squares touching one vertex", "[Ge
Polygon B = A;
B.translate(10 / SCALING_FACTOR, 10 / SCALING_FACTOR);
SVG svg{std::string("one_vertex_touch") + ".svg"};
svg.draw(A, "blue");
svg.draw(B, "green");
svg.Close();
bool is_inters = Geometry::convex_polygons_intersect(A, B);
REQUIRE(is_inters == false);

View File

@@ -1,6 +1,7 @@
#include <catch2/catch_all.hpp>
#include <boost/filesystem.hpp>
#include <fstream>
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/AppConfig.hpp"
@@ -132,7 +133,7 @@ TEST_CASE("Current vendor type tolerates missing printer model", "[Preset][Bundl
{
PresetBundle bundle;
VendorProfile orca_vendor("ORCA");
VendorProfile orca_vendor; orca_vendor.id = "ORCA";
VendorProfile::PrinterModel model;
model.name = "Orca Test";
orca_vendor.models.emplace_back(model);
@@ -143,6 +144,31 @@ TEST_CASE("Current vendor type tolerates missing printer model", "[Preset][Bundl
CHECK(bundle.get_current_vendor_type() == VendorType::Unknown);
}
TEST_CASE("A malformed entry in a vendor's preset list is counted, not thrown", "[Preset][Bundle]")
{
ScopedTemporaryDir dir;
// A bare number where the list wants an object. An array element has no key,
// so reporting one as if it did throws nlohmann's invalid_iterator - which is
// not a parse_error, and escapes the catch around the vendor profile parse.
std::ofstream((dir.path() / "Acme.json").string())
<< R"({"version":"1.0.0","name":"Acme","process_list":[123,)"
<< R"({"name":"0.20mm Standard @Acme","sub_path":"process/standard.json"}]})";
fs::create_directories(dir.path() / "Acme" / "process");
std::ofstream((dir.path() / "Acme" / "process" / "standard.json").string())
<< R"({"type":"process","name":"0.20mm Standard @Acme","from":"system",)"
<< R"("instantiation":"true","layer_height":"0.2"})";
PresetBundle bundle;
size_t loaded = 0;
REQUIRE_NOTHROW(loaded = bundle.load_vendor_configs_from_json(
dir.path().string(), "Acme", PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent).second);
CHECK(bundle.error_count() > 0); // the malformed element was counted
CHECK(loaded == 1); // the well-formed one beside it still loaded
}
TEST_CASE("Printer extruder count tolerates missing nozzle diameter", "[Preset][Bundle]")
{
PresetBundle bundle;

File diff suppressed because it is too large Load Diff