mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-20 06:01:16 +00:00
Merge branch 'main' into feat/generic-n-extruders
This commit is contained in:
473 files changed
+127462
-21603
No files matched your search
@@ -6,7 +6,12 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_creality_cfs_match.cpp
|
||||
test_dev_mapping.cpp
|
||||
test_dev_extder_system.cpp
|
||||
test_device_access.cpp
|
||||
test_moonraker_discovery.cpp
|
||||
test_network_discovery.cpp
|
||||
test_filament_bitmap_utils.cpp
|
||||
test_colour_picker_field.cpp
|
||||
test_filament_color_picker.cpp
|
||||
test_device_progress.cpp
|
||||
test_device_manager_integration.cpp
|
||||
test_web_media_controller.cpp
|
||||
|
||||
@@ -255,7 +255,7 @@ TEST_CASE("Native command catalog includes the Help and developer-mode commands"
|
||||
|
||||
const Slic3r::GUI::NativeCommand* first = find("help_keyboard_shortcuts");
|
||||
REQUIRE(first != nullptr);
|
||||
for (const char* key : {"help_setup_wizard", "help_open_config_folder", "help_troubleshoot", "help_network_test",
|
||||
for (const char* key : {"help_setup_wizard", "help_open_config_folder", "help_migrate_filament_presets", "help_troubleshoot", "help_network_test",
|
||||
"help_benchmark_3d_scene", "help_tip_of_the_day", "help_check_updates", "help_about", "open_wiki",
|
||||
"open_youtube"}) {
|
||||
const Slic3r::GUI::NativeCommand* c = find(key);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#include "slic3r/GUI/Field.hpp"
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include <boost/any.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <string>
|
||||
#include <wx/colour.h>
|
||||
#include <wx/string.h>
|
||||
|
||||
using namespace Slic3r;
|
||||
using namespace Slic3r::GUI;
|
||||
|
||||
namespace {
|
||||
class TestColourPicker : public ColourPicker {
|
||||
public:
|
||||
TestColourPicker() : ColourPicker(ConfigOptionDef{}, "filament_colour") {}
|
||||
std::string value() { return boost::any_cast<std::string>(get_value()); }
|
||||
};
|
||||
}
|
||||
|
||||
TEST_CASE("Undefined color fields remain distinct from opaque black", "[ColourPickerField]")
|
||||
{
|
||||
TestColourPicker field;
|
||||
REQUIRE(field.value().empty());
|
||||
field.set_value(std::string("#000000"));
|
||||
REQUIRE(field.value() == "#000000");
|
||||
field.set_value(std::string("invalid color"));
|
||||
REQUIRE(field.value().empty());
|
||||
field.set_value(boost::any(wxString("#123456")));
|
||||
REQUIRE(field.value() == "#123456");
|
||||
field.set_value(std::string());
|
||||
REQUIRE(field.value().empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Programmatic color field assignment stays silent", "[ColourPickerField]")
|
||||
{
|
||||
TestColourPicker field;
|
||||
int changes = 0;
|
||||
field.m_on_change = [&](const std::string&, const boost::any&) { ++changes; };
|
||||
field.set_value(std::string("#123456"), false);
|
||||
field.set_value(boost::any(wxString("#654321")), true);
|
||||
REQUIRE(changes == 0);
|
||||
REQUIRE(field.value() == "#654321");
|
||||
field.m_disable_change_event = true;
|
||||
field.set_value(std::string("#FF0000"));
|
||||
REQUIRE(field.value() == "#FF0000");
|
||||
REQUIRE(changes == 0);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
#ifdef WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/generators/catch_generators.hpp>
|
||||
#include <memory>
|
||||
|
||||
#include "libslic3r/AppConfig.hpp"
|
||||
#include "slic3r/GUI/DeviceCore/DevManager.h"
|
||||
#include "slic3r/GUI/DeviceManager.hpp"
|
||||
#include "slic3r/Utils/IPrinterAgent.hpp"
|
||||
#include "slic3r/Utils/NetworkAgentFactory.hpp"
|
||||
#include "slic3r/Utils/MoonrakerPrinterAgent.hpp"
|
||||
#include "slic3r/Utils/NetworkAgent.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
TEST_CASE("Moonraker permits an empty access code", "[DeviceAccess]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
machine.printer_agent_id = MOONRAKER_PRINTER_AGENT_ID;
|
||||
machine.set_access_code("", false);
|
||||
|
||||
REQUIRE(machine.has_access_right());
|
||||
}
|
||||
|
||||
TEST_CASE("Moonraker logout revokes access until explicitly rebound", "[DeviceAccess]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
machine.printer_agent_id = MOONRAKER_PRINTER_AGENT_ID;
|
||||
const std::string code = GENERATE("", "configured-key");
|
||||
machine.set_access_code(code, false);
|
||||
REQUIRE(machine.has_access_right());
|
||||
|
||||
machine.revoke_access();
|
||||
REQUIRE(machine.get_access_code().empty());
|
||||
REQUIRE_FALSE(machine.has_access_right());
|
||||
|
||||
machine.set_access_code(code, false);
|
||||
REQUIRE(machine.has_access_right());
|
||||
REQUIRE(machine.get_access_code() == code);
|
||||
}
|
||||
|
||||
TEST_CASE("Revoked access prevents calls to the printer agent", "[DeviceAccess]")
|
||||
{
|
||||
class RecordingAgent : public MoonrakerPrinterAgent
|
||||
{
|
||||
public:
|
||||
RecordingAgent() : MoonrakerPrinterAgent("") {}
|
||||
int connections = 0;
|
||||
int connect_printer(const PrinterConnectionParams&) override
|
||||
{
|
||||
++connections;
|
||||
return 0;
|
||||
}
|
||||
int disconnect_printer() override { return 0; }
|
||||
};
|
||||
|
||||
auto agent = std::make_shared<RecordingAgent>();
|
||||
NetworkAgent network(nullptr, agent);
|
||||
MachineObject machine(nullptr, &network, "test", "test_dev", "127.0.0.1");
|
||||
machine.printer_agent_id = GENERATE(BBL_PRINTER_AGENT_ID, ORCA_PRINTER_AGENT_ID, MOONRAKER_PRINTER_AGENT_ID);
|
||||
machine.set_access_code("configured-key", false);
|
||||
REQUIRE(machine.connect() == 0);
|
||||
REQUIRE(agent->connections == 1);
|
||||
|
||||
machine.revoke_access();
|
||||
REQUIRE(machine.connect() == -1);
|
||||
REQUIRE(agent->connections == 1);
|
||||
|
||||
machine.set_access_code("configured-key", false);
|
||||
REQUIRE(machine.connect() == 0);
|
||||
REQUIRE(agent->connections == 2);
|
||||
}
|
||||
|
||||
TEST_CASE("Other printer agents require an access code", "[DeviceAccess]")
|
||||
{
|
||||
MachineObject machine(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
machine.printer_agent_id = GENERATE(
|
||||
BBL_PRINTER_AGENT_ID, ORCA_PRINTER_AGENT_ID, "qidi", "snapmaker", "crealityprint", "plugin", "unknown");
|
||||
|
||||
machine.set_access_code("", false);
|
||||
REQUIRE_FALSE(machine.has_access_right());
|
||||
|
||||
machine.set_access_code("88888888", false);
|
||||
REQUIRE(machine.has_access_right());
|
||||
}
|
||||
|
||||
TEST_CASE("Rebinding Moonraker with an empty key saves the machine", "[DeviceAccess]")
|
||||
{
|
||||
AppConfig config;
|
||||
DeviceManager manager(nullptr, false, &config);
|
||||
MachineObject machine(&manager, nullptr, "test", "test_dev", "127.0.0.1");
|
||||
machine.printer_agent_id = MOONRAKER_PRINTER_AGENT_ID;
|
||||
machine.dev_connection_type = "lan";
|
||||
machine.revoke_access();
|
||||
|
||||
machine.set_access_code("");
|
||||
const auto& machines = config.get_local_machines();
|
||||
REQUIRE(machines.count("test_dev") == 1);
|
||||
REQUIRE(machines.at("test_dev").printer_agent_id == MOONRAKER_PRINTER_AGENT_ID);
|
||||
REQUIRE(machines.at("test_dev").access_code.empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Moonraker discovery leaves another agent's saved machine alone", "[DeviceAccess]")
|
||||
{
|
||||
AppConfig config;
|
||||
NetworkAgent network(nullptr, std::make_shared<MoonrakerPrinterAgent>(""));
|
||||
DeviceManager manager(&network, false, &config);
|
||||
|
||||
const std::string owner = GENERATE(as<std::string>{}, ORCA_PRINTER_AGENT_ID, MOONRAKER_PRINTER_AGENT_ID);
|
||||
BBLocalMachine saved;
|
||||
saved.dev_id = "192.0.2.10:7125";
|
||||
saved.dev_ip = saved.dev_id;
|
||||
saved.printer_agent_id = owner;
|
||||
config.update_local_machine(saved);
|
||||
|
||||
manager.on_machine_alive(R"({"dev_name":"Klipper","dev_id":"192.0.2.10:7125","dev_ip":"192.0.2.10:7125",
|
||||
"dev_type":"","dev_signal":"0","connect_type":"lan","bind_state":"free"})");
|
||||
REQUIRE((manager.get_local_machine(saved.dev_id) != nullptr) == (owner == MOONRAKER_PRINTER_AGENT_ID));
|
||||
}
|
||||
@@ -22,6 +22,13 @@
|
||||
#include <cmath>
|
||||
|
||||
#include <wx/colour.h>
|
||||
#ifdef __WXMSW__
|
||||
#include <wx/bitmap.h>
|
||||
#include <wx/image.h>
|
||||
#include <cstdlib>
|
||||
#include <utility>
|
||||
#include <catch2/generators/catch_generators.hpp>
|
||||
#endif
|
||||
#include <wx/string.h>
|
||||
|
||||
#include "libslic3r/FilamentMixer.hpp"
|
||||
@@ -260,3 +267,71 @@ TEST_CASE("sample_gradient_ramp returns nothing without a usable curve or step c
|
||||
curve.points = {{0.0, kGradientMaxRatio, NAN, NAN}, {1.0, kGradientMinRatio, NAN, NAN}};
|
||||
REQUIRE(Slic3r::GUI::sample_gradient_ramp(wxColour(255, 0, 0), wxColour(0, 0, 255), curve, 0).empty());
|
||||
}
|
||||
|
||||
// The headless GTK/macOS suite does not initialize GUI display/font stock objects.
|
||||
// Exercise native bitmap pixels on Windows without adding that dependency elsewhere.
|
||||
#ifdef __WXMSW__
|
||||
TEST_CASE("Filament gradient pixels follow both endpoint directions without changing input", "[FilamentBitmapUtils]")
|
||||
{
|
||||
const bool reverse = GENERATE(false, true);
|
||||
const std::vector<wxColour> colors = reverse ? std::vector<wxColour>{wxColour(0, 0, 255), wxColour(255, 0, 0)}
|
||||
: std::vector<wxColour>{wxColour(255, 0, 0), wxColour(0, 0, 255)};
|
||||
const auto original = colors;
|
||||
const wxBitmap bitmap = Slic3r::GUI::create_filament_bitmap(colors, wxSize(64, 16), true);
|
||||
REQUIRE(bitmap.IsOk());
|
||||
const wxImage image = bitmap.ConvertToImage();
|
||||
REQUIRE(image.IsOk());
|
||||
// Interior pixels avoid backend-specific rasterization at the gradient boundary.
|
||||
for (const auto& sample : {std::pair<int, wxColour>{2, colors.front()}, {61, colors.back()}}) {
|
||||
REQUIRE(std::abs(int(image.GetRed(sample.first, 8)) - int(sample.second.Red())) <= 16);
|
||||
REQUIRE(std::abs(int(image.GetBlue(sample.first, 8)) - int(sample.second.Blue())) <= 16);
|
||||
}
|
||||
REQUIRE(colors == original);
|
||||
}
|
||||
|
||||
TEST_CASE("Filament halves preserve the same caller color order as gradients", "[FilamentBitmapUtils]")
|
||||
{
|
||||
const bool reverse = GENERATE(false, true);
|
||||
const std::vector<wxColour> colors = reverse ? std::vector<wxColour>{wxColour(0, 0, 255), wxColour(255, 0, 0)}
|
||||
: std::vector<wxColour>{wxColour(255, 0, 0), wxColour(0, 0, 255)};
|
||||
const auto original = colors;
|
||||
const wxBitmap bitmap = Slic3r::GUI::create_filament_bitmap(colors, wxSize(64, 16), false);
|
||||
REQUIRE(bitmap.IsOk());
|
||||
const wxImage image = bitmap.ConvertToImage();
|
||||
REQUIRE(image.IsOk());
|
||||
require_same_rgb(wxColour(image.GetRed(16, 8), image.GetGreen(16, 8), image.GetBlue(16, 8)), colors.front());
|
||||
require_same_rgb(wxColour(image.GetRed(48, 8), image.GetGreen(48, 8), image.GetBlue(48, 8)), colors.back());
|
||||
REQUIRE(colors == original);
|
||||
}
|
||||
|
||||
TEST_CASE("Three color filament stripes retain caller order", "[FilamentBitmapUtils]")
|
||||
{
|
||||
const std::vector<wxColour> colors{wxColour(0, 0, 255), wxColour(0, 255, 0), wxColour(255, 0, 0)};
|
||||
const auto original = colors;
|
||||
const wxBitmap bitmap = Slic3r::GUI::create_filament_bitmap(colors, wxSize(72, 24), false);
|
||||
REQUIRE(bitmap.IsOk());
|
||||
const wxImage image = bitmap.ConvertToImage();
|
||||
REQUIRE(image.IsOk());
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const int x = 12 + i * 24;
|
||||
require_same_rgb(wxColour(image.GetRed(x, 12), image.GetGreen(x, 12), image.GetBlue(x, 12)), colors[i]);
|
||||
}
|
||||
REQUIRE(colors == original);
|
||||
}
|
||||
|
||||
TEST_CASE("Four color filament quadrants retain caller order", "[FilamentBitmapUtils]")
|
||||
{
|
||||
const std::vector<wxColour> colors{wxColour(0, 0, 255), wxColour(0, 255, 0), wxColour(255, 0, 0), wxColour(255, 255, 0)};
|
||||
const auto original = colors;
|
||||
const wxBitmap bitmap = Slic3r::GUI::create_filament_bitmap(colors, wxSize(64, 32), false);
|
||||
REQUIRE(bitmap.IsOk());
|
||||
const wxImage image = bitmap.ConvertToImage();
|
||||
REQUIRE(image.IsOk());
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const int x = i % 2 ? 48 : 16, y = i / 2 ? 24 : 8;
|
||||
require_same_rgb(wxColour(image.GetRed(x, y), image.GetGreen(x, y), image.GetBlue(x, y)), colors[i]);
|
||||
}
|
||||
REQUIRE(colors == original);
|
||||
}
|
||||
|
||||
#endif // __WXMSW__
|
||||
@@ -0,0 +1,179 @@
|
||||
#include "slic3r/GUI/ColorPickerDialog.hpp"
|
||||
#include "libslic3r/Color.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstddef>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/generators/catch_generators.hpp>
|
||||
|
||||
using namespace Slic3r;
|
||||
using namespace Slic3r::GUI;
|
||||
|
||||
// One color more than an ordered gradient may hold.
|
||||
static std::vector<std::string> too_many_gradient_colors()
|
||||
{
|
||||
return std::vector<std::string>(COLOR_PICKER_MAX_GRADIENT_COLORS + 1, "#FF0000");
|
||||
}
|
||||
|
||||
TEST_CASE("Project gradient direction survives editing and RGB serialization", "[FilamentColorPicker]")
|
||||
{
|
||||
const std::vector<std::string> endpoints = {"#FF0000", "#0000FF"};
|
||||
const FilamentColorPickerValue stored{endpoints, true};
|
||||
const auto initial = filament_color_picker_selection(stored);
|
||||
REQUIRE(initial);
|
||||
REQUIRE(filament_color_picker_result(*initial).colors == endpoints);
|
||||
REQUIRE_FALSE(filament_color_picker_changed(*initial, initial));
|
||||
REQUIRE_FALSE(filament_color_picker_changed(*initial, std::nullopt));
|
||||
const auto reverse = filament_color_picker_selection({{endpoints[1], endpoints[0]}, true});
|
||||
REQUIRE(filament_color_picker_changed(*initial, reverse));
|
||||
REQUIRE(filament_color_picker_result(*reverse).colors.front() == endpoints.back());
|
||||
}
|
||||
|
||||
TEST_CASE("Unrepresentable project colors remain unchanged until a different selection is confirmed", "[FilamentColorPicker]")
|
||||
{
|
||||
const auto value = GENERATE(FilamentColorPickerValue{too_many_gradient_colors(), true},
|
||||
FilamentColorPickerValue{{"#FF0000", "#0000FF"}, false});
|
||||
REQUIRE_FALSE(filament_color_picker_selection(value));
|
||||
const auto seed = filament_color_picker_initial(value);
|
||||
REQUIRE_FALSE(filament_color_picker_changed(seed, seed));
|
||||
REQUIRE_FALSE(filament_color_picker_changed(seed, std::nullopt));
|
||||
const auto changed = filament_color_picker_selection({{"#FE0000"}, false});
|
||||
REQUIRE(filament_color_picker_changed(seed, changed));
|
||||
REQUIRE(filament_color_picker_result(*changed).colors == std::vector<std::string>{"#FE0000"});
|
||||
}
|
||||
|
||||
TEST_CASE("Filament seeds preserve primary alpha when colors are invalid or absent", "[FilamentColorPicker]")
|
||||
{
|
||||
const FilamentColorPickerValue stale{{"invalid", "#00FF00"}, false, "#12345680"};
|
||||
REQUIRE_FALSE(filament_color_picker_selection(stale));
|
||||
REQUIRE(filament_color_picker_result(filament_color_picker_initial(stale)).colors == std::vector<std::string>{"#12345680"});
|
||||
const auto value = filament_color_picker_selection({{"#12345680", "#ABCDEF01"}, true});
|
||||
REQUIRE(value);
|
||||
REQUIRE(filament_color_picker_result(*value).colors == std::vector<std::string>{"#12345680", "#ABCDEF01"});
|
||||
const auto missing = filament_color_picker_initial({});
|
||||
REQUIRE(filament_color_picker_result(missing).colors == std::vector<std::string>{"#000000"});
|
||||
}
|
||||
|
||||
TEST_CASE("Filament project colors round trip every alpha boundary with compatible opaque spelling", "[FilamentColorPicker]")
|
||||
{
|
||||
const auto hex = GENERATE(std::string("#12345600"), std::string("#12345601"),
|
||||
std::string("#12345680"), std::string("#123456FE"),
|
||||
std::string("#123456FF"), std::string("#123456"));
|
||||
const FilamentColorPickerValue stored{{hex}, false};
|
||||
const auto selection = filament_color_picker_selection(stored);
|
||||
REQUIRE(selection);
|
||||
REQUIRE_FALSE(filament_color_picker_changed(*selection, selection));
|
||||
REQUIRE_FALSE(filament_color_picker_changed(*selection, std::nullopt));
|
||||
const auto result = filament_color_picker_result(*selection);
|
||||
const std::string expected = hex == "#123456FF" ? "#123456" : hex;
|
||||
REQUIRE(result.colors == std::vector<std::string>{expected});
|
||||
ColorRGBA decoded;
|
||||
REQUIRE(decode_color(result.colors.front(), decoded));
|
||||
REQUIRE(filament_color_picker_result(ColorSelection{decoded}).colors == result.colors);
|
||||
const auto reloaded = filament_color_picker_selection(result);
|
||||
REQUIRE(reloaded);
|
||||
REQUIRE(filament_color_picker_result(*reloaded).colors == result.colors);
|
||||
}
|
||||
|
||||
TEST_CASE("Alpha only filament edits preserve ordered independent gradient endpoint alpha", "[FilamentColorPicker]")
|
||||
{
|
||||
const auto original = filament_color_picker_selection({{"#FF000000", "#0000FF80"}, true});
|
||||
const auto edited = filament_color_picker_selection({{"#FF000001", "#0000FFFE"}, true});
|
||||
REQUIRE(original);
|
||||
REQUIRE(edited);
|
||||
REQUIRE(filament_color_picker_changed(*original, edited));
|
||||
const auto result = filament_color_picker_result(*edited);
|
||||
REQUIRE(result.gradient);
|
||||
REQUIRE(result.colors == std::vector<std::string>{"#FF000001", "#0000FFFE"});
|
||||
const auto reloaded = filament_color_picker_selection(result);
|
||||
REQUIRE(reloaded);
|
||||
REQUIRE_FALSE(filament_color_picker_changed(*edited, reloaded));
|
||||
const auto reversed = filament_color_picker_selection({{result.colors[1], result.colors[0]}, true});
|
||||
REQUIRE(filament_color_picker_changed(*edited, reversed));
|
||||
const auto solid = filament_color_picker_selection({{"#12345680"}, false});
|
||||
const auto alpha_only = filament_color_picker_selection({{"#12345681"}, false});
|
||||
REQUIRE(solid);
|
||||
REQUIRE(alpha_only);
|
||||
REQUIRE(filament_color_picker_changed(*solid, alpha_only));
|
||||
REQUIRE(filament_color_picker_result(*alpha_only).colors == std::vector<std::string>{"#12345681"});
|
||||
const auto mixed = filament_color_picker_selection({{"#FF0000FF", "#0000FF00"}, true});
|
||||
REQUIRE(mixed);
|
||||
REQUIRE(filament_color_picker_result(*mixed).colors == std::vector<std::string>{"#FF0000", "#0000FF00"});
|
||||
}
|
||||
|
||||
TEST_CASE("Unrepresentable filament seeds retain first endpoint or valid primary alpha", "[FilamentColorPicker]")
|
||||
{
|
||||
auto too_many = too_many_gradient_colors();
|
||||
too_many.front() = "#FF000000";
|
||||
const auto value = GENERATE_COPY(FilamentColorPickerValue{too_many, true},
|
||||
FilamentColorPickerValue{{"#FF000000", "#0000FF80"}, false},
|
||||
FilamentColorPickerValue{{"invalid", "#FF0000"}, true, "#FF000000"});
|
||||
REQUIRE_FALSE(filament_color_picker_selection(value));
|
||||
const auto initial = filament_color_picker_initial(value);
|
||||
REQUIRE(filament_color_picker_result(initial).colors == std::vector<std::string>{"#FF000000"});
|
||||
REQUIRE_FALSE(filament_color_picker_changed(initial, initial));
|
||||
REQUIRE_FALSE(filament_color_picker_changed(initial, std::nullopt));
|
||||
const auto edited = filament_color_picker_selection({{"#FF000001"}, false});
|
||||
REQUIRE(filament_color_picker_changed(initial, edited));
|
||||
}
|
||||
|
||||
TEST_CASE("Ordered multi-color gradients round trip up to the color limit", "[FilamentColorPicker]")
|
||||
{
|
||||
const std::vector<std::string> all = {"#FF0000", "#FFFF0080", "#00FF00", "#00FFFF01", "#0000FF", "#FF00FF00", "#123456"};
|
||||
for (std::size_t count = 2; count <= std::min(all.size(), COLOR_PICKER_MAX_GRADIENT_COLORS); ++count) {
|
||||
const std::vector<std::string> colors(all.begin(), all.begin() + count);
|
||||
const auto selection = filament_color_picker_selection({colors, true});
|
||||
REQUIRE(selection);
|
||||
const auto result = filament_color_picker_result(*selection);
|
||||
REQUIRE(result.gradient);
|
||||
REQUIRE(result.colors == colors);
|
||||
REQUIRE_FALSE(filament_color_picker_changed(*selection, filament_color_picker_selection(result)));
|
||||
std::vector<std::string> reordered = colors;
|
||||
std::swap(reordered.front(), reordered.back());
|
||||
REQUIRE(filament_color_picker_changed(*selection, filament_color_picker_selection({reordered, true})));
|
||||
}
|
||||
REQUIRE_FALSE(filament_color_picker_selection({too_many_gradient_colors(), true}));
|
||||
}
|
||||
|
||||
TEST_CASE("Filament selections require complete hexadecimal colors and exact endpoint counts", "[FilamentColorPicker]")
|
||||
{
|
||||
const std::string hex = GENERATE("", "112233", "#ABC", "#1234567", "#123456789", "#GG2233", "#11223Z", "#112233G0", " #112233", "#112233 ");
|
||||
REQUIRE_FALSE(filament_color_picker_selection({{hex}, false}));
|
||||
REQUIRE_FALSE(filament_color_picker_selection({{"#112233", hex}, true}));
|
||||
const auto malformed = GENERATE(FilamentColorPickerValue{{}, false},
|
||||
FilamentColorPickerValue{{}, true},
|
||||
FilamentColorPickerValue{{"#112233"}, true});
|
||||
REQUIRE_FALSE(filament_color_picker_selection(malformed));
|
||||
}
|
||||
|
||||
TEST_CASE("Every RGBA byte survives the public filament conversion round trip", "[FilamentColorPicker]")
|
||||
{
|
||||
constexpr char digits[] = "0123456789ABCDEF";
|
||||
for (int byte = 0; byte < 256; ++byte) {
|
||||
std::string hex = "#";
|
||||
for (int channel = 0; channel < 4; ++channel) {
|
||||
hex += digits[byte >> 4];
|
||||
hex += digits[byte & 15];
|
||||
}
|
||||
const auto selection = filament_color_picker_selection({{hex}, false});
|
||||
REQUIRE(selection);
|
||||
const auto result = filament_color_picker_result(*selection);
|
||||
REQUIRE(result.colors == std::vector<std::string>{byte == 255 ? hex.substr(0, 7) : hex});
|
||||
REQUIRE_FALSE(filament_color_picker_changed(*selection, filament_color_picker_selection(result)));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Filament serialization rejects invalid native RGBA channels", "[FilamentColorPicker]")
|
||||
{
|
||||
const float invalid = GENERATE(-0.1f, 1.1f, std::numeric_limits<float>::infinity(), std::numeric_limits<float>::quiet_NaN());
|
||||
const int channel = GENERATE(0, 1, 2, 3);
|
||||
ColorRGBA color;
|
||||
color[channel] = invalid;
|
||||
REQUIRE_THROWS_AS(filament_color_picker_result(color), std::invalid_argument);
|
||||
REQUIRE_THROWS_AS(filament_color_picker_result(ColorGradient{ColorRGBA::WHITE(), color}), std::invalid_argument);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include <string>
|
||||
#include "slic3r/Utils/MoonrakerPrinterAgent.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
namespace {
|
||||
|
||||
class DiscoveryAgent : public MoonrakerPrinterAgent
|
||||
{
|
||||
public:
|
||||
using MoonrakerPrinterAgent::MoonrakerPrinterAgent;
|
||||
using MoonrakerPrinterAgent::device_info;
|
||||
mutable unsigned fetch_count = 0;
|
||||
|
||||
protected:
|
||||
bool fetch_device_info(const std::string&, const std::string&, MoonrakerDeviceInfo& info, std::string&) const override
|
||||
{
|
||||
++fetch_count;
|
||||
info.dev_name = "Test printer";
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Moonraker discovery waits for a complete device identity", "[MoonrakerDiscovery]")
|
||||
{
|
||||
DiscoveryAgent agent("");
|
||||
unsigned announcements = 0;
|
||||
nlohmann::json announcement;
|
||||
agent.set_on_ssdp_msg_fn([&](std::string payload) {
|
||||
++announcements;
|
||||
announcement = nlohmann::json::parse(payload);
|
||||
});
|
||||
|
||||
agent.start_discovery(true, false);
|
||||
REQUIRE(announcements == 0);
|
||||
REQUIRE(agent.fetch_count == 0);
|
||||
|
||||
agent.device_info.dev_id = "127.0.0.1:7125";
|
||||
agent.device_info.dev_ip = agent.device_info.dev_id;
|
||||
agent.device_info.base_url = "http://127.0.0.1:7125";
|
||||
agent.start_discovery(true, false);
|
||||
REQUIRE(announcements == 1);
|
||||
REQUIRE(agent.fetch_count == 1);
|
||||
REQUIRE(announcement.at("dev_id") == agent.device_info.dev_id);
|
||||
REQUIRE(announcement.at("dev_ip") == agent.device_info.dev_ip);
|
||||
REQUIRE(announcement.at("dev_name") == "Test printer");
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
#include <utility>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
|
||||
#include "slic3r/Utils/bambu_networking.hpp"
|
||||
#include "slic3r/Utils/MoonrakerPrinterAgent.hpp"
|
||||
#include "slic3r/Utils/NetworkAgent.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
namespace {
|
||||
|
||||
class DiscoverySource : public MoonrakerPrinterAgent
|
||||
{
|
||||
public:
|
||||
DiscoverySource() : MoonrakerPrinterAgent("") {}
|
||||
|
||||
OnMsgArrivedFn discovery_callback;
|
||||
|
||||
int set_on_ssdp_msg_fn(OnMsgArrivedFn fn) override
|
||||
{
|
||||
discovery_callback = std::move(fn);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Queued discovery cannot cross printer agent sessions", "[NetworkDiscovery]")
|
||||
{
|
||||
auto source = std::make_shared<DiscoverySource>();
|
||||
NetworkAgent network(nullptr, source);
|
||||
std::vector<std::function<void()>> pending;
|
||||
std::vector<std::string> delivered;
|
||||
network.set_queue_on_main_fn([&](std::function<void()> fn) { pending.push_back(std::move(fn)); });
|
||||
network.set_on_ssdp_msg_fn([&](std::string payload) { delivered.push_back(std::move(payload)); });
|
||||
|
||||
auto outgoing_callback = source->discovery_callback;
|
||||
outgoing_callback("unsaved-device");
|
||||
outgoing_callback("saved-moonraker-device");
|
||||
REQUIRE(delivered.empty());
|
||||
REQUIRE(pending.size() == 2);
|
||||
|
||||
bool expect_active = true;
|
||||
SECTION("Switch to another agent")
|
||||
{
|
||||
network.set_printer_agent(std::make_shared<DiscoverySource>());
|
||||
}
|
||||
SECTION("Clear the live agent")
|
||||
{
|
||||
network.set_printer_agent(nullptr);
|
||||
expect_active = false;
|
||||
}
|
||||
SECTION("Switch away and back to the cached agent")
|
||||
{
|
||||
network.set_printer_agent(std::make_shared<DiscoverySource>());
|
||||
network.set_printer_agent(source);
|
||||
}
|
||||
SECTION("Unregister discovery")
|
||||
{
|
||||
network.set_on_ssdp_msg_fn(nullptr);
|
||||
expect_active = false;
|
||||
}
|
||||
SECTION("Replace the discovery listener")
|
||||
{
|
||||
network.set_on_ssdp_msg_fn([&](std::string payload) { delivered.push_back(std::move(payload)); });
|
||||
}
|
||||
|
||||
for (auto& fn : pending)
|
||||
fn();
|
||||
pending.clear();
|
||||
REQUIRE(delivered.empty());
|
||||
|
||||
outgoing_callback("late-outgoing-device");
|
||||
REQUIRE(pending.empty());
|
||||
REQUIRE(delivered.empty());
|
||||
|
||||
if (expect_active) {
|
||||
auto active = std::dynamic_pointer_cast<DiscoverySource>(network.get_printer_agent());
|
||||
REQUIRE(active);
|
||||
REQUIRE(active->discovery_callback);
|
||||
active->discovery_callback("active-device");
|
||||
REQUIRE(delivered.empty());
|
||||
REQUIRE(pending.size() == 1);
|
||||
pending.front()();
|
||||
REQUIRE(delivered == std::vector<std::string>{"active-device"});
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Discovery callbacks expire when networking is destroyed", "[NetworkDiscovery]")
|
||||
{
|
||||
auto source = std::make_shared<DiscoverySource>();
|
||||
std::vector<std::function<void()>> pending;
|
||||
unsigned delivered = 0;
|
||||
{
|
||||
NetworkAgent network(nullptr, source);
|
||||
network.set_queue_on_main_fn([&](std::function<void()> fn) { pending.push_back(std::move(fn)); });
|
||||
network.set_on_ssdp_msg_fn([&](std::string) { ++delivered; });
|
||||
source->discovery_callback("queued-device");
|
||||
}
|
||||
|
||||
REQUIRE(pending.size() == 1);
|
||||
pending.front()();
|
||||
pending.clear();
|
||||
source->discovery_callback("late-device");
|
||||
REQUIRE(pending.empty());
|
||||
REQUIRE(delivered == 0);
|
||||
}
|
||||
@@ -56,6 +56,14 @@ TEST_CASE("Plugin host API exposes host-owned bundle and preset surface to Pytho
|
||||
REQUIRE(has_attr(host, "Model"));
|
||||
REQUIRE(has_attr(host, "ModelObject"));
|
||||
REQUIRE(has_attr(host, "Plater"));
|
||||
REQUIRE(has_attr(host, "preset"));
|
||||
py::object preset = host.attr("preset");
|
||||
REQUIRE(has_attr(preset, "bundles"));
|
||||
py::object bundles = preset.attr("bundles");
|
||||
CHECK(has_attr(bundles, "list_ids"));
|
||||
CHECK(has_attr(bundles, "reload_local"));
|
||||
CHECK_FALSE(has_attr(host, "reload_local_bundle"));
|
||||
CHECK_FALSE(has_attr(host, "list_bundle_ids"));
|
||||
|
||||
py::object preset_bundle_type = host.attr("PresetBundle");
|
||||
CHECK(has_attr(preset_bundle_type, "prints"));
|
||||
@@ -132,6 +140,7 @@ TEST_CASE("Plugin host API exposes host-owned bundle and preset surface to Pytho
|
||||
TEST_CASE("Plugin host API reports unavailable GUI objects before Orca app initialization", "[PluginHost][Python]")
|
||||
{
|
||||
py::object host = import_orca_module().attr("host");
|
||||
py::object bundles = host.attr("preset").attr("bundles");
|
||||
|
||||
for (const char* function_name : { "preset_bundle", "plater", "model", "app_info", "selected_printer" }) {
|
||||
CAPTURE(function_name);
|
||||
@@ -143,6 +152,22 @@ TEST_CASE("Plugin host API reports unavailable GUI objects before Orca app initi
|
||||
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
bundles.attr("reload_local")("test-bundle");
|
||||
FAIL("host.preset.bundles.reload_local unexpectedly succeeded without a wx application");
|
||||
} catch (const py::error_already_set& error) {
|
||||
CHECK(error.matches(PyExc_RuntimeError));
|
||||
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
|
||||
}
|
||||
|
||||
try {
|
||||
bundles.attr("list_ids")();
|
||||
FAIL("host.preset.bundles.list_ids unexpectedly succeeded without a wx application");
|
||||
} catch (const py::error_already_set& error) {
|
||||
CHECK(error.matches(PyExc_RuntimeError));
|
||||
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Plugin host API reports no GL details before the 3D view exists", "[PluginHost][Python]")
|
||||
|
||||
Reference in new issue
Block a user