Merge impl branch

This commit is contained in:
Lam Wei Lun
2026-09-30 12:42:51 +08:00
2934 changed files with 209925 additions and 266931 deletions
+5
View File
@@ -9,6 +9,10 @@ add_executable(${_TEST_NAME}_tests
test_ams_item.cpp
test_device_manager_integration.cpp
test_web_media_controller.cpp
test_scene_raycaster.cpp
test_lazy.cpp
test_prebuild_queue.cpp
test_staged_build.cpp
test_network_versions.cpp
test_action_source.cpp
test_plugin_host_api.cpp
@@ -23,6 +27,7 @@ add_executable(${_TEST_NAME}_tests
test_plugin_install.cpp
test_plugin_lifecycle.cpp
test_plugin_printer_agent.cpp
test_printhost.cpp
test_slicing_pipeline_bindings.cpp
test_slicing_pipeline_config.cpp
test_plugin_sort.cpp
+240
View File
@@ -0,0 +1,240 @@
#include <catch2/catch_all.hpp>
#include <functional>
#include <memory>
#include <stdexcept>
#include <vector>
#include "slic3r/GUI/Lazy.hpp"
using Slic3r::GUI::Lazy;
using Slic3r::GUI::LazyBase;
using Slic3r::GUI::LazyInstance;
using Slic3r::GUI::StagedBuild;
namespace {
struct Plain
{
int value{ 1 };
};
struct One : LazyInstance<One>
{
int value{ 2 };
};
// Two steps after the constructor.
struct Staged : StagedBuild, LazyInstance<Staged>
{
std::vector<int> ran;
Staged()
{
add_build_step([this] { ran.push_back(1); });
add_build_step([this] { ran.push_back(2); });
}
void add_step(std::function<void()> step) { add_build_step(std::move(step)); }
};
// Owns what the factories make, since a Lazy does not.
template <class T>
struct Made
{
std::vector<std::unique_ptr<T>> objects;
T* make()
{
objects.push_back(std::make_unique<T>());
return objects.back().get();
}
typename Lazy<T>::Factory factory()
{
return [this] { return make(); };
}
};
} // namespace
TEST_CASE("The factory runs on the first unit, not at construction", "[Lazy]")
{
Made<Plain> made;
Lazy<Plain> lazy("plain", 0, made.factory());
REQUIRE(made.objects.empty());
REQUIRE_FALSE(lazy.built());
REQUIRE(lazy.pending());
REQUIRE(lazy.get() == nullptr);
REQUIRE_FALSE(lazy.build_step()); // the only unit
REQUIRE(made.objects.size() == 1);
REQUIRE(lazy.built());
REQUIRE_FALSE(lazy.pending());
REQUIRE(lazy.get() == made.objects[0].get());
REQUIRE_FALSE(lazy.build_step());
REQUIRE(made.objects.size() == 1);
}
TEST_CASE("A staged type takes one unit for the constructor and one per step", "[Lazy]")
{
Made<Staged> made;
Lazy<Staged> lazy("staged", 0, made.factory());
REQUIRE(lazy.build_step());
REQUIRE(made.objects.size() == 1);
REQUIRE(lazy.get() == nullptr); // exists but incomplete
REQUIRE(lazy.build_step());
REQUIRE(made.objects[0]->ran == std::vector<int>{1});
REQUIRE_FALSE(lazy.build_step());
REQUIRE(made.objects[0]->ran == std::vector<int>{1, 2});
REQUIRE(lazy.get() == made.objects[0].get());
}
TEST_CASE("ensure builds whatever is left and is a no-op afterwards", "[Lazy]")
{
Made<Staged> made;
Lazy<Staged> lazy("staged", 0, made.factory());
lazy.build_step();
Staged* s = lazy.ensure();
REQUIRE(s == made.objects[0].get());
REQUIRE(s->ran == std::vector<int>{1, 2});
REQUIRE(lazy.ensure() == s);
REQUIRE(made.objects.size() == 1);
}
TEST_CASE("when_built waits for completion, then runs at once", "[Lazy]")
{
Made<Staged> made;
Lazy<Staged> lazy("staged", 0, made.factory());
std::vector<int> seen;
lazy.when_built([&](Staged& s) { seen.push_back(int(s.ran.size())); });
lazy.build_step();
lazy.build_step();
REQUIRE(seen.empty());
lazy.build_step();
REQUIRE(seen == std::vector<int>{2});
lazy.when_built([&](Staged&) { seen.push_back(9); });
REQUIRE(seen == std::vector<int>{2, 9});
}
TEST_CASE("A LazyInstance type reaches its holder through the statics", "[Lazy]")
{
REQUIRE(One::if_built() == nullptr);
REQUIRE(One::ensure() == nullptr);
Made<One> made;
{
Lazy<One> lazy("one", 0, made.factory());
REQUIRE(One::if_built() == nullptr);
One* one = One::ensure();
REQUIRE(one == made.objects[0].get());
REQUIRE(One::if_built() == one);
int seen = 0;
One::when_built([&](One& o) { seen = o.value; });
REQUIRE(seen == 2);
}
REQUIRE(One::if_built() == nullptr);
}
TEST_CASE("A newer holder replaces the registration; the older one leaves it alone", "[Lazy]")
{
Made<One> made;
auto first = std::make_unique<Lazy<One>>("first", 0, made.factory());
first->ensure();
Lazy<One> second("second", 0, made.factory());
REQUIRE(One::if_built() == nullptr); // the new holder has not built yet
second.ensure();
REQUIRE(One::if_built() == made.objects[1].get());
first.reset();
REQUIRE(One::if_built() == made.objects[1].get());
}
TEST_CASE("The holder reports the name and order it was given", "[Lazy]")
{
Made<Plain> made;
Lazy<Plain> lazy("plain", 7, made.factory());
LazyBase& base = lazy;
REQUIRE(base.name() == "plain");
REQUIRE(base.prebuild_order() == 7);
}
TEST_CASE("A unit that re-enters the holder builds nothing twice", "[Lazy]")
{
Made<Plain> made;
Lazy<Plain>* self = nullptr;
int nested_units = 0;
Lazy<Plain> lazy("plain", 0, [&] {
if (self->build_step()) // as if the constructor pumped the event loop into a slice
++nested_units;
return made.make();
});
self = &lazy;
REQUIRE_FALSE(lazy.build_step());
REQUIRE(nested_units == 0);
REQUIRE(made.objects.size() == 1);
REQUIRE(lazy.built());
}
TEST_CASE("A factory that returns null leaves the holder unbuilt and not pending", "[Lazy]")
{
int calls = 0;
Lazy<Plain> lazy("plain", 0, [&] { ++calls; return static_cast<Plain*>(nullptr); });
REQUIRE_FALSE(lazy.build_step());
REQUIRE_FALSE(lazy.built());
REQUIRE_FALSE(lazy.pending());
REQUIRE(lazy.get() == nullptr);
REQUIRE_FALSE(lazy.build_step()); // not retried
REQUIRE(calls == 1);
}
TEST_CASE("ensure returns null for a factory that returned null", "[Lazy]")
{
Lazy<Plain> lazy("plain", 0, [] { return static_cast<Plain*>(nullptr); });
REQUIRE(lazy.ensure() == nullptr);
REQUIRE_FALSE(lazy.built());
}
TEST_CASE("A nested ensure inside the factory returns null", "[Lazy]")
{
Made<Plain> made;
Lazy<Plain>* self = nullptr;
Plain* nested = reinterpret_cast<Plain*>(1);
Lazy<Plain> lazy("plain", 0, [&] {
nested = self->ensure(); // as if the constructor pumped the event loop into a caller
return made.make();
});
self = &lazy;
Plain* built = lazy.ensure();
REQUIRE(built == made.objects[0].get());
REQUIRE(nested == nullptr);
}
TEST_CASE("A nested ensure during a staged step returns null", "[Lazy]")
{
Made<Staged> made;
Lazy<Staged>* self = nullptr;
Staged* nested = reinterpret_cast<Staged*>(1);
Lazy<Staged> lazy("staged", 0, [&] {
Staged* s = made.make();
s->add_step([&] { nested = self->ensure(); }); // as if a step pumped the event loop into a caller
return s;
});
self = &lazy;
Staged* built = lazy.ensure();
REQUIRE(built == made.objects[0].get());
REQUIRE(nested == nullptr);
}
TEST_CASE("A unit that throws leaves the holder free to build the rest", "[Lazy]")
{
Made<Staged> made;
bool thrown = false;
Lazy<Staged> lazy("staged", 0, [&] {
Staged* s = made.make();
s->add_step([&] { thrown = true; throw std::runtime_error("step"); });
return s;
});
lazy.build_step();
lazy.build_step();
lazy.build_step();
REQUIRE_THROWS(lazy.build_step());
REQUIRE(thrown);
REQUIRE(lazy.pending());
REQUIRE_FALSE(lazy.build_step()); // the next unit runs
REQUIRE(lazy.built());
}
+183
View File
@@ -0,0 +1,183 @@
#include <catch2/catch_all.hpp>
#include <string>
#include <vector>
#include "slic3r/GUI/PrebuildQueue.hpp"
using Slic3r::GUI::LazyBase;
using Slic3r::GUI::PrebuildQueue;
namespace {
// A task with `left` units, each taking `unit_ms` of the shared fake clock and logging its id.
struct Counter : LazyBase
{
std::string id;
int left;
int order;
long long unit_ms{ 1 };
inline static std::vector<int> log;
inline static long long now = 0;
Counter(int id, int left, int order, long long unit_ms = 1) : id(std::to_string(id)), left(left), order(order), unit_ms(unit_ms) {}
const std::string& name() const override { return id; }
bool built() const override { return left == 0; }
bool build_step() override
{
now += unit_ms;
log.push_back(std::stoi(id));
return --left > 0;
}
int prebuild_order() const override { return order; }
};
// Resets the shared log and clock at the start of a case.
struct Reset
{
Reset() { Counter::log.clear(); Counter::now = 0; }
};
const auto fake_clock = [] { return Counter::now; };
const auto no_input = [] { return false; };
// Runs slices with an unlimited budget until nothing is pending; one task per slice.
void drain(PrebuildQueue& q)
{
while (q.pending())
q.run_slice(1000000, fake_clock, no_input);
}
} // namespace
TEST_CASE("Tasks run lowest order first, equal order in the order added", "[PrebuildQueue]")
{
Reset reset;
Counter a{ 1, 1, 10 }, b{ 2, 1, 10 }, c{ 3, 1, 50 }, d{ 4, 1, 100 };
PrebuildQueue q;
q.add(c);
q.add(a);
q.add(b);
q.add(d);
REQUIRE(q.names() == "1, 2, 3, 4");
drain(q);
REQUIRE(Counter::log == std::vector<int>{1, 2, 3, 4});
}
TEST_CASE("A task with nothing pending is skipped, not removed", "[PrebuildQueue]")
{
Reset reset;
Counter a{ 1, 0, 0 }, b{ 2, 2, 1 };
PrebuildQueue q;
q.add(a);
q.add(b);
REQUIRE(q.pending());
auto slice = q.run_slice(1, fake_clock, no_input); // one unit of b
REQUIRE(slice.units == 1);
REQUIRE(Counter::log == std::vector<int>{2});
a.left = 1; // a's work returned; it comes first again
q.run_slice(1, fake_clock, no_input);
REQUIRE(Counter::log == std::vector<int>{2, 1});
}
TEST_CASE("A slice with nothing pending runs no unit", "[PrebuildQueue]")
{
Reset reset;
PrebuildQueue q;
REQUIRE_FALSE(q.pending());
auto slice = q.run_slice(40, fake_clock, no_input);
REQUIRE(slice.units == 0);
REQUIRE_FALSE(slice.completed);
REQUIRE_FALSE(slice.remaining);
}
TEST_CASE("A slice stops once its budget is spent, after the unit that crossed it", "[PrebuildQueue]")
{
Reset reset;
Counter a{ 1, 10, 0, 15 };
PrebuildQueue q;
q.add(a);
auto slice = q.run_slice(40, fake_clock, no_input);
REQUIRE(slice.units == 3); // units end at 15, 30 and 45 ms; the one crossing 40 is the last
REQUIRE(slice.ms == 45);
REQUIRE_FALSE(slice.completed);
REQUIRE(slice.remaining);
REQUIRE(a.left == 7);
}
TEST_CASE("A slice stops after the unit during which input arrived", "[PrebuildQueue]")
{
Reset reset;
Counter a{ 1, 10, 0 };
bool input = false;
PrebuildQueue q;
q.add(a);
auto slice = q.run_slice(40, fake_clock, [&] { input = a.left == 8; return input; });
REQUIRE(slice.units == 2);
REQUIRE_FALSE(slice.completed);
REQUIRE(slice.remaining);
}
TEST_CASE("A slice reports completion, whether work remains, and each unit's time", "[PrebuildQueue]")
{
Reset reset;
Counter a{ 1, 2, 0, 5 }, b{ 2, 1, 1 };
std::vector<long long> unit_ms;
PrebuildQueue q;
q.add(a);
q.add(b);
auto slice = q.run_slice(40, fake_clock, no_input, [&](const std::string& name, long long ms) {
REQUIRE(name == "1");
unit_ms.push_back(ms);
});
REQUIRE(slice.units == 2);
REQUIRE(slice.completed);
REQUIRE(slice.name == "1");
REQUIRE(slice.remaining); // b
REQUIRE(unit_ms == std::vector<long long>{5, 5});
slice = q.run_slice(40, fake_clock, no_input);
REQUIRE(slice.completed);
REQUIRE_FALSE(slice.remaining);
REQUIRE_FALSE(q.pending());
}
TEST_CASE("A unit may add a task to the queue it runs from", "[PrebuildQueue]")
{
Reset reset;
PrebuildQueue q;
Counter later{ 2, 1, 5 };
struct Adder : LazyBase
{
PrebuildQueue& q;
Counter& later;
std::string id{ "1" };
bool done{ false };
Adder(PrebuildQueue& q, Counter& later) : q(q), later(later) {}
const std::string& name() const override { return id; }
bool built() const override { return done; }
bool build_step() override
{
Counter::log.push_back(1);
done = true;
q.add(later);
return false;
}
int prebuild_order() const override { return 0; }
} first{ q, later };
q.add(first);
drain(q);
REQUIRE(Counter::log == std::vector<int>{1, 2});
}
TEST_CASE("clear drops every task", "[PrebuildQueue]")
{
Reset reset;
Counter a{ 1, 1, 0 };
PrebuildQueue q;
q.add(a);
q.clear();
REQUIRE_FALSE(q.pending());
REQUIRE(q.run_slice(40, fake_clock, no_input).units == 0);
}
+213
View File
@@ -0,0 +1,213 @@
#include <catch2/catch_all.hpp>
#include <nlohmann/json.hpp>
#include "slic3r/Utils/PrintHost.hpp"
using namespace Slic3r;
namespace {
class TestPrintHost : public PrintHost
{
public:
using PrintHost::format_error;
const char* get_name() const override { return "Test"; }
bool test(wxString&) const override { return true; }
wxString get_test_ok_msg() const override { return {}; }
wxString get_test_failed_msg(wxString&) const override { return {}; }
bool upload(PrintHostUpload, ProgressFn, ErrorFn, InfoFn) const override { return true; }
bool has_auto_discovery() const override { return false; }
bool can_test() const override { return false; }
PrintHostPostUploadActions get_post_upload_actions() const override { return {}; }
std::string get_host() const override { return {}; }
};
std::string format_error(const std::string& body, const std::string& error, unsigned status)
{
return TestPrintHost().format_error(body, error, status).ToStdString();
}
std::string envelope(int code, const std::string& message, const std::string& traceback)
{
return nlohmann::json{{"error", {{"code", code}, {"message", message}, {"traceback", traceback}}}}.dump();
}
std::string moonraker_error(int code, const std::string& message, const std::string& detail = {})
{
std::string line = "tornado.web.HTTPError: HTTP " + std::to_string(code) + ": " + message;
if (!detail.empty())
line += " (" + detail + ")";
return envelope(code, message, "Traceback (most recent call last):\n ...\n" + line + "\n");
}
// A real Moonraker body for uploading a file that is being printed.
constexpr const char* k_busy_file_403 =
R"JSON({"error": {"code": 403, "message": "Forbidden", "traceback": "Traceback (most recent call last):\n\n File \"/home/lava/moonraker/moonraker/components/file_manager/file_manager.py\", line 1017, in _finish_gcode_upload\n can_start = self._handle_operation_check(check_path)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nmoonraker.utils.exceptions.ServerError: File currently in use\n\nDuring handling of the above exception, another exception occurred:\n\nTraceback (most recent call last):\n\n File \"/home/lava/moonraker/moonraker/components/application.py\", line 1069, in post\n raise tornado.web.HTTPError(\ntornado.web.HTTPError: HTTP 403: Forbidden (File is loaded, upload not permitted)\n"}})JSON";
} // namespace
TEST_CASE("A Klipper upload error shows its reason instead of a Python traceback", "[PrintHost][Regression]")
{
const std::string msg = format_error(k_busy_file_403, "", 403);
INFO("actual: " << msg);
CHECK(msg == "HTTP 403: Forbidden (File is loaded, upload not permitted)");
CHECK_THAT(msg, !Catch::Matchers::ContainsSubstring("Traceback"));
CHECK_THAT(msg, !Catch::Matchers::ContainsSubstring("file_manager.py"));
}
TEST_CASE("The specific cause is recovered from a file endpoint's traceback", "[PrintHost]")
{
SECTION("a plain detail")
{
const std::string body = moonraker_error(403, "Forbidden", "File is loaded, upload not permitted");
CHECK(format_error(body, "", 403) == "HTTP 403: Forbidden (File is loaded, upload not permitted)");
}
SECTION("a detail whose own parentheses nest (a filename)")
{
const std::string detail = "Directory does not exist (/home/pi/gcodes/plate (1).gcode)";
const std::string body = moonraker_error(400, "Bad Request", detail);
CHECK(format_error(body, "", 400) == "HTTP 400: Bad Request (" + detail + ")");
}
SECTION("a detail that contains the reason phrase")
{
const std::string body = moonraker_error(403, "Forbidden", "Forbidden zone: access denied");
CHECK(format_error(body, "", 403) == "HTTP 403: Forbidden (Forbidden zone: access denied)");
}
SECTION("a detail that spans lines")
{
const std::string detail = "Move out of range\nX=250.000 Y=10.000";
const std::string body = moonraker_error(400, "Bad Request", detail);
CHECK(format_error(body, "", 400) == "HTTP 400: Bad Request (" + detail + ")");
}
SECTION("a detail that only repeats the reason phrase is dropped")
{
const std::string body = moonraker_error(401, "Unauthorized", "Unauthorized");
CHECK(format_error(body, "", 401) == "HTTP 401: Unauthorized");
}
}
TEST_CASE("An unhandled exception shows its type and message", "[PrintHost]")
{
const std::string frame = "Traceback (most recent call last):\n"
" File \"/home/pi/moonraker/moonraker/components/file_manager/file_manager.py\", line 1, in write\n"
" self._write(data)\n";
SECTION("a one-line message")
{
const std::string body = envelope(500, "Internal Server Error", frame + "OSError: [Errno 28] No space left on device\n");
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (OSError: [Errno 28] No space left on device)");
}
SECTION("a message that spans lines")
{
const std::string body = envelope(500, "Internal Server Error", frame + "ServerError: Klippy request failed\n see klippy.log\n");
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (ServerError: Klippy request failed\n see klippy.log)");
}
SECTION("raised while handling an HTTPError with the same code")
{
const std::string traceback = frame + "tornado.web.HTTPError: HTTP 500: Internal Server Error (Database locked)\n\n"
"During handling of the above exception, another exception occurred:\n\n" +
frame + "OSError: [Errno 5] Input/output error\n";
const std::string body = envelope(500, "Internal Server Error", traceback);
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (OSError: [Errno 5] Input/output error)");
}
SECTION("a traceback with no header")
{
const std::string body = envelope(500, "Internal Server Error", "OSError: [Errno 5] Input/output error");
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (OSError: [Errno 5] Input/output error)");
}
}
TEST_CASE("A reason already complete in message is shown unchanged", "[PrintHost]")
{
SECTION("message is the whole reason, no trailing detail")
{
const std::string body = moonraker_error(503, "Klippy is not ready");
CHECK(format_error(body, "", 503) == "HTTP 503: Klippy is not ready");
}
SECTION("a message that itself contains parentheses is not duplicated")
{
const std::string reason = "Requested blocks (0-5) are unavailable";
const std::string body = moonraker_error(400, reason);
CHECK(format_error(body, "", 400) == "HTTP 400: " + reason);
}
}
TEST_CASE("A Moonraker error with no usable detail shows just the reason phrase", "[PrintHost]")
{
struct Case
{
const char* name;
const char* body;
unsigned status;
const char* expected;
};
const auto c = GENERATE(
Case{"an empty traceback", R"JSON({"error": {"code": 500, "message": "Internal Server Error", "traceback": ""}})JSON", 500,
"HTTP 500: Internal Server Error"},
Case{"a traceback of only whitespace", R"JSON({"error": {"code": 500, "message": "Internal Server Error", "traceback": "\n \n"}})JSON",
500, "HTTP 500: Internal Server Error"});
DYNAMIC_SECTION(c.name) { CHECK(format_error(c.body, "", c.status) == c.expected); }
}
TEST_CASE("A percent sign in the reason is not a format specifier", "[PrintHost]")
{
const std::string body = moonraker_error(507, "Insufficient Storage", "disk 100% full");
CHECK(format_error(body, "", 507) == "HTTP 507: Insufficient Storage (disk 100% full)");
}
TEST_CASE("Error bodies that are not a Moonraker envelope are left unchanged", "[PrintHost]")
{
SECTION("OctoPrint's string-valued error member")
{
const std::string body = R"JSON({"error": "File not found"})JSON";
CHECK(format_error(body, "", 404) == "HTTP 404: " + body);
}
SECTION("PrusaLink's top-level message, not under error")
{
const std::string body = R"JSON({"title": "Conflict", "message": "Printer is printing"})JSON";
CHECK(format_error(body, "", 409) == "HTTP 409: " + body);
}
SECTION("a body that is not JSON")
{
const std::string html = "<html><head><title>502 Bad Gateway</title></head></html>";
CHECK(format_error(html, "", 502) == "HTTP 502: " + html);
}
SECTION("an error object with no traceback")
{
const std::string body = R"JSON({"error": {"code": 500, "message": "Internal Server Error"}})JSON";
CHECK(format_error(body, "", 500) == "HTTP 500: " + body);
}
SECTION("an error object whose traceback is null")
{
const std::string body = R"JSON({"error": {"code": 500, "message": "Internal Server Error", "traceback": null}})JSON";
CHECK(format_error(body, "", 500) == "HTTP 500: " + body);
}
SECTION("an envelope whose reason phrase is empty")
{
const std::string body = envelope(403, "", "Traceback (most recent call last):\nOSError: denied\n");
CHECK(format_error(body, "", 403) == "HTTP 403: " + body);
}
SECTION("a transport error with no HTTP status")
{
CHECK(format_error("", "curl:Could not connect", 0) == "curl:Could not connect");
}
}
+178
View File
@@ -0,0 +1,178 @@
// Orca: This suite links libslic3r_gui; navigation raycasts need no wx application or GL context.
#ifdef WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
// Match the GUI precompiled header: wx/msw/wrapcctl.h needs HDITEM from CommCtrl.h.
#include <CommCtrl.h>
#endif
#include <catch2/catch_all.hpp>
#include "slic3r/GUI/Camera.hpp"
#include "slic3r/GUI/CameraUtils.hpp"
#include "slic3r/GUI/PartPlate.hpp"
#include "slic3r/GUI/SceneRaycaster.hpp"
using namespace Slic3r;
using namespace Slic3r::GUI;
namespace {
Camera horizontal_camera(Camera::EType type = Camera::EType::Perspective)
{
Camera camera;
camera.set_type(type);
camera.look_at({0.0, 0.0, 10.0}, {0.0, 100.0, 10.0}, Vec3d::UnitZ());
camera.set_viewport(0, 0, 600, 600);
camera.apply_projection(-1.0, 1.0, -1.0, 1.0, 1.0, 1000.0);
return camera;
}
SceneRaycaster::HitResult scene_hit(const SceneRaycaster& scene, const Camera& camera, const Vec3d& point,
SceneRaycaster::EHitMode mode = SceneRaycaster::EHitMode::SceneOnly)
{
return scene.hit(CameraUtils::project(camera, point).cast<double>(), camera, nullptr, mode);
}
} // namespace
TEST_CASE("Navigation hits the visible bed below a horizontal perspective view", "[SceneRaycaster][Regression]")
{
const MeshRaycaster bed(TriangleMesh(
{{-100.f, 20.f, 0.f}, {100.f, 20.f, 0.f}, {100.f, 200.f, 0.f}, {-100.f, 200.f, 0.f}},
{{0, 1, 2}, {0, 2, 3}}));
SceneRaycaster scene;
scene.add_raycaster(SceneRaycaster::EType::Bed, 0, bed, Transform3d::Identity());
const Camera camera = horizontal_camera();
const auto hit = scene_hit(scene, camera, {0.0, 100.0, 0.0});
REQUIRE(hit.is_valid());
CHECK(hit.type == SceneRaycaster::EType::Bed);
CHECK_THAT(hit.position.z(), Catch::Matchers::WithinAbs(0.0, 1e-4));
}
TEST_CASE("Navigation hits a visible side face away from the perspective view center", "[SceneRaycaster][Regression]")
{
const auto mode = GENERATE(SceneRaycaster::EHitMode::SceneOnly, SceneRaycaster::EHitMode::VolumesOnly);
const MeshRaycaster side(TriangleMesh(
{{10.f, 20.f, -100.f}, {10.f, 20.f, 100.f}, {10.f, 200.f, 100.f}, {10.f, 200.f, -100.f}},
{{0, 1, 2}, {0, 2, 3}}));
SceneRaycaster scene;
scene.add_raycaster(SceneRaycaster::EType::Volume, 0, side, Transform3d::Identity());
const Camera camera = horizontal_camera();
const auto hit = scene_hit(scene, camera, {10.0, 100.0, 10.0}, mode);
REQUIRE(hit.is_valid());
CHECK(hit.type == SceneRaycaster::EType::Volume);
CHECK_THAT(hit.position.x(), Catch::Matchers::WithinAbs(10.0, 1e-4));
}
TEST_CASE("Navigation ignores gizmos and inactive volumes and chooses the nearest scene surface", "[SceneRaycaster]")
{
const bool gizmos_on_top = GENERATE(false, true);
const auto mode = GENERATE(SceneRaycaster::EHitMode::SceneOnly, SceneRaycaster::EHitMode::VolumesOnly);
const auto type = GENERATE(Camera::EType::Perspective, Camera::EType::Ortho);
const MeshRaycaster cube(make_cube(20.0, 20.0, 20.0));
SceneRaycaster scene;
scene.set_gizmos_on_top(gizmos_on_top);
scene.add_raycaster(SceneRaycaster::EType::Gizmo, 0, cube, Geometry::translation_transform({-10.0, 20.0, 0.0}));
scene.add_raycaster(SceneRaycaster::EType::FallbackGizmo, 0, cube, Geometry::translation_transform({-10.0, 30.0, 0.0}));
scene.add_raycaster(SceneRaycaster::EType::Volume, 0, cube, Geometry::translation_transform({-10.0, 40.0, 0.0}))->set_active(false);
scene.add_raycaster(SceneRaycaster::EType::Volume, 1, cube, Geometry::translation_transform({-10.0, 150.0, 0.0}));
scene.add_raycaster(SceneRaycaster::EType::Volume, 2, cube, Geometry::translation_transform({-10.0, 100.0, 0.0}));
const Camera camera = horizontal_camera(type);
const auto hit = scene_hit(scene, camera, {0.0, 100.0, 10.0}, mode);
REQUIRE(hit.is_valid());
CHECK(hit.type == SceneRaycaster::EType::Volume);
CHECK(hit.raycaster_id == 2);
CHECK_THAT(hit.position.y(), Catch::Matchers::WithinAbs(100.0, 1e-4));
}
TEST_CASE("Navigation skips bed raycasters when the bed is hidden", "[SceneRaycaster][Regression]")
{
const auto mode = GENERATE(SceneRaycaster::EHitMode::SceneOnly, SceneRaycaster::EHitMode::VolumesOnly);
const auto type = GENERATE(Camera::EType::Perspective, Camera::EType::Ortho);
const bool looking_downward = GENERATE(false, true);
const MeshRaycaster cube(make_cube(20.0, 20.0, 20.0));
SceneRaycaster scene;
scene.add_raycaster(SceneRaycaster::EType::Bed, 0, cube, Geometry::translation_transform({-10.0, 40.0, 0.0}));
scene.add_raycaster(SceneRaycaster::EType::Volume, 0, cube, Geometry::translation_transform({-10.0, 100.0, 0.0}));
Camera camera = horizontal_camera(type);
if (looking_downward)
camera.look_at({0.0, 0.0, 10.0}, {0.0, 100.0, 0.0}, Vec3d::UnitZ());
const auto hit = scene_hit(scene, camera, {0.0, 100.0, 10.0}, mode);
REQUIRE(hit.is_valid());
CHECK(hit.type == (mode == SceneRaycaster::EHitMode::SceneOnly ?
SceneRaycaster::EType::Bed : SceneRaycaster::EType::Volume));
scene.remove_raycasters(SceneRaycaster::EType::Volume);
CHECK(scene_hit(scene, camera, {0.0, 100.0, 10.0}, mode).is_valid() ==
(mode == SceneRaycaster::EHitMode::SceneOnly));
}
TEST_CASE("Navigation ignores plate controls while retaining plate surfaces and volumes", "[SceneRaycaster][Regression]")
{
const int plate_index = GENERATE(0, 2);
const int component = GENERATE(range(1, int(PartPlate::GRABBER_COUNT)));
const int bed_id = plate_index * PartPlate::GRABBER_COUNT;
const MeshRaycaster cube(make_cube(20.0, 20.0, 20.0));
SceneRaycaster scene;
scene.add_raycaster(SceneRaycaster::EType::Bed, bed_id + component, cube,
Geometry::translation_transform({-10.0, 40.0, 0.0}));
scene.add_raycaster(SceneRaycaster::EType::Bed, bed_id, cube,
Geometry::translation_transform({-10.0, 100.0, 0.0}));
scene.add_raycaster(SceneRaycaster::EType::Volume, 0, cube,
Geometry::translation_transform({-10.0, 150.0, 0.0}));
const Camera camera = horizontal_camera();
auto hit = scene_hit(scene, camera, {0.0, 100.0, 10.0});
REQUIRE(hit.is_valid());
CHECK(hit.type == SceneRaycaster::EType::Bed);
CHECK(hit.raycaster_id == bed_id);
scene.remove_raycasters(SceneRaycaster::EType::Bed, bed_id);
hit = scene_hit(scene, camera, {0.0, 100.0, 10.0});
REQUIRE(hit.is_valid());
CHECK(hit.type == SceneRaycaster::EType::Volume);
// A control alone must leave navigation free to choose its fallback anchor.
scene.remove_raycasters(SceneRaycaster::EType::Volume);
CHECK_FALSE(scene_hit(scene, camera, {0.0, 100.0, 10.0}).is_valid());
}
TEST_CASE("Navigation respects the back-face policy away from the perspective view center", "[SceneRaycaster]")
{
const bool use_back_faces = GENERATE(false, true);
const auto mode = GENERATE(SceneRaycaster::EHitMode::SceneOnly, SceneRaycaster::EHitMode::VolumesOnly);
const MeshRaycaster side(TriangleMesh(
{{10.f, 20.f, -100.f}, {10.f, 20.f, 100.f}, {10.f, 200.f, 100.f}, {10.f, 200.f, -100.f}},
{{0, 2, 1}, {0, 3, 2}}));
SceneRaycaster scene;
scene.add_raycaster(SceneRaycaster::EType::Volume, 0, side, Transform3d::Identity(), use_back_faces);
const auto hit = scene_hit(scene, horizontal_camera(), {10.0, 100.0, 10.0}, mode);
CHECK(hit.is_valid() == use_back_faces);
}
TEST_CASE("Navigation ignores volume surfaces removed by the clipping plane", "[SceneRaycaster]")
{
const auto mode = GENERATE(SceneRaycaster::EHitMode::SceneOnly, SceneRaycaster::EHitMode::VolumesOnly);
const MeshRaycaster cube(make_cube(20.0, 20.0, 20.0));
SceneRaycaster scene;
scene.add_raycaster(SceneRaycaster::EType::Volume, 0, cube, Geometry::translation_transform({-10.0, 100.0, 0.0}));
scene.add_raycaster(SceneRaycaster::EType::Volume, 1, cube, Geometry::translation_transform({-10.0, 150.0, 0.0}));
const Camera camera = horizontal_camera();
const ClippingPlane clipping_plane(-Vec3d::UnitY(), -130.0);
const auto hit = scene.hit({300.0, 300.0}, camera, &clipping_plane, mode);
REQUIRE(hit.is_valid());
CHECK(hit.raycaster_id == 1);
CHECK_THAT(hit.position.y(), Catch::Matchers::WithinAbs(150.0, 1e-4));
}
+14
View File
@@ -120,6 +120,20 @@ TEST_CASE("Only modified or non-printable chords qualify as menu accelerators",
CHECK(registry.accelerator(Shortcut::KeyboardShortcuts).empty());
}
TEST_CASE("Chords the desktop keeps for itself are recognized", "[Shortcuts]")
{
#ifdef _WIN32
CHECK(KeyChord{ WXK_F4, wxMOD_ALT }.is_system_shortcut());
CHECK(KeyChord{ WXK_SPACE, wxMOD_ALT }.is_system_shortcut());
#else
CHECK_FALSE(KeyChord{ WXK_F4, wxMOD_ALT }.is_system_shortcut());
CHECK_FALSE(KeyChord{ WXK_SPACE, wxMOD_ALT }.is_system_shortcut());
#endif
CHECK_FALSE(KeyChord{ WXK_F4, wxMOD_ALT | wxMOD_SHIFT }.is_system_shortcut());
CHECK_FALSE(KeyChord{ WXK_F4, wxMOD_CONTROL }.is_system_shortcut());
CHECK_FALSE(KeyChord{ WXK_SPACE }.is_system_shortcut());
}
TEST_CASE("Chords convert to wx accelerator entries", "[Shortcuts]")
{
const wxAcceleratorEntry entry = KeyChord{ 'S', wxMOD_CONTROL | wxMOD_SHIFT }.to_accelerator_entry(42);
+105
View File
@@ -0,0 +1,105 @@
#include <catch2/catch_all.hpp>
#include <vector>
#include "slic3r/GUI/StagedBuild.hpp"
using Slic3r::GUI::StagedBuild;
namespace {
// Exposes the protected queueing calls and records the order steps ran in.
struct Staged : StagedBuild
{
std::vector<int> ran;
void queue(int id) { add_build_step([this, id] { ran.push_back(id); }); }
void queue_child(Staged& child) { add_build_steps_of(child); }
void queue_nested(int id, int nested)
{
add_build_step([this, id, nested] {
ran.push_back(id);
queue(nested);
});
}
};
} // namespace
TEST_CASE("Steps run in the order they were queued, one per build_step", "[StagedBuild]")
{
Staged s;
s.queue(1);
s.queue(2);
s.queue(3);
REQUIRE_FALSE(s.built());
REQUIRE(s.build_step());
REQUIRE(s.ran == std::vector<int>{1});
REQUIRE(s.build_step());
REQUIRE(s.ran == std::vector<int>{1, 2});
REQUIRE_FALSE(s.build_step());
REQUIRE(s.ran == std::vector<int>{1, 2, 3});
REQUIRE(s.built());
REQUIRE_FALSE(s.build_step());
REQUIRE(s.ran.size() == 3);
}
TEST_CASE("A panel with no steps is built from the start", "[StagedBuild]")
{
Staged s;
REQUIRE(s.built());
REQUIRE_FALSE(s.build_step());
}
TEST_CASE("A step may queue another step, which runs after the ones already queued", "[StagedBuild]")
{
Staged s;
s.queue_nested(1, 3);
s.queue(2);
REQUIRE(s.build_step());
REQUIRE_FALSE(s.built());
REQUIRE(s.build_step());
REQUIRE_FALSE(s.build_step());
REQUIRE(s.ran == std::vector<int>{1, 2, 3});
REQUIRE(s.built());
}
TEST_CASE("A parent waits for steps a child queues after being adopted", "[StagedBuild]")
{
Staged child;
child.queue_nested(1, 2); // step 1 queues step 2 while it runs
Staged parent;
parent.queue_child(child); // one forwarder, for step 1
parent.queue(10);
REQUIRE(parent.build_step()); // child step 1, which queues step 2
REQUIRE(parent.build_step()); // 10; own steps exhausted, the child still has 2
REQUIRE_FALSE(parent.built());
REQUIRE_FALSE(parent.build_step()); // child step 2
REQUIRE(child.ran == std::vector<int>{1, 2});
REQUIRE(parent.ran == std::vector<int>{10});
REQUIRE(parent.built());
}
TEST_CASE("A child's remaining steps are forwarded one per parent step", "[StagedBuild]")
{
Staged child;
child.queue(1);
child.queue(2);
child.queue(3);
REQUIRE(child.build_step()); // the parent adopts only what is left
Staged parent;
parent.queue_child(child);
parent.queue(10);
REQUIRE(parent.build_step());
REQUIRE(child.ran == std::vector<int>{1, 2});
REQUIRE(parent.build_step());
REQUIRE(child.ran == std::vector<int>{1, 2, 3});
REQUIRE(child.built());
REQUIRE_FALSE(parent.build_step());
REQUIRE(parent.ran == std::vector<int>{10});
REQUIRE(parent.built());
}