Plate controls

This commit is contained in:
Lam Wei Lun
2026-09-09 11:29:18 +08:00
parent a1ee7f099f
commit cae23a933e
5 changed files with 207 additions and 13 deletions
+171 -2
View File
@@ -10,6 +10,7 @@
#include "MainFrame.hpp"
#include "Notebook.hpp"
#include "Plater.hpp"
#include "PlateSettingsDialog.hpp"
#include "Search.hpp"
#include "Tab.hpp"
#include "slic3r/plugin/PluginManager.hpp"
@@ -33,6 +34,7 @@
#include <cmath>
#include <cstdlib>
#include <ctime>
#include <exception>
#include <fstream>
#include <iterator>
#include <string>
@@ -142,6 +144,7 @@ constexpr const char* kCommandPrefix = "orca_command";
constexpr const char* kOrcaSourceKey = "orca";
constexpr const char* kOrcaSourceName = "OrcaSlicer";
constexpr const char* kSettingPrefix = "orca_setting";
constexpr const char* kPlateGotoPrefix = "orca_plate_goto";
// Display context for a setting action's eyebrow, e.g. the "Process" in "Process : Quality : Layers".
// Keyed by the option's preset type so the palette reads like the settings sidebar tabs.
@@ -492,6 +495,80 @@ AppActionRunResult run_native_command(const std::string& command_key, const std:
// Auto-orient has no dedicated can_*; can_arrange covers "objects exist + UI worker idle".
if (command_key == "obj_orient")
return obj([](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->orient(); });
// ---- Plate management. Plates are a filament (FFF) feature: SLA builds a single plate with
// no plate UI, and gcode-only mode has no editable project - so gate every plate op on FFF +
// the normal editor (mirroring where the plate toolbar/menu live). These act on the CURRENT
// plate (delete/duplicate take -1) except plate_goto, which jumps to the index in `param`.
auto plate_plater = [&]() -> Plater* {
return (plater && plater->printer_technology() == ptFFF && !plater->only_gcode_mode()) ? plater : nullptr;
};
const AppActionRunResult plate_unavailable{AppActionRunResult::Level::Info, _L("Plates are a filament (FFF) feature.")};
if (command_key == "plate_add") {
if (Plater* p = plate_plater(); p) {
if (!p->can_add_plate())
return {AppActionRunResult::Level::Info, _L("Cannot add another plate (maximum reached).")};
p->add_plate();
return {AppActionRunResult::Level::Success};
}
return plate_unavailable;
}
if (command_key == "plate_duplicate") {
if (Plater* p = plate_plater(); p) {
if (!p->can_add_plate())
return {AppActionRunResult::Level::Info, _L("Cannot duplicate a plate (maximum reached).")};
p->duplicate_plate();
return {AppActionRunResult::Level::Success};
}
return plate_unavailable;
}
if (command_key == "plate_delete") {
if (Plater* p = plate_plater(); p) {
if (!p->can_delete_plate())
return {AppActionRunResult::Level::Info, _L("Cannot delete the only plate.")};
p->delete_plate();
return {AppActionRunResult::Level::Success};
}
return plate_unavailable;
}
if (command_key == "plate_rename") {
if (Plater* p = plate_plater(); p) {
PartPlate* curr = p->get_partplate_list().get_curr_plate();
PlateNameEditDialog dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, _L("Edit Plate Name"));
dlg.set_plate_name(from_u8(curr->get_plate_name()));
if (dlg.ShowModal() == wxID_YES)
curr->set_plate_name(dlg.get_plate_name().ToUTF8().data());
return {AppActionRunResult::Level::Success};
}
return plate_unavailable;
}
if (command_key == "plate_toggle_lock") {
if (Plater* p = plate_plater(); p) {
PartPlateList& plates = p->get_partplate_list();
const int index = plates.get_curr_plate_index();
p->take_snapshot("lock partplate");
plates.lock_plate(index, !plates.is_locked(index));
return {AppActionRunResult::Level::Success};
}
return plate_unavailable;
}
if (command_key == "plate_goto") {
if (Plater* p = plate_plater(); p) {
PartPlateList& plates = p->get_partplate_list();
const int count = plates.get_plate_count();
if (count <= 0)
return {AppActionRunResult::Level::Info, _L("No plates available.")};
int index = 0;
try {
index = std::stoi(param);
} catch (const std::exception&) {}
index = std::clamp(index, 0, count - 1);
p->select_plate(index, false);
return {AppActionRunResult::Level::Success};
}
return plate_unavailable;
}
return {AppActionRunResult::Level::Info, _L("Unknown command.")};
}
@@ -515,6 +592,29 @@ struct CommandAction : AppAction
std::unique_ptr<AppAction> make_command(std::string key, std::string title, std::string group, std::string input = "")
{ return std::make_unique<CommandAction>(std::move(key), std::move(title), std::move(group), std::move(input)); }
// A dynamic "Go to Plate N" action, one per live plate, rebuilt on every snapshot() (so a
// rename/move immediately shows up). id is keyed by plate index, NOT the display title, so
// renaming a plate never re-keys it - the same contract as SettingAction. A pinned "Go to
// Plate N" whose plate is deleted simply stops resolving (visibleFavourites drops dead pins).
struct PlateAction : AppAction
{
int plate_index;
static std::string id_for(int index)
{ return AppAction::compose_id(kPlateGotoPrefix, std::to_string(index), kOrcaSourceKey); }
PlateAction(int index, std::string title, std::string source_name)
: AppAction(AppActionId{id_for(index)}, std::move(title), kOrcaSourceKey, std::move(source_name))
, plate_index(index)
{
this->kind = AppActionKind::Command;
this->group = _u8L("Plate");
}
AppActionRunResult run(const std::string& /*param*/) const override
{ return run_native_command("plate_goto", std::to_string(plate_index)); }
};
// The built-in palette commands, registered once at init().
std::vector<std::unique_ptr<AppAction>> native_commands()
{
@@ -582,6 +682,14 @@ std::vector<std::unique_ptr<AppAction>> native_commands()
out.push_back(make_command("obj_instances_down", _u8L("Decrease Instances"), _u8L("Object")));
out.push_back(make_command("obj_arrange", _u8L("Auto-Arrange"), _u8L("Object")));
out.push_back(make_command("obj_orient", _u8L("Auto-Orient"), _u8L("Object")));
// Plate management. These act on the CURRENT plate (like Plater::delete_plate(-1)); the
// per-plate "Go to Plate N" actions are dynamic and materialised in materialize_plate_actions().
out.push_back(make_command("plate_add", _u8L("Add Plate"), _u8L("Plate")));
out.push_back(make_command("plate_duplicate", _u8L("Duplicate Plate"), _u8L("Plate")));
out.push_back(make_command("plate_delete", _u8L("Delete Plate"), _u8L("Plate")));
out.push_back(make_command("plate_rename", _u8L("Rename Plate"), _u8L("Plate")));
out.push_back(make_command("plate_toggle_lock", _u8L("Toggle Plate Lock"), _u8L("Plate")));
return out;
}
@@ -1195,6 +1303,65 @@ void ActionRegistry::materialize_setting_actions()
}
}
void ActionRegistry::materialize_plate_actions()
{
assert(wxThread::IsMain());
// Plates are a filament (FFF) feature: SLA has a single plate and no plate UI, and gcode-only
// mode has no editable project - so no "Go to Plate N" actions are offered there.
Plater* plater = wxTheApp ? wxGetApp().plater() : nullptr;
if (!plater || plater->printer_technology() != ptFFF || plater->only_gcode_mode()) {
// Drop any stale plate actions (e.g. the printer technology switched to SLA).
for (auto it = m_actions.begin(); it != m_actions.end();) {
if (it->first.rfind(kPlateGotoPrefix, 0) == 0)
it = m_actions.erase(it);
else
++it;
}
return;
}
// Persisted per-action state, read ONCE (mirrors materialize_setting_actions) so a relisted
// "Go to Plate N" keeps its recency/favourite when the plate is renamed - the id is index-keyed.
nlohmann::json stats = read_section("stats", nlohmann::json::object());
if (!stats.is_object())
stats = nlohmann::json::object();
const std::vector<std::string> favs = favourite_ids();
const std::vector<PartPlate*>& list = plater->get_partplate_list().get_plate_list();
std::unordered_set<std::string> seen;
for (size_t i = 0; i < list.size(); ++i) {
PartPlate* plate = list[i];
if (!plate)
continue;
const std::string id = PlateAction::id_for(int(i));
seen.insert(id);
// "Go to Plate N" + " (name)" when the plate is named, matching the object-list label.
std::string title(_u8L("Go to Plate"));
title += " " + std::to_string(i + 1);
const std::string name = plate->get_plate_name();
if (!name.empty())
title += " (" + name + ")";
auto action = std::make_unique<PlateAction>(int(i), title, kOrcaSourceName);
action->favourite = std::find(favs.begin(), favs.end(), id) != favs.end();
if (auto it = stats.find(id); it != stats.end() && it->is_object()) {
action->count = it->value("count", 0);
action->last = it->value("last", 0LL);
}
m_actions.insert_or_assign(action->id(), std::shared_ptr<AppAction>(std::move(action)));
}
// Drop plate actions whose index no longer exists (a plate was deleted / moved to the front).
for (auto it = m_actions.begin(); it != m_actions.end();) {
if (it->first.rfind(kPlateGotoPrefix, 0) == 0 && !seen.count(it->first))
it = m_actions.erase(it);
else
++it;
}
}
bool ActionRegistry::should_ask(const std::string& id) const
{
assert(wxThread::IsMain());
@@ -1216,9 +1383,11 @@ void ActionRegistry::suppress_ask(const std::string& id)
nlohmann::json ActionRegistry::snapshot()
{
assert(wxThread::IsMain());
// Settings are first-class actions; make sure the current visible option set is materialised
// before we serialise the pool (tabs_list is built by the time the palette opens).
// Settings and plates are first-class actions; make sure the current visible option set and the
// live plate list are materialised before we serialise the pool (tabs_list is built by the time
// the palette opens).
materialize_setting_actions();
materialize_plate_actions();
std::vector<const AppAction*> sorted;
sorted.reserve(m_actions.size());
+5
View File
@@ -192,6 +192,11 @@ private:
// Called at the top of snapshot() so the palette always reflects the current configs.
void materialize_setting_actions();
// (Re)materialise one "Go to Plate N" action per live plate, so the palette lists every plate
// directly on each spawn (no second-phase picker). FFF-editor only; SLA/gcode modes have no
// plate UI, so nothing is materialised and stale ids are dropped. Called at the top of snapshot().
void materialize_plate_actions();
// Loader callbacks (marshalled to the UI thread) land here. refresh_source rebuilds
// one plugin's whole action set; refresh_capability touches a single capability.
void refresh_source(const std::string& plugin_key, ActionChange change);
+20 -11
View File
@@ -12610,17 +12610,8 @@ void Plater::priv::on_action_add(SimpleEvent&)
//BBS: add plate from toolbar
void Plater::priv::on_action_add_plate(SimpleEvent&)
{
if (q != nullptr) {
take_snapshot("add partplate");
this->partplate_list.create_plate();
int new_plate = this->partplate_list.get_plate_count() - 1;
this->partplate_list.select_plate(new_plate);
update();
// BBS set default view
//q->get_camera().select_view("topfront");
q->get_camera().requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE;
}
if (q != nullptr)
q->add_plate();
}
//BBS: remove plate from toolbar
@@ -21492,6 +21483,24 @@ int Plater::select_plate_by_hover_id(int hover_id, bool right_click, bool isModi
return ret;
}
//BBS: add an empty plate and switch to it (mirrors the toolbar's Add Plate).
int Plater::add_plate()
{
if (!p->can_add_plate())
return -1;
take_snapshot("add partplate");
int new_plate = p->partplate_list.create_plate();
if (new_plate < 0)
return new_plate;
p->partplate_list.select_plate(new_plate);
update();
// BBS set default view
//get_camera().select_view("topfront");
p->camera.requires_zoom_to_plate = REQUIRES_ZOOM_TO_ALL_PLATE;
return new_plate;
}
int Plater::duplicate_plate(int plate_index)
{
int index = plate_index, ret;
+3
View File
@@ -775,6 +775,9 @@ public:
void apply_background_progress();
//BBS: select the plate by hover_id
int select_plate_by_hover_id(int hover_id, bool right_click = false, bool isModidyPlateName = false);
//BBS: add an empty plate and switch to it (the toolbar's Add Plate). Returns the new
// plate index, or -1 when the plate cap is reached.
int add_plate();
//BBS: delete the plate, index= -1 means the current plate
int delete_plate(int plate_index = -1);
int duplicate_plate(int plate_index = -1);
+8
View File
@@ -53,3 +53,11 @@ TEST_CASE("ActionRegistry takes exclusive ownership of published actions", "[spe
STATIC_CHECK(std::is_same_v<decltype(&ActionRegistry::upsert), ExpectedUpsert>);
}
// A dynamic "Go to Plate N" action is keyed by plate index (not the display title), so renaming
// a plate never re-keys it - the same contract as a setting action.
TEST_CASE("Go-to-plate actions are keyed by index, not title", "[speeddial][actions]")
{
CHECK(AppAction::compose_id("orca_plate_goto", "0", "orca") == "orca_plate_goto:0:orca");
CHECK(AppAction::compose_id("orca_plate_goto", "2", "orca") == "orca_plate_goto:2:orca");
}