feat: introduce plugin APIs for mutable presets

This commit is contained in:
Ian Chua
2026-10-15 20:32:49 +08:00
parent aacc36b9d2
commit 193cfc6dbf
7 changed files with 1369 additions and 0 deletions
+104
View File
@@ -6,8 +6,12 @@
#include <libslic3r/TriangleMesh.hpp>
#include <slic3r/GUI/DockPanel.hpp>
#include <slic3r/GUI/AuiPaneLayout.hpp>
#include <slic3r/GUI/OptionsGroup.hpp>
#include <slic3r/GUI/Widgets/WebHosting.hpp>
#include <map>
#include <utility>
#include <catch2/catch_test_macros.hpp>
#include <catch2/catch_message.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
@@ -137,6 +141,106 @@ TEST_CASE("Plugin host API exposes host-owned bundle and preset surface to Pytho
CHECK(printers.attr("find_preset")(printer_preset.name).attr("name").cast<std::string>() == printer_preset.name);
}
TEST_CASE("Plugin host API exposes the editable preset category handles and guards them before Orca app initialization",
"[PluginHost][Python]")
{
py::object host = import_orca_module().attr("host");
py::object preset = host.attr("preset");
REQUIRE(has_attr(preset, "PresetEditor"));
REQUIRE(has_attr(preset, "process"));
REQUIRE(has_attr(preset, "printer"));
REQUIRE(has_attr(preset, "filament"));
py::object process = preset.attr("process");
py::object printer = preset.attr("printer");
py::object filament = preset.attr("filament");
CHECK(process.attr("type").cast<Slic3r::Preset::Type>() == Slic3r::Preset::TYPE_PRINT);
CHECK(printer.attr("type").cast<Slic3r::Preset::Type>() == Slic3r::Preset::TYPE_PRINTER);
CHECK(filament.attr("type").cast<Slic3r::Preset::Type>() == Slic3r::Preset::TYPE_FILAMENT);
for (const py::object& editor : { process, printer, filament })
CHECK(has_attr(editor, "update"));
// With no wx application the call must fail cleanly, like the rest of host.preset, and must not
// be mistaken for a missing attribute or an argument error.
for (const py::object& editor : { process, printer, filament }) {
try {
editor.attr("update")("layer_height", "0.25");
FAIL("preset editor update 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 exposes the filament slot form of preset update and guards it", "[PluginHost][Python]")
{
py::object host = import_orca_module().attr("host");
py::object preset = host.attr("preset");
// The filament editor accepts update(slot_number, key, value) as well as update(key, value); with
// no wx application it reaches the same clean failure as the rest of host.preset. The slot
// validation and the switch/cancel path need a running app, so they are not exercised here.
try {
preset.attr("filament").attr("update")(1, "layer_height", "0.25");
FAIL("slot-addressed filament update 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);
}
// The process and printer editors accept only update(key, value); the slot form is rejected
// before any application access, naming the editor that does accept it.
for (const char* name : { "process", "printer" }) {
CAPTURE(name);
try {
preset.attr(name).attr("update")(1, "layer_height", "0.25");
FAIL("slot-addressed update on a non-filament editor unexpectedly succeeded");
} catch (const py::error_already_set& error) {
CHECK(error.matches(PyExc_ValueError));
CHECK(std::string(error.what()).find("only available on") != std::string::npos);
}
}
}
TEST_CASE("A settings group resolves an option's id from its option map", "[PluginHost]")
{
using Slic3r::GUI::option_id_in_map;
using Slic3r::GUI::t_opt_map;
// The map ConfigOptionsGroup::get_option leaves: a scalar option, and a per-extruder option
// whose entries carry their index.
t_opt_map options;
options.emplace("layer_height", std::make_pair(std::string("layer_height"), -1));
options.emplace("nozzle_diameter#0", std::make_pair(std::string("nozzle_diameter"), 0));
options.emplace("nozzle_diameter#1", std::make_pair(std::string("nozzle_diameter"), 1));
// A negative index resolves a scalar option and yields its own (negative) index.
int index = -1;
CHECK(option_id_in_map(options, "layer_height", index) == "layer_height");
CHECK(index == -1);
// A negative index on a vector option matches several entries, so it resolves to nothing.
index = -1;
CHECK(option_id_in_map(options, "nozzle_diameter", index).empty());
// An explicit index selects that entry.
index = 1;
CHECK(option_id_in_map(options, "nozzle_diameter", index) == "nozzle_diameter#1");
CHECK(index == 1);
// An index no entry carries, and a key the map does not own, resolve to nothing.
index = 7;
CHECK(option_id_in_map(options, "nozzle_diameter", index).empty());
index = -1;
CHECK(option_id_in_map(options, "wall_loops", index).empty());
const t_opt_map empty;
CHECK(option_id_in_map(empty, "layer_height", index).empty());
}
TEST_CASE("Plugin host API reports unavailable GUI objects before Orca app initialization", "[PluginHost][Python]")
{
py::object host = import_orca_module().attr("host");