Merge branch 'main' into u1-hf

This commit is contained in:
SoftFever authored and GitHub committed 2026-09-29 23:34:35 +08:00
commit afaa94b3bf
61 files changed
+583 -216

No files matched your search

+125
View File
@@ -768,3 +768,128 @@ TEST_CASE("Each filament prints with its variant of the extruder's variant strin
CHECK(gcode.find("; nozzle_temperature = " + resolved + "\n") != std::string::npos);
}
}
// An adaptive pressure advance model predicting the same pressure advance at every flow and acceleration.
static std::string constant_pressure_advance_model(const std::string &pa)
{
return pa + ",1,1000\n" + pa + ",500,1000\n" + pa + ",1,100000\n" + pa + ",500,100000";
}
// The pressure advance values a Klipper G-code sets.
static std::set<std::string> pressure_advance_values(const std::string &gcode)
{
const std::string token = "SET_PRESSURE_ADVANCE ADVANCE=";
std::set<std::string> values;
std::istringstream stream(gcode);
for (std::string line; std::getline(stream, line);)
if (line.rfind(token, 0) == 0)
values.insert(line.substr(token.size(), line.find(';') - token.size()));
return values;
}
// Pressure advance, and the adaptive pressure advance model, are tuned per extruder variant like the
// other filament variant settings.
TEST_CASE("Each filament sets the pressure advance of its extruder variant", "[MultiFilament]")
{
auto [nozzle_volume_type, filament, pressure_advance, adaptive_pressure_advance] = GENERATE(table<NozzleVolumeType, int, std::string, std::string>({
{ nvtStandard, 1, "0.021", "0.012" },
{ nvtHighFlow, 1, "0.037", "0.034" },
{ nvtHighFlow, 2, "0.043", "0.056" }, // filament 2 defines no High Flow variant
}));
const bool adaptive = GENERATE(false, true);
DYNAMIC_SECTION(get_nozzle_volume_type_string(nozzle_volume_type) << " nozzle, filament " << filament << (adaptive ? ", adaptive" : "")) {
DynamicPrintConfig config = multifilament_config(2, {
{ "gcode_flavor", "klipper" },
{ "extruder_variant_list", "Direct Drive Standard,Direct Drive High Flow" },
// filament 1 defines Standard (0.021) and High Flow (0.037), filament 2 Standard (0.043)
{ "filament_extruder_variant", "Direct Drive Standard;Direct Drive High Flow;Direct Drive Standard" },
{ "filament_self_index", "1,1,2" },
{ "enable_pressure_advance", "1,1,1" },
{ "pressure_advance", "0.021,0.037,0.043" },
{ "adaptive_pressure_advance", adaptive ? "1,1,1" : "0,0,0" },
{ "sparse_infill_filament_id", filament },
{ "internal_solid_filament_id", filament },
{ "top_surface_filament_id", filament },
{ "bottom_surface_filament_id", filament },
{ "outer_wall_filament_id", filament },
{ "inner_wall_filament_id", filament },
{ "enable_prime_tower", 0 },
{ "skirt_loops", 0 },
{ "brim_type", "no_brim" },
// custom G-code indexes the per-filament arrays by filament
{ "machine_start_gcode", "; start pressure advance {pressure_advance[initial_extruder]}" },
});
config.option<ConfigOptionStrings>("adaptive_pressure_advance_model")->values = {
constant_pressure_advance_model("0.012"), constant_pressure_advance_model("0.034"), constant_pressure_advance_model("0.056") };
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = { nozzle_volume_type };
const std::string gcode = slice({ cube(20) }, config);
// The toolchange sets the variant's pressure advance; with adaptive pressure advance, the
// prediction of the variant's model then replaces it.
std::set<std::string> expected{ pressure_advance };
if (adaptive)
expected.insert(adaptive_pressure_advance);
CHECK(pressure_advance_values(gcode) == expected);
CHECK(gcode.find("; start pressure advance " + pressure_advance + "\n") != std::string::npos);
}
}
// On a printer with two extruders, a filament takes the pressure advance of the variant of the extruder
// it is mapped to, whichever filament and extruder that is.
TEST_CASE("Each filament sets the pressure advance of its extruder variant on a two-extruder printer", "[MultiFilament]")
{
auto [filament, extruder, pressure_advance, adaptive_pressure_advance] = GENERATE(table<int, int, std::string, std::string>({
{ 1, 1, "0.021", "0.012" },
{ 1, 2, "0.037", "0.034" },
{ 2, 1, "0.043", "0.056" },
{ 2, 2, "0.049", "0.078" },
}));
const bool adaptive = GENERATE(false, true);
DYNAMIC_SECTION("filament " << filament << " on extruder " << extruder << (adaptive ? ", adaptive" : "")) {
// the other filament goes on the other extruder
const std::string filament_map = filament == 1 ? std::to_string(extruder) + "," + std::to_string(3 - extruder) :
std::to_string(3 - extruder) + "," + std::to_string(extruder);
DynamicPrintConfig config = multifilament_config(2, {
{ "gcode_flavor", "klipper" },
{ "single_extruder_multi_material", 0 },
{ "nozzle_diameter", "0.4,0.4" },
{ "extruder_printable_height", "0,0" },
// extruder 1 has a Standard nozzle, extruder 2 a High Flow one
{ "printer_extruder_id", "1,2" },
{ "printer_extruder_variant", "Direct Drive Standard;Direct Drive High Flow" },
{ "extruder_variant_list", "Direct Drive Standard;Direct Drive High Flow" },
{ "filament_map", filament_map },
// both filaments define Standard and High Flow
{ "filament_extruder_variant", "Direct Drive Standard;Direct Drive High Flow;Direct Drive Standard;Direct Drive High Flow" },
{ "filament_self_index", "1,1,2,2" },
{ "enable_pressure_advance", "1,1,1,1" },
{ "pressure_advance", "0.021,0.037,0.043,0.049" },
{ "adaptive_pressure_advance", adaptive ? "1,1,1,1" : "0,0,0,0" },
{ "sparse_infill_filament_id", filament },
{ "internal_solid_filament_id", filament },
{ "top_surface_filament_id", filament },
{ "bottom_surface_filament_id", filament },
{ "outer_wall_filament_id", filament },
{ "inner_wall_filament_id", filament },
{ "enable_prime_tower", 0 },
{ "skirt_loops", 0 },
{ "brim_type", "no_brim" },
// custom G-code indexes the per-filament arrays by filament
{ "machine_start_gcode", "; start pressure advance {pressure_advance[initial_extruder]}" },
});
config.option<ConfigOptionStrings>("adaptive_pressure_advance_model")->values = {
constant_pressure_advance_model("0.012"), constant_pressure_advance_model("0.034"),
constant_pressure_advance_model("0.056"), constant_pressure_advance_model("0.078") };
config.option<ConfigOptionEnumsGeneric>("extruder_type", true)->values = { etDirectDrive, etDirectDrive };
config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type", true)->values = { nvtStandard, nvtHighFlow };
// keep the mapping above rather than grouping the filaments automatically
config.option<ConfigOptionEnum<FilamentMapMode>>("filament_map_mode", true)->value = fmmManual;
const std::string gcode = slice({ cube(20) }, config);
std::set<std::string> expected{ pressure_advance };
if (adaptive)
expected.insert(adaptive_pressure_advance);
CHECK(pressure_advance_values(gcode) == expected);
CHECK(gcode.find("; start pressure advance " + pressure_advance + "\n") != std::string::npos);
}
}
@@ -807,3 +807,15 @@ TEST_CASE("get_index_for_extruder scales the variant column by the requested str
REQUIRE(col0_stride2 == 0);
REQUIRE(col1_stride2 == 2);
}
// A per-variant filament option read with a single value gives it to every filament variant. A project
// exported by an older CLI holds a single value for an option no loaded preset defined, such as
// filament_ironing_flow.
TEST_CASE("A per-variant filament option read with a single value gives it to every filament variant", "[Config]")
{
// filament 1 defines Standard and High Flow, filament 2 Standard
DynamicPrintConfig config;
config.option<ConfigOptionInts>("filament_self_index", true)->values = {1, 1, 2};
config.load_from_ini_string("pressure_advance = 0.021", ForwardCompatibilitySubstitutionRule::Disable);
REQUIRE(config.option<ConfigOptionFloats>("pressure_advance")->values == std::vector<double>({0.021, 0.021, 0.021}));
}
@@ -5482,3 +5482,39 @@ TEST_CASE("Config import confines zip entries, preset names and bundle ids to th
CHECK_FALSE(any_filename_contains(temp_dir.path(), "bundle-escape"));
}
}
// A project saved before a key joined filament_options_with_variant stores it once per filament,
// while the keys that were already per variant store it once per filament variant. Loading such a
// project gives every variant of a filament that filament's value.
TEST_CASE("A project saved with pressure advance per filament applies it to every variant of the filament", "[Preset][Bundle]")
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.opt<ConfigOptionStrings>("filament_colour")->values = { "#FF0000", "#00FF00" };
config.opt<ConfigOptionFloats>("filament_diameter")->values = { 1.75, 1.75 };
config.option<ConfigOptionStrings>("filament_settings_id", true)->values = { "Project PLA", "Project PETG" };
// A multi-variant printer: full_print_config() leaves the list out, and the loader splits the
// variant keys per filament only when the project carries it.
config.option<ConfigOptionStrings>("extruder_variant_list", true)->values = { "Direct Drive Standard,Direct Drive High Flow" };
// filament 1 defines Standard and High Flow, filament 2 Standard
config.opt<ConfigOptionStrings>("filament_extruder_variant")->values = { "Direct Drive Standard", "Direct Drive High Flow", "Direct Drive Standard" };
config.opt<ConfigOptionInts>("filament_self_index")->values = { 1, 1, 2 };
config.opt<ConfigOptionFloatsNullable>("filament_flow_ratio")->values = { 0.95, 0.96, 0.97 }; // one per filament variant
// One per filament, read through the loader that project files go through.
config.load_from_ini_string("pressure_advance = 0.021,0.043", ForwardCompatibilitySubstitutionRule::Disable);
// The CLI slices the config as loaded.
check_double_vector(config.opt<ConfigOptionFloats>("pressure_advance")->values, { 0.021, 0.021, 0.043 });
check_double_vector(config.opt<ConfigOptionFloatsNullable>("filament_flow_ratio")->values, { 0.95, 0.96, 0.97 });
// The GUI normalizes the config before load; mirror that so only the production path runs.
Preset::normalize(config);
PresetBundle bundle;
bundle.load_config_model("test.3mf", std::move(config));
REQUIRE(bundle.filament_presets.size() == 2);
const DynamicPrintConfig &pla = bundle.filaments.find_preset(bundle.filament_presets[0], false, true)->config;
const DynamicPrintConfig &petg = bundle.filaments.find_preset(bundle.filament_presets[1], false, true)->config;
check_double_vector(pla.opt<ConfigOptionFloats>("pressure_advance")->values, { 0.021, 0.021 });
check_double_vector(petg.opt<ConfigOptionFloats>("pressure_advance")->values, { 0.043 });
check_double_vector(pla.opt<ConfigOptionFloatsNullable>("filament_flow_ratio")->values, { 0.95, 0.96 });
check_double_vector(petg.opt<ConfigOptionFloatsNullable>("filament_flow_ratio")->values, { 0.97 });
}
+1
View File
@@ -27,6 +27,7 @@ add_executable(${_TEST_NAME}_tests
test_plugin_cloud_metadata.cpp
test_plugin_audit.cpp
test_shortcuts.cpp
test_file_url.cpp
../fff_print/test_helpers.cpp
)
+74
View File
@@ -0,0 +1,74 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include "slic3r/GUI/GUI.hpp"
#include <boost/filesystem/path.hpp>
#include <wx/uri.h>
using namespace Slic3r::GUI;
#ifndef _WIN32
TEST_CASE("A file URL keeps characters special to URLs in its path", "[FileUrl]")
{
const std::string path = GENERATE(as<std::string>{},
"/opt/test#dir/resources/web/homepage/index.html",
"/opt/test%20x/resources/web/homepage/index.html",
"/opt/Orca Slicer/resources/web/homepage/index.html",
"/opt/what?/resources/web/homepage/index.html",
"/home/Jos\xC3\xA9/\xE8\xB5\x84\xE6\xBA\x90/resources/web/homepage/index.html");
const wxString url = file_url_from_path(boost::filesystem::path(path));
CAPTURE(path, url.utf8_string());
// WebView::CreateWebView() and WebView::LoadUrl() re-parse the URL before loading it.
const wxURI uri(wxURI(url).BuildURI());
CHECK(uri.GetScheme() == "file");
CHECK(!uri.HasQuery());
CHECK(!uri.HasFragment());
CHECK(wxURI::Unescape(uri.GetPath()).utf8_string() == path);
}
TEST_CASE("A file URL of a plain path is the path behind file://", "[FileUrl]")
{
const std::string path = "/opt/OrcaSlicer/resources/web/homepage/index.html";
CHECK(file_url_from_path(boost::filesystem::path(path)) == "file://" + path);
}
TEST_CASE("A query appended to a file URL stays separate from its path", "[FileUrl]")
{
const std::string path = "/opt/test#dir%20x/resources/web/guide/0/index.html";
const wxURI uri(file_url_from_path(boost::filesystem::path(path)) + "?target=21&lang=de");
CHECK(wxURI::Unescape(uri.GetPath()).utf8_string() == path);
CHECK(uri.GetQuery() == "target=21&lang=de");
CHECK(!uri.HasFragment());
}
#else
TEST_CASE("A file URL of a Windows path has a drive letter and forward slashes", "[FileUrl]")
{
const auto [path, url] = GENERATE(table<std::wstring, std::string>({
{ L"C:\\Program Files\\OrcaSlicer\\resources\\web\\homepage\\index.html",
"file:///C:/Program%20Files/OrcaSlicer/resources/web/homepage/index.html" },
{ L"D:\\#OneDrive\\OrcaSlicer\\resources\\web\\guide\\0\\index.html",
"file:///D:/%23OneDrive/OrcaSlicer/resources/web/guide/0/index.html" },
{ L"D:\\100%\\OrcaSlicer\\resources\\web\\homepage\\index.html",
"file:///D:/100%25/OrcaSlicer/resources/web/homepage/index.html" },
// Callers join the resources directory with a forward-slash relative path.
{ L"D:\\#OneDrive\\OrcaSlicer\\resources/web/homepage/index.html",
"file:///D:/%23OneDrive/OrcaSlicer/resources/web/homepage/index.html" },
{ L"\\\\server\\share\\OrcaSlicer\\resources\\web\\homepage\\index.html",
"file://server/share/OrcaSlicer/resources/web/homepage/index.html" },
}));
CHECK(file_url_from_path(boost::filesystem::path(path)).utf8_string() == url);
}
TEST_CASE("A query appended to a Windows file URL stays separate from its path", "[FileUrl]")
{
const wxURI uri(file_url_from_path(boost::filesystem::path(L"D:\\#OneDrive\\OrcaSlicer\\resources\\web\\guide\\0\\index.html")) +
"?target=21&lang=de");
CHECK(wxURI::Unescape(uri.GetPath()) == "/D:/#OneDrive/OrcaSlicer/resources/web/guide/0/index.html");
CHECK(uri.GetQuery() == "target=21&lang=de");
CHECK(!uri.HasFragment());
}
#endif
+18 -6
View File
@@ -16,6 +16,8 @@
#include <string>
#include <wx/uri.h>
namespace py = pybind11;
namespace {
@@ -210,23 +212,33 @@ TEST_CASE("A reloaded plugin page is recognised by its base URL, fragment aside"
{
using namespace Slic3r::GUI::web_hosting;
// A resources path holding a space, which the web view reports escaped.
// A resources path holding a space, which the web view may report escaped differently.
const Slic3r::ScopedResourcesDir resources("web content check");
// The swapped-in page, then after an in-page anchor and a reload.
CHECK(is_content_url(content_base_url()));
CHECK(is_content_url(content_base_url() + "#tab2"));
wxString escaped = content_base_url();
escaped.Replace(" ", "%20");
REQUIRE(escaped != content_base_url());
CHECK(is_content_url(escaped));
CHECK(is_content_url(escaped + "#tab2"));
const wxString unescaped = wxURI::Unescape(content_base_url());
REQUIRE(unescaped != content_base_url());
CHECK(is_content_url(unescaped));
CHECK(is_content_url(unescaped + "#tab2"));
// A page the plugin linked to keeps its own URL and must be left alone.
CHECK_FALSE(is_content_url(content_base_url() + "guide.html"));
CHECK_FALSE(is_content_url("https://example.com/"));
CHECK_FALSE(is_content_url(""));
}
TEST_CASE("A reloaded plugin page is recognised when the resources path holds a '#'", "[PluginHost]")
{
using namespace Slic3r::GUI::web_hosting;
const Slic3r::ScopedResourcesDir resources("web#content check");
CHECK(is_content_url(content_base_url()));
CHECK(is_content_url(content_base_url() + "#tab2"));
CHECK_FALSE(is_content_url(content_base_url() + "guide.html"));
}
TEST_CASE("Plugin host API exposes model geometry and structure to Python", "[PluginHost][Python]")
{
using Catch::Matchers::WithinAbs;