Merge branch 'feat/printer-agent-infra' into feat/printer-agent-impl

This commit is contained in:
Ian Chua
2026-09-21 16:02:11 +08:00
3218 changed files with 298916 additions and 102441 deletions
+34 -2
View File
@@ -14,8 +14,6 @@ set(SLIC3R_GUI_SOURCES
GUI/2DBed.hpp
GUI/3DBed.cpp
GUI/3DBed.hpp
GUI/Widgets/StaticGroup.cpp
GUI/Widgets/StaticGroup.hpp
GUI/3DScene.cpp
GUI/3DScene.hpp
GUI/Widgets/FilamentLoad.cpp
@@ -129,6 +127,8 @@ set(SLIC3R_GUI_SOURCES
GUI/SpeedDialDialog.hpp
GUI/ActionRegistry.cpp
GUI/ActionRegistry.hpp
GUI/NativeCommands.cpp
GUI/NativeCommands.hpp
GUI/PluginsConfigDialog.cpp
GUI/PluginsConfigDialog.hpp
GUI/ProcessRunner.cpp
@@ -472,6 +472,8 @@ set(SLIC3R_GUI_SOURCES
GUI/SavePresetDialog.hpp
GUI/SceneRaycaster.cpp
GUI/SceneRaycaster.hpp
GUI/SceneCache.cpp
GUI/SceneCache.hpp
GUI/PartSkipCommon.hpp
GUI/PartSkipDialog.cpp
GUI/PartSkipDialog.hpp
@@ -479,6 +481,8 @@ set(SLIC3R_GUI_SOURCES
GUI/SkipPartCanvas.hpp
GUI/Search.cpp
GUI/Search.hpp
GUI/SettingsIndex.cpp
GUI/SettingsIndex.hpp
GUI/Selection.cpp
GUI/Selection.hpp
GUI/SelectMachine.cpp
@@ -687,6 +691,8 @@ set(SLIC3R_GUI_SOURCES
Utils/Bonjour.hpp
Utils/MeshInspect.cpp
Utils/MeshInspect.hpp
Utils/PaintCLI.cpp
Utils/PaintCLI.hpp
Utils/CalibUtils.cpp
Utils/CalibUtils.hpp
Utils/ColorSpaceConvert.cpp
@@ -780,6 +786,8 @@ set(SLIC3R_GUI_SOURCES
Utils/SimplyPrint.hpp
Utils/TCPConsole.cpp
Utils/TCPConsole.hpp
Utils/UltiMaker.cpp
Utils/UltiMaker.hpp
Utils/UndoRedo.cpp
Utils/UndoRedo.hpp
Utils/WebSocketClient.hpp
@@ -796,6 +804,30 @@ set(SLIC3R_GUI_SOURCES
Utils/wxInspectorPlugins/Registration.hpp
)
# Design/CAD tab: parametric sketch UI, its gizmos, and the MCP control socket.
# All of it sits behind SLIC3R_CAD and links the CAD kernel in libslic3r.
if (SLIC3R_CAD)
list(APPEND SLIC3R_GUI_SOURCES
GUI/CAD/DesignPanel.cpp
GUI/CAD/DesignPanel.hpp
GUI/CAD/DesignCanvas.cpp
GUI/CAD/DesignCanvas.hpp
GUI/CAD/DesignSketchTool.cpp
GUI/CAD/DesignSketchTool.hpp
GUI/CAD/DesignOffer.hpp
GUI/CAD/DesignInteraction.hpp
GUI/CAD/SketchInlineEditor.cpp
GUI/CAD/SketchInlineEditor.hpp
GUI/CAD/McpControl.cpp
GUI/CAD/McpControl.hpp
GUI/Gizmos/GLGizmoSketch.cpp
GUI/Gizmos/GLGizmoSketch.hpp
# Needs GeometryEngine (make_primitive / apply_fillet / tessellate).
GUI/Gizmos/GLGizmoPrimitive.cpp
GUI/Gizmos/GLGizmoPrimitive.hpp
)
endif ()
add_subdirectory(GUI/DeviceCore)
add_subdirectory(GUI/DeviceTab)
+1
View File
@@ -134,6 +134,7 @@ public:
void set_position(Vec2d& position);
void set_axes_mode(bool origin);
void set_axes_origin(const Vec3d& origin) { m_axes.set_origin(origin); } // Design tab: triad at bed centre
const Vec2d& get_position() const { return m_position; }
// Build volume geometry for various collision detection tasks.
+12 -4
View File
@@ -507,10 +507,10 @@ void GLVolume::render_with_outline(const GUI::Size& cnv_size)
glsafe(::glClearStencil(0));
glsafe(::glClear(GL_STENCIL_BUFFER_BIT));
glsafe(::glStencilFunc(GL_ALWAYS, 0xFF, 0xFF));
if (tverts_range == std::make_pair<size_t, size_t>(0, -1))
model.render(shader);
else
model.render(this->tverts_range, shader);
// This pass paints the visible surface, so it must go through simple_render() to keep
// per-triangle MMU paint colors; the later is_outline passes only draw the flat silhouette
// highlight and are fine using the single-color model.
simple_render(shader, model_objects, colors);
glsafe(::glStencilFunc(GL_NOTEQUAL, 0xFF, 0xFF));
glsafe(::glStencilMask(0x00));
shader->set_uniform("is_outline", true);
@@ -670,6 +670,8 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj
} while (0);
if (color_volume && !picking) {
const bool brighten_selected = selected && !disabled && !force_native_color && !force_neutral_color;
// when force_transparent, we need to keep the alpha
if (force_native_color && render_color.is_transparent()) {
for (auto &extruder_color : extruder_colors)
@@ -691,6 +693,8 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj
int color_idx = std::clamp(extruder_id - 1, 0, int(extruder_colors.size()) - 1);
//to make black not too hard too see
ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[color_idx]);
if (brighten_selected)
new_color = brighten_color(new_color, 1.25f);
if (ban_light) {
new_color[3] = (255 - color_idx)/255.0f;
}
@@ -702,6 +706,8 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj
if (idx <= extruder_colors.size()) {
//to make black not too hard too see
ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[idx - 1]);
if (brighten_selected)
new_color = brighten_color(new_color, 1.25f);
if (ban_light) {
new_color[3] = (255 - (idx - 1))/255.0f;
}
@@ -711,6 +717,8 @@ void GLVolume::simple_render(GLShaderProgram* shader, ModelObjectPtrs& model_obj
else {
//to make black not too hard too see
ColorRGBA new_color = adjust_color_for_rendering(extruder_colors[0]);
if (brighten_selected)
new_color = brighten_color(new_color, 1.25f);
if (ban_light) {
new_color[3] = (255 - 0) / 255.0f;
}
+2 -2
View File
@@ -1197,11 +1197,11 @@ void AMSDryCtrWin::update_normal_description(DevAms* dev_ams)
for (const auto& lim : ams_limits) {
if (dev_ams->GetAmsType() == lim.type) {
if (temp_val > lim.max_temp) {
wxString msg = wxString(lim.name) + _L(" maximum drying temperature is ") + wxString::Format(wxT("%d"), lim.max_temp) + wxString::FromUTF8("°C.");
wxString msg = wxString::Format(_L("%s maximum drying temperature is %d°C."), wxString(lim.name), lim.max_temp);
warning_text += msg + "\n";
can_enable_button = false;
} else if (temp_val < lim.min_temp) {
wxString msg = wxString(lim.name) + _L(" minimum drying temperature is ") + wxString::Format(wxT("%d"), lim.min_temp) + wxString::FromUTF8("°C.");
wxString msg = wxString::Format(_L("%s minimum drying temperature is %d°C."), wxString(lim.name), lim.min_temp);
warning_text += msg + "\n";
can_enable_button = false;
}
+523 -60
View File
@@ -3,20 +3,48 @@
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "I18N.hpp"
#include "MainFrame.hpp"
#include "NativeCommands.hpp"
#include "Notebook.hpp"
#include "OptionsGroup.hpp"
#include "Plater.hpp"
#include "SettingsIndex.hpp"
#include "Tab.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include <libslic3r/AppConfig.hpp>
#include <libslic3r/Config.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <wx/thread.h>
#include <boost/filesystem.hpp>
#include <boost/nowide/convert.hpp>
#include <algorithm>
#include <cmath>
#include <ctime>
#include <exception>
#include <iterator>
#include <string>
#include <unordered_map>
#include <unordered_set>
namespace Slic3r { namespace GUI {
std::vector<std::string> cap_favourites(const std::vector<std::string>& ids, size_t limit)
{
std::vector<std::string> out;
out.reserve(std::min(ids.size(), limit));
for (const auto& id : ids) {
if (out.size() >= limit)
break;
if (std::find(out.begin(), out.end(), id) == out.end())
out.push_back(id);
}
return out;
}
namespace {
constexpr const char* kConfigSection = "speed_dial";
@@ -28,14 +56,9 @@ nlohmann::json parse_config_json(const std::string& value, nlohmann::json fallba
}
nlohmann::json read_section(const char* key, nlohmann::json fallback)
{
return parse_config_json(wxGetApp().app_config->get(kConfigSection, key), std::move(fallback));
}
{ return parse_config_json(wxGetApp().app_config->get(kConfigSection, key), std::move(fallback)); }
void write_section(const char* key, const nlohmann::json& j)
{
wxGetApp().app_config->set(kConfigSection, key, j.dump());
}
void write_section(const char* key, const nlohmann::json& j) { wxGetApp().app_config->set(kConfigSection, key, j.dump()); }
std::vector<std::string> read_string_array(const char* key)
{
@@ -53,7 +76,7 @@ double frecency_score(int count, long long last, long long now)
if (count <= 0)
return 0.0;
constexpr double HALF_LIFE_DAYS = 30.0;
double age = std::max(0.0, double(now - last) / 86400.0);
double age = std::max(0.0, double(now - last) / 86400.0);
return count * std::pow(2.0, -age / HALF_LIFE_DAYS);
}
@@ -79,26 +102,25 @@ struct PluginScriptAction : AppAction
// The id an action for (plugin_key, capability) would have - lets refresh_capability
// remove a gone capability without materialising the action.
static std::string id_for(const std::string& plugin_key, const std::string& capability)
{
return AppAction::compose_id(kIdPrefix, capability.empty() ? plugin_key : capability, plugin_key);
}
{ return AppAction::compose_id(kIdPrefix, capability.empty() ? plugin_key : capability, plugin_key); }
PluginScriptAction(std::string plugin_key_in, std::string capability_in, std::string source_name)
: AppAction(kIdPrefix,
capability_in.empty() ? plugin_key_in : capability_in, // title
plugin_key_in, // source_key
std::move(source_name)),
plugin_key(std::move(plugin_key_in)), capability(std::move(capability_in))
capability_in.empty() ? plugin_key_in : capability_in, // title
plugin_key_in, // source_key
std::move(source_name))
, plugin_key(std::move(plugin_key_in))
, capability(std::move(capability_in))
{}
AppActionRunResult run() const override
AppActionRunResult run(const std::string& /*param*/) const override
{
std::string error;
const ExecutionResult result = PluginManager::instance().run_script_capability(plugin_key, capability, error);
if (!error.empty())
return {AppActionRunResult::Level::Error, from_u8(error)};
const bool skipped = result.status == PluginResult::Skipped;
const bool skipped = result.status == PluginResult::Skipped;
const wxString fallback = skipped ? _L("Script plugin skipped.") : _L("Script plugin finished.");
return {skipped ? AppActionRunResult::Level::Info : AppActionRunResult::Level::Success,
result.message.empty() ? fallback : from_u8(result.message)};
@@ -107,8 +129,7 @@ struct PluginScriptAction : AppAction
// Builds an action for a capability, or nullptr if it is not a currently-loaded,
// enabled script capability.
std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std::string& capability,
const std::string& source_name)
std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std::string& capability, const std::string& source_name)
{
PluginManager& manager = PluginManager::instance();
if (!manager.is_plugin_loaded(plugin_key))
@@ -119,8 +140,151 @@ std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std:
return std::make_unique<PluginScriptAction>(plugin_key, capability, source_name);
}
// ---- built-in command actions (the speed dial "commands" section) ------
constexpr const char* kSettingPrefix = "orca_setting";
constexpr const char* kPlateGotoPrefix = "orca_plate_goto";
constexpr const char* kRecentProjectPrefix = "orca_recent_project";
// 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.
std::string setting_type_context(Preset::Type type)
{
switch (type) {
case Preset::TYPE_FILAMENT:
case Preset::TYPE_SLA_MATERIAL: return _u8L("Filament");
case Preset::TYPE_PRINTER: return _u8L("Printer");
case Preset::TYPE_PRINT:
case Preset::TYPE_SLA_PRINT:
default: return _u8L("Process");
}
}
// Stable, non-localized mode token for the webview, which maps it to a badge ("Developer" etc.).
const char* mode_key(ConfigOptionMode mode)
{
switch (mode) {
case comAdvanced: return "advanced";
case comExpert: return "expert";
case comDevelop: return "develop";
default: return "simple";
}
}
// A config setting exposed as a first-class action: selecting it jumps the sidebar to the option.
// The id is keyed by opt_key+type (NOT the display label), so renaming/localizing never re-keys
// the action; title/group/source are purely for display + search. run() performs the jump, and
// the generic registry run() bumps stats so a jump shows up in "recents" like any other action.
struct SettingAction : AppAction
{
std::string opt_key;
Preset::Type type;
std::wstring category; // English category, forwarded to jump_to_option (it localizes)
static std::string id_for(const std::string& opt_key, Preset::Type type)
{ return std::string(kSettingPrefix) + ":" + opt_key + ":" + std::to_string(int(type)); }
SettingAction(std::string opt_key_in,
Preset::Type type_in,
std::string title,
std::string group,
std::wstring category_in,
std::string source_name,
ConfigOptionMode mode_in)
: AppAction(AppActionId{id_for(opt_key_in, type_in)}, std::move(title), kOrcaSourceKey, std::move(source_name))
, opt_key(std::move(opt_key_in))
, type(type_in)
, category(std::move(category_in))
{
// A setting is a single-phase command: activating it jumps the sidebar to the option
// (like the sidebar's own settings search), then the dial closes. run() performs the jump.
this->kind = AppActionKind::Command;
this->group = std::move(group);
this->required_mode = mode_in;
}
AppActionRunResult run(const std::string& /*param*/) const override
{
wxGetApp().sidebar().jump_to_option(opt_key, type, category);
return {AppActionRunResult::Level::Success};
}
};
// Seed one action's persisted state (favourite flag + frecency counters) from an already-parsed
// stats blob and capped favourite list. Shared by the dynamic materialisers.
void seed_from(const nlohmann::json& stats, const std::vector<std::string>& favs, const std::string& id, AppAction& a)
{
a.favourite = std::find(favs.begin(), favs.end(), id) != favs.end();
if (auto it = stats.find(id); it != stats.end() && it->is_object()) {
a.count = it->value("count", 0);
a.last = it->value("last", 0LL);
}
}
// Drop actions whose id starts with `prefix` but that were not seen in this pass (a stale materialisation).
void drop_stale(std::unordered_map<std::string, std::shared_ptr<AppAction>>& actions, const char* prefix,
const std::unordered_set<std::string>& seen)
{
for (auto it = actions.begin(); it != actions.end();) {
if (it->first.rfind(prefix, 0) == 0 && !seen.count(it->first))
it = actions.erase(it);
else
++it;
}
}
// 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 NativeCommands::run("plate_goto", std::to_string(plate_index)); }
};
// A dynamic "Open recent project <name>" action, one per recent project file, rebuilt on every
// snapshot() (like PlateAction) so the list always reflects the current recents. The id is keyed
// by the file PATH, NOT the display title - the same contract as SettingAction/PlateAction, so a
// rename of a project (or a reordered recents list) never re-keys the action. A pinned recent whose
// file is deleted simply stops resolving (visibleFavourites drops dead pins). run() loads the
// project through MainFrame::open_recent_project so the existing missing-file handling is reused.
struct RecentProjectAction : AppAction
{
std::string file_path;
static std::string id_for(const std::string& path) { return AppAction::compose_id(kRecentProjectPrefix, path, kOrcaSourceKey); }
RecentProjectAction(std::string path, std::string title, std::string source)
: AppAction(AppActionId{id_for(path)}, std::move(title), kOrcaSourceKey, std::move(source)), file_path(std::move(path))
{
this->kind = AppActionKind::Command;
this->group = _u8L("Recent Projects");
}
AppActionRunResult run(const std::string& /*param*/) const override
{
if (MainFrame* mf = wxGetApp().mainframe; mf)
mf->open_recent_project(size_t(-1), wxString::FromUTF8(file_path));
return {AppActionRunResult::Level::Success};
}
};
} // namespace
ActionRegistry::~ActionRegistry() = default;
void ActionRegistry::init()
{
assert(wxThread::IsMain());
@@ -151,18 +315,12 @@ void ActionRegistry::init()
// Subscribe before enumerating so a concurrent load cannot land between the initial
// snapshot and callback registration. Duplicate notifications are safe: upsert is by
// id and the m_actions scan in refresh_source is idempotent.
manager.subscribe_on_load_callback(
[on_source](const std::string& key) { on_source(key, ActionChange::Added); });
manager.subscribe_on_unload_callback(
[on_source](const std::string& key) { on_source(key, ActionChange::Removed); });
manager.subscribe_on_load_callback([on_source](const std::string& key) { on_source(key, ActionChange::Added); });
manager.subscribe_on_unload_callback([on_source](const std::string& key) { on_source(key, ActionChange::Removed); });
manager.subscribe_on_capability_load_callback(
[on_capability](const PluginCapabilityId& capability) {
on_capability(capability, ActionChange::Added);
});
[on_capability](const PluginCapabilityId& capability) { on_capability(capability, ActionChange::Added); });
manager.subscribe_on_capability_unload_callback(
[on_capability](const PluginCapabilityId& capability) {
on_capability(capability, ActionChange::Removed);
});
[on_capability](const PluginCapabilityId& capability) { on_capability(capability, ActionChange::Removed); });
// enumerate current script capabilities
std::unordered_map<std::string, std::string> source_names;
@@ -173,12 +331,40 @@ void ActionRegistry::init()
for (const auto& capability : manager.get_plugin_capabilities("", PluginCapabilityType::Script)) {
if (!capability)
continue;
const std::string& key = capability->audit_plugin_key();
auto it = source_names.find(key);
const std::string& key = capability->audit_plugin_key();
auto it = source_names.find(key);
const std::string& source_name = it == source_names.end() ? key : it->second;
if (auto action = make_action(key, capability->name(), source_name))
upsert(std::move(action));
}
// Built-in palette commands (Save/Load, Preferences, Mode switch, Slice/Preview, Go to layer).
// Register after plugins so the plugin ids win on any (unlikely) id collision - ids are distinct
// by prefix, so this is order-independent. The catalog (and its thin AppAction adapter) lives in
// NativeCommands; the registry only stores and dispatches the result.
for (const NativeCommand& c : NativeCommands::catalog())
upsert(NativeCommands::make_action(c));
}
void ActionRegistry::relocalize_builtins()
{
assert(wxThread::IsMain());
if (!m_started)
return;
// Drop only the built-in commands; plugins and the dynamically materialised families are
// either unlocalized or rebuilt per snapshot. remove() only erases the map entry, and upsert()
// re-seeds favourites/stats from config, so key-based ids keep their pinned state.
std::vector<std::string> stale;
for (const auto& [id, action] : m_actions)
if (action->source_key() == kOrcaSourceKey && action->kind == AppActionKind::Command)
stale.push_back(id);
for (const std::string& id : stale)
remove(id);
NativeCommands::rebuild_catalog();
for (const NativeCommand& c : NativeCommands::catalog())
upsert(NativeCommands::make_action(c));
}
void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange change)
@@ -198,7 +384,7 @@ void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange
if (change == ActionChange::Removed)
return;
PluginManager& manager = PluginManager::instance();
PluginManager& manager = PluginManager::instance();
const std::string source_name = find_loaded_source_name(manager, plugin_key);
for (const auto& capability : manager.get_plugin_capabilities(plugin_key, PluginCapabilityType::Script)) {
if (!capability)
@@ -208,8 +394,7 @@ void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange
}
}
void ActionRegistry::refresh_capability(const std::string& plugin_key, const std::string& capability,
ActionChange change)
void ActionRegistry::refresh_capability(const std::string& plugin_key, const std::string& capability, ActionChange change)
{
assert(wxThread::IsMain());
@@ -233,7 +418,7 @@ void ActionRegistry::upsert(std::unique_ptr<AppAction> action)
return;
seed_state(*action);
std::string id = action->id();
std::string id = action->id();
std::shared_ptr<AppAction> stored = std::move(action);
m_actions.insert_or_assign(std::move(id), std::move(stored));
}
@@ -246,11 +431,13 @@ void ActionRegistry::remove(const std::string& id)
void ActionRegistry::seed_state(AppAction& a) const
{
auto favs = read_string_array("favourite_actions");
// Favourites carry the quick-launch order, so the persisted list is the source of truth
// (not re-derived from the frecency sort). Cap it so stale configs can't exceed kFavLimit.
auto favs = favourite_ids();
a.favourite = std::find(favs.begin(), favs.end(), a.id()) != favs.end();
nlohmann::json stats = read_section("stats", nlohmann::json::object());
auto it = stats.find(a.id());
auto it = stats.find(a.id());
if (it != stats.end() && it->is_object()) {
a.count = it->value("count", 0);
a.last = it->value("last", 0LL);
@@ -260,6 +447,14 @@ void ActionRegistry::seed_state(AppAction& a) const
}
}
void ActionRegistry::load_persisted(nlohmann::json& stats, std::vector<std::string>& favs) const
{
stats = read_section("stats", nlohmann::json::object());
if (!stats.is_object())
stats = nlohmann::json::object();
favs = favourite_ids();
}
// ---- read surface -----------------------------------------------------------
const AppAction* ActionRegistry::by_id(const std::string& id) const
@@ -269,14 +464,11 @@ const AppAction* ActionRegistry::by_id(const std::string& id) const
return it == m_actions.end() ? nullptr : it->second.get();
}
AppAction* ActionRegistry::find(const std::string& id)
{
return const_cast<AppAction*>(by_id(id));
}
AppAction* ActionRegistry::find(const std::string& id) { return const_cast<AppAction*>(by_id(id)); }
// ---- dispatch + write-through ----------------------------------------------
AppActionRunResult ActionRegistry::run(const std::string& id)
AppActionRunResult ActionRegistry::run(const std::string& id, const std::string& param)
{
assert(wxThread::IsMain());
auto it = m_actions.find(id);
@@ -286,13 +478,13 @@ AppActionRunResult ActionRegistry::run(const std::string& id)
// nested event loop; a queued source refresh can erase the entry while the
// keep-alive preserves the action until run returns.
std::shared_ptr<AppAction> keep = it->second;
AppActionRunResult o = keep->run();
AppActionRunResult o = keep->run(param);
if (o.level == AppActionRunResult::Level::Busy)
return o;
// Bump stats (write-through). Re-read to avoid clobbering a concurrent field.
nlohmann::json stats = read_section("stats", nlohmann::json::object());
if (!stats.is_object()) // corrupt (valid-JSON, non-object) value degrades to empty
if (!stats.is_object()) // corrupt (valid-JSON, non-object) value degrades to empty
stats = nlohmann::json::object();
nlohmann::json& e = stats[id];
if (!e.is_object())
@@ -300,22 +492,37 @@ AppActionRunResult ActionRegistry::run(const std::string& id)
e["count"] = e.value("count", 0) + 1;
e["last"] = (long long) std::time(nullptr);
write_section("stats", stats);
if (AppAction* live = find(id)) { live->count = e["count"]; live->last = e["last"]; }
if (AppAction* live = find(id)) {
live->count = e["count"];
live->last = e["last"];
}
return o;
}
void ActionRegistry::set_favourite(const std::string& id, bool on)
bool ActionRegistry::set_favourite(const std::string& id, bool on)
{
assert(wxThread::IsMain());
auto favs = read_string_array("favourite_actions");
// Start from the capped, deduped list so a persisted config can never be written back larger.
auto favs = favourite_ids();
auto it = std::find(favs.begin(), favs.end(), id);
if (on && it == favs.end())
if (on && it == favs.end()) {
if (favs.size() >= kFavLimit)
return false; // bar is full - the caller surfaces a hint
favs.push_back(id);
}
if (!on && it != favs.end())
favs.erase(it);
write_section("favourite_actions", nlohmann::json(favs));
if (AppAction* live = find(id))
live->favourite = on;
return true;
}
std::vector<std::string> ActionRegistry::favourite_ids() const
{
assert(wxThread::IsMain());
// Enforce the cap + dedupe on read so the persisted order can never grow past kFavLimit, even from an older config.
return cap_favourites(read_string_array("favourite_actions"), kFavLimit);
}
void ActionRegistry::reorder_favourites(const std::vector<std::string>& ids)
@@ -325,16 +532,184 @@ void ActionRegistry::reorder_favourites(const std::vector<std::string>& ids)
std::vector<std::string> next;
// keep the requested order, but only ids that are actually favourites (guard a bad payload)
for (const auto& id : ids)
if (std::find(cur.begin(), cur.end(), id) != cur.end() &&
std::find(next.begin(), next.end(), id) == next.end())
if (std::find(cur.begin(), cur.end(), id) != cur.end() && std::find(next.begin(), next.end(), id) == next.end())
next.push_back(id);
// why: don't drop favourites the page omitted (e.g. pins with no live action hidden from the bar)
for (const auto& id : cur)
if (std::find(next.begin(), next.end(), id) == next.end())
next.push_back(id);
// never write the bar back larger than the quick-launch slots
next = cap_favourites(next, kFavLimit);
write_section("favourite_actions", nlohmann::json(next));
}
void ActionRegistry::materialize_setting_actions()
{
assert(wxThread::IsMain());
// Reuse the Sidebar's live settings index: it's the only catalog whose group/category map is
// populated (Tab::add_key feeds it at build time), and it already mirrors the current
// configs/printer-technology. Use the all-modes view so the Speed Dial lists every setting,
// including those above the user's current mode, and can prompt to switch before jumping.
const std::vector<Search::Option>& options = wxGetApp().sidebar().settings_index().all_options();
// Load the persisted per-action state ONCE (not per-option) so a re-materialised setting keeps
// its recency/favourite; mirroring seed_state but amortised over the whole option set.
nlohmann::json stats;
std::vector<std::string> favs;
load_persisted(stats, favs);
std::unordered_set<std::string> seen;
for (const Search::Option& opt : options) {
// The row's live state drives both the hidden filter and the title (labels can change at
// runtime, e.g. brim_width -> "Brim ear radius"). Hidden rows are skipped, not marked seen.
Tab* tab = wxGetApp().get_tab(opt.type);
Tab::SettingRowState row;
if (tab)
row = tab->setting_row_state(opt.opt_key());
if (!row.visible)
continue;
const std::string id = SettingAction::id_for(opt.opt_key(), opt.type);
seen.insert(id);
// The page draws Line::label; the descriptive ConfigOptionDef name stays a search-only alias
// ("overhang reversal" still finds "Reverse on even").
const std::string search_label = boost::nowide::narrow(opt.label_local.empty() ? opt.label : opt.label_local);
std::string title = into_u8(Search::resolve_setting_title(from_u8(opt.display_label), row.label, row.multi));
if (title.empty())
title = search_label;
// Eyebrow/source = the full settings path "Process : Quality : Layers" (localized). The JS
// renders group || source and searches source + " " + group, so putting the whole path in
// source both displays it and makes it matchable by any segment (e.g. a "quality" query).
std::wstring path = boost::nowide::widen(setting_type_context(opt.type));
if (!opt.category_local.empty())
path += L" : " + opt.category_local;
if (!opt.group_local.empty())
path += L" : " + opt.group_local;
// title = the label the settings row draws; group stays empty so the source path (above) is
// the single display/search breadcrumb rather than being duplicated.
auto action = std::make_unique<SettingAction>(opt.opt_key(), opt.type, title, std::string(), opt.category,
boost::nowide::narrow(path), opt.mode);
if (title != search_label)
action->full_label = search_label;
// Tile pictogram = the icon of the setting's own group header (e.g. Advanced -> param_advanced),
// the one shown next to it in the page. Fall back to the page/category icon for groups
// without one. Keys are the English titles the GUI registers.
action->icon = opt.group_icon;
if (action->icon.empty() && !opt.category.empty() && tab) {
const auto& icons = tab->get_category_icon_map();
auto it = icons.find(wxString(opt.category));
if (it != icons.end())
action->icon = it->second;
}
// Footer description + wiki affordance; only settings whose row declared a wiki path have one.
action->tooltip = opt.tooltip;
if (!opt.wiki_path.empty())
action->help_url = into_u8(OptionsGroup::get_url(opt.wiki_path));
seed_from(stats, favs, id, *action);
auto const action_id = action->id();
auto const app_action = std::shared_ptr<AppAction>(std::move(action));
m_actions.insert_or_assign(action_id, app_action);
}
// Drop SettingActions whose option no longer exists in the current configs (e.g. the printer
// technology / UI mode changed). Non-setting actions are untouched.
drop_stale(m_actions, kSettingPrefix, seen);
}
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).
drop_stale(m_actions, kPlateGotoPrefix, {});
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;
std::vector<std::string> favs;
load_persisted(stats, favs);
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);
seed_from(stats, favs, id, *action);
auto const action_id = action->id();
auto const app_action = std::shared_ptr<AppAction>(std::move(action));
m_actions.insert_or_assign(action_id, app_action);
}
// Drop plate actions whose index no longer exists (a plate was deleted / moved to the front).
drop_stale(m_actions, kPlateGotoPrefix, seen);
}
void ActionRegistry::materialize_recent_project_actions()
{
assert(wxThread::IsMain());
// Persisted per-action state, read ONCE (mirrors materialize_plate_actions) so a relisted recent
// project keeps its recency/favourite when the recents list reorders - the id is path-keyed.
nlohmann::json stats;
std::vector<std::string> favs;
load_persisted(stats, favs);
// app_config stores recents oldest-first; the palette shows newest-first.
std::vector<std::string> recents = wxGetApp().app_config->get_recent_projects();
std::reverse(recents.begin(), recents.end());
std::unordered_set<std::string> seen;
for (const std::string& path : recents) {
// Skip projects whose file is gone; the stale id is dropped below.
boost::system::error_code ec;
if (path.empty() || !boost::filesystem::exists(boost::filesystem::path(path), ec))
continue;
const std::string id = RecentProjectAction::id_for(path);
seen.insert(id);
// Title = file basename; source/eyebrow = the full path so search can match either.
boost::filesystem::path p(path);
std::string title = p.filename().string();
if (title.empty())
title = path;
auto action = std::make_unique<RecentProjectAction>(path, std::move(title), path);
seed_from(stats, favs, id, *action);
auto const action_id = action->id();
auto const app_action = std::shared_ptr<AppAction>(std::move(action));
m_actions.insert_or_assign(action_id, app_action);
}
// Drop recent-project actions whose file no longer exists / was removed from the recents list.
drop_stale(m_actions, kRecentProjectPrefix, seen);
}
bool ActionRegistry::should_ask(const std::string& id) const
{
assert(wxThread::IsMain());
@@ -351,11 +726,31 @@ void ActionRegistry::suppress_ask(const std::string& id)
write_section("ask_suppressed", nlohmann::json(arr));
}
// ---- snapshot ---------------------------------------------------------------
nlohmann::json ActionRegistry::snapshot() const
bool ActionRegistry::tooltip_expanded() const
{
assert(wxThread::IsMain());
const nlohmann::json j = read_section("tooltip_expanded", nlohmann::json(true));
return j.is_boolean() ? j.get<bool>() : true;
}
void ActionRegistry::set_tooltip_expanded(bool expanded)
{
assert(wxThread::IsMain());
write_section("tooltip_expanded", nlohmann::json(expanded));
}
// ---- snapshot ---------------------------------------------------------------
nlohmann::json ActionRegistry::snapshot()
{
assert(wxThread::IsMain());
// 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();
materialize_recent_project_actions();
std::vector<const AppAction*> sorted;
sorted.reserve(m_actions.size());
for (const auto& entry : m_actions)
@@ -374,17 +769,85 @@ nlohmann::json ActionRegistry::snapshot() const
return a->id() < b->id();
});
auto action_to_json = [](const AppAction* a) {
return nlohmann::json({{"id", a->id()},
{"title", a->title()},
{"full_label", a->full_label},
{"source", a->source_name()},
{"group", a->group},
{"kind", a->kind == AppActionKind::Plugin ? "plugin" : "command"},
{"input", a->input},
{"icon", a->icon},
{"mode", mode_key(a->required_mode)},
{"desc", a->tooltip},
{"wiki", !a->help_url.empty()}});
};
nlohmann::json actions = nlohmann::json::array();
for (const AppAction* a : sorted)
actions.push_back({{"id", a->id()},
{"title", a->title()},
{"source", a->source_name()},
{"shortcut", ""}});
actions.push_back(action_to_json(a));
// why: favourites is the ORDERED pin list - it must come from favourite_actions
// as stored, not be re-derived from the frecency-sorted actions (that would
// reorder the favourites bar). The page (js) filters out ids with no live action itself.
nlohmann::json favourites(read_string_array("favourite_actions"));
return {{"actions", std::move(actions)}, {"favourites", std::move(favourites)}};
// reorder the favourites bar). Drop pins with no live action (an option hidden by the
// current mode, an unloaded plugin, a gone plate/project) and persist the pruned list, so
// invisible pins can't silently fill the quick-launch cap. Order is preserved.
std::vector<std::string> favs = favourite_ids();
std::vector<std::string> live_favs;
live_favs.reserve(favs.size());
for (const auto& id : favs)
if (m_actions.count(id))
live_favs.push_back(id);
if (live_favs.size() != favs.size())
write_section("favourite_actions", nlohmann::json(live_favs));
nlohmann::json favourites(live_favs);
// Recent = the last-N launched actions by recency (only actions with a run history). N is a
// user preference; 0 hides recents without affecting the frecency order below.
const size_t recent_limit = size_t(wxGetApp().app_config->get_speed_dial_recent_count());
std::vector<const AppAction*> recent;
for (const auto& entry : m_actions)
if (entry.second->last > 0)
recent.push_back(entry.second.get());
std::sort(recent.begin(), recent.end(), [](const AppAction* a, const AppAction* b) {
if (a->last != b->last)
return a->last > b->last;
return a->id() < b->id();
});
if (recent.size() > recent_limit)
recent.resize(recent_limit);
nlohmann::json recent_json = nlohmann::json::array();
for (const AppAction* a : recent)
recent_json.push_back(action_to_json(a));
return {{"actions", std::move(actions)},
{"favourites", std::move(favourites)},
{"recent", std::move(recent_json)},
{"user_mode", mode_key(wxGetApp().get_mode())},
{"tooltip_expanded", tooltip_expanded()}};
}
// ---- tab options (enumerate the MainFrame notebook's current pages) ----------
nlohmann::json ActionRegistry::tab_options() const
{
assert(wxThread::IsMain());
nlohmann::json out = nlohmann::json::array();
if (!wxTheApp || wxGetApp().is_closing())
return out;
MainFrame* mf = wxGetApp().mainframe;
if (!mf || !mf->m_tabpanel)
return out;
Notebook* notebook = mf->m_tabpanel;
for (size_t i = 0; i < notebook->GetPageCount(); ++i) {
const wxString id = notebook->GetPageName(i);
if (id.empty())
continue;
out.push_back({{"id", id.ToStdString()},
{"title", notebook->GetPageLabel(i).ToStdString()},
{"icon", notebook->GetPageIcon(i)}});
}
return out;
}
}} // namespace Slic3r::GUI
+137 -25
View File
@@ -2,10 +2,13 @@
#include <nlohmann/json.hpp>
#include <libslic3r/Config.hpp>
#include <wx/string.h>
#include <wx/thread.h>
#include <cassert>
#include <cstddef>
#include <memory>
#include <string>
#include <string_view>
@@ -18,14 +21,25 @@ namespace Slic3r { namespace GUI {
// How a source's action set changed. Drives the registry's refresh handlers.
enum class ActionChange { Added, Removed };
// What kind of runnable thing an action is. Drives the run-confirm gate (plugins ask, commands don't).
enum class AppActionKind { Plugin, Command };
// Result of running an AppAction, in the action layer's own vocabulary. Concrete
// actions translate their runner-specific result into this generic shape.
struct AppActionRunResult
{
enum class Level { Success, Info, Error, Busy };
Level level = Level::Info;
wxString message; // empty = "nothing worth showing"
Level level = Level::Info;
wxString message; // empty = "nothing worth showing"
};
// Tag carrying a precomputed action id, used by the explicit-id ctor below. It exists so the
// id ctor and the compose-from-prefix ctor are NOT both reachable from a `const char*` first
// argument (which would make calls like AppAction("orca_command", ...) ambiguous).
struct AppActionId
{
std::string id;
};
// A speed-dial action: identity + user-state seeded from config + how to run itself.
@@ -52,12 +66,35 @@ struct AppAction
}
// seeded from AppConfig for the snapshot / sort:
bool favourite = false;
int count = 0;
long long last = 0; // epoch seconds
bool favourite = false;
int count = 0;
long long last = 0; // epoch seconds
// Speed Dial presentation: Plugin keeps group empty (the UI falls back to the
// source name); Command sets a section label (e.g. "Commands", "Mode").
AppActionKind kind = AppActionKind::Plugin;
std::string group;
// Second-phase input descriptor for the palette: "percent" (jump to layer by a 0-100
// value) or "tab" (pick a notebook tab). Empty = run immediately on activation.
std::string input;
// Tile pictogram: SVG base name under resources/images; empty renders a blank tile (commands
// without a GUI icon, plugins). Set from NativeCommands / the setting's category icon.
std::string icon;
// Settings mode required to edit this action (SettingActions only). The palette prompts before
// running an action whose mode is above the user's current mode. comSimple for everything else.
ConfigOptionMode required_mode = comSimple;
// Description shown in the Speed Dial's footer strip (SettingActions: the localized tooltip).
std::string tooltip;
// Search-only alias when the title differs from the descriptive ConfigOptionDef name (e.g. title
// "Reverse on even", full_label "Overhang reversal"). Empty when the two agree.
std::string full_label;
// Full wiki URL, when the action has one (SettingActions whose row declared a label_path).
std::string help_url;
virtual ~AppAction() = default;
virtual AppActionRunResult run() const = 0; // re-resolves + runs (UI thread)
// Re-resolves + runs (UI thread). `param` carries an optional per-run argument for
// commands (e.g. a layer percentage); plugins ignore it.
virtual AppActionRunResult run(const std::string& param = {}) const = 0;
protected:
// The definition is constructor-set and immutable. Refreshes replace an action
@@ -65,10 +102,17 @@ protected:
// why: source_key (not the display name) carries identity, so renaming the source's
// display name leaves the id - and its persisted stats/favourite - intact.
AppAction(std::string_view prefix, std::string title, std::string source_key, std::string source_name)
: m_id(compose_id(prefix, title, source_key)),
m_title(std::move(title)),
m_source_key(std::move(source_key)),
m_source_name(std::move(source_name)) {}
: m_id(compose_id(prefix, title, source_key))
, m_title(std::move(title))
, m_source_key(std::move(source_key))
, m_source_name(std::move(source_name))
{}
// Explicit-id ctor: for actions whose id must NOT be derived from the display title
// (e.g. a setting action keyed by opt_key+type, so a rename/localization never re-keys it).
AppAction(AppActionId id, std::string title, std::string source_key, std::string source_name)
: m_id(std::move(id.id)), m_title(std::move(title)), m_source_key(std::move(source_key)), m_source_name(std::move(source_name))
{}
private:
std::string m_id; // <prefix>:<title>:<source_key> - stable identity + AppConfig key
@@ -77,26 +121,53 @@ private:
std::string m_source_name; // display name of the action's source
};
// Stable identity/display name of the built-in ("OrcaSlicer") action source. Shared by the native
// command catalog and the dynamically materialised setting/plate/recent actions so every built-in
// action re-keys together.
inline constexpr const char* kOrcaSourceKey = "orca";
inline constexpr const char* kOrcaSourceName = "OrcaSlicer";
// True when a setting at `setting_mode` cannot be edited in `current_mode` and the UI must switch
// first. Developer settings are handled as a separate prompt by the Speed Dial.
inline bool requires_mode_switch(ConfigOptionMode setting_mode, ConfigOptionMode current_mode)
{
return setting_mode > current_mode;
}
// Cap + dedupe a persisted favourite-id list, preserving first-occurrence order. A stale or
// hand-edited config must never grow the quick-launch bar past `limit`, and a duplicated id must collapse to its first pin.
std::vector<std::string> cap_favourites(const std::vector<std::string>& ids, size_t limit);
// Self-contained sink and single owner of runnable actions for the app session.
//
// Workflow:
// 1. init() (once, UI thread) subscribes to the plugin loader and enumerates the
// current script capabilities into actions.
// 1. init() (once, UI thread) subscribes to the plugin loader and enumerates the current script
// capabilities into actions, then materialises the static built-ins from the NativeCommands catalog.
// 2. Loader load/unload callbacks route through refresh_source()/refresh_capability(),
// which upsert()/remove() actions. The registry keeps the only action list and
// restores persisted user state as actions arrive.
// 3. Consumers use by_id(), snapshot(), and run() without knowing the source.
// 3. Dynamic built-in families (settings, plates, recent projects) are re-materialised at the top of
// snapshot(), because their membership follows live state (the current configs, plate list, recents).
// 4. Consumers use by_id(), snapshot(), and run() without knowing the source.
//
// note: there is exactly one source (script plugins), so it lives inline here rather
// than behind a polymorphic source interface.
// note: the static catalog lives in NativeCommands; the registry owns the pool, persistence and
// dispatch, and materialises the dynamic families inline rather than behind a source interface.
class ActionRegistry
{
public:
~ActionRegistry();
// Subscribes to the plugin loader and enumerates its current actions. Call once
// on the UI thread after the plugin system is up; wires the initial list and live
// updates together.
void init();
// Rebuilds the built-in command actions in the current UI locale. The command catalog copies
// translated titles/groups at construction, so after a live language switch the stored titles
// are stale until this runs. Ids are key-based and upsert re-seeds persisted state, so
// favourites/run history survive. UI thread only. No-op before init().
void relocalize_builtins();
// Takes ownership, seeds persisted state, then inserts the action or replaces
// the action with the same id. A null action is ignored.
void upsert(std::unique_ptr<AppAction> action);
@@ -105,31 +176,72 @@ public:
void remove(const std::string& id);
// Always-clean read surface. UI thread only.
const AppAction* by_id(const std::string& id) const;
const AppAction* by_id(const std::string& id) const;
// Hard cap on the favourites bar: the numbered quick-launch slots (Alt/Option+1..9, 0).
static constexpr size_t kFavLimit = 10;
// Dispatch + write-through (registry is the only thing that touches AppConfig).
AppActionRunResult run(const std::string& id); // runs + bumps stats
void set_favourite(const std::string& id, bool on);
void reorder_favourites(const std::vector<std::string>& ids); // persist a new bar order
AppActionRunResult run(const std::string& id, const std::string& param = {}); // runs + bumps stats
// Pin/unpin. Returns false when `on` would exceed kFavLimit (the bar is full) so the
// caller can surface a "favourites are full" hint instead of silently dropping the pin.
bool set_favourite(const std::string& id, bool on);
void reorder_favourites(const std::vector<std::string>& ids); // persist a new bar order
// Ordered pinned list (the source of truth), capped at kFavLimit and deduped, matching the
// visible bar the palette renders.
std::vector<std::string> favourite_ids() const;
// Run-confirm gate, keyed by action id (per-action "don't ask again").
bool should_ask(const std::string& id) const;
void suppress_ask(const std::string& id);
// Flat, frecency-sorted snapshot for the webview: {actions:[...], favourites:[...]}.
nlohmann::json snapshot() const;
// Footer expand/collapse preference. Global (applies to every action) and persisted; absent
// means expanded, so a fresh config picks the richer default with no migration.
bool tooltip_expanded() const;
void set_tooltip_expanded(bool expanded);
// Flat, frecency-sorted snapshot for the webview:
// {actions:[...], favourites:[...], recent:[...]} (recent = last-N launched by recency).
nlohmann::json snapshot();
// "Go to tab..." Speed Dial helper: enumerate the MainFrame notebook's current pages
// as [{id,title,icon},...], using the page's real label (not the compact-blanked button text).
// Live by construction - built-in tabs (Home/Prepare/Preview/Device/Project/Calibration) and
// plugin tabs (plugin.<key>.<name>) are all Notebook pages, so a page appears/disappears with
// the notebook. Plugin tabs hidden in the overflow menu (many plugins) aren't separate pages and
// are not listed. Call on the UI thread; null-safe.
nlohmann::json tab_options() const;
private:
void seed_state(AppAction& a) const; // favourite/stats from config
AppAction* find(const std::string& id);
void seed_state(AppAction& a) const; // favourite/stats from config
AppAction* find(const std::string& id);
// Read the persisted stats blob + capped favourite list once for a materialisation pass.
void load_persisted(nlohmann::json& stats, std::vector<std::string>& favs) const;
// (Re)materialise the current visible config settings as SettingActions from the live
// searcher (respecting printer-tech + user-mode + visibility filtering), removing stale ones.
// 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();
// (Re)materialise one "Open recent project <name>" action per recent project file, so the
// palette lists every recent project and can load it by clicking. Keyed by file path (stable);
// files that no longer exist are skipped and their stale ids dropped. Called at the top of snapshot().
void materialize_recent_project_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);
void refresh_capability(const std::string& plugin_key, const std::string& capability, ActionChange change);
bool m_started = false; // init() runs exactly once; guards double-subscription
std::unordered_map<std::string, std::shared_ptr<AppAction>> m_actions; // UI-thread confined; no lock
bool m_started = false; // init() runs exactly once; guards double-subscription
std::unordered_map<std::string, std::shared_ptr<AppAction>> m_actions; // UI-thread confined; no lock
};
}} // namespace Slic3r::GUI
File diff suppressed because it is too large Load Diff
+436
View File
@@ -0,0 +1,436 @@
#ifndef slic3r_DesignCanvas_hpp_
#define slic3r_DesignCanvas_hpp_
#include <wx/panel.h>
#include <wx/popupwin.h>
#include <functional>
#include <memory>
#include <string>
#include "slic3r/GUI/3DBed.hpp"
#include "slic3r/GUI/Camera.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/CAD/SketchEngine.hpp"
#include "slic3r/GUI/CAD/DesignSketchTool.hpp"
class wxGLCanvas;
class wxFrame;
class wxStaticText;
namespace Slic3r {
class TriangleMesh;
namespace GUI {
class GLCanvas3D;
class SketchInlineEditor;
class DesignCanvas : public wxPanel
{
public:
explicit DesignCanvas(wxWindow* parent);
~DesignCanvas() override;
void set_mesh(const TriangleMesh& mesh);
// Multi-body display: one GLVolume per body, each coloured distinctly (per-body colour).
// `visible` (optional, indexed by body) hides bodies whose flag is false.
void set_bodies(const std::vector<TriangleMesh>& body_meshes,
const std::vector<bool>& visible = {});
void clear_mesh();
void set_preview_mesh(const TriangleMesh& mesh);
void clear_preview();
void fit_view();
void set_view(const std::string& view_name);
void begin_sketch(const SketchPlane& plane, DesignSketchTool::Mode mode);
// Re-open a committed entity sketch for full in-canvas editing (load geometry +
// constraints, re-detect feature groups). Re-commits via finish_sketch().
void edit_sketch(const std::vector<SketchEntity>& entities,
const std::vector<SketchEntityConstraintDef>& constraints,
const SketchPlane& plane);
void set_sketch_tool(DesignSketchTool::Mode mode);
void set_sketch_plane(const SketchPlane& plane); // re-plane the live sketch when a reference plane is clicked in 3D
void set_sketch_construction(bool c);
// Flip the sketch selection between construction and real geometry; returns the
// number of entities changed (0 = nothing selected, caller falls back to the mode).
// Open the in-canvas value field on the sketch selection's defining number.
bool edit_sketch_selection_value();
int toggle_sketch_construction_selection();
// Is the sketch tool on Select (as opposed to a draw/edit tool being armed)? The
// Construction box needs it to tell "convert what I picked" from "arm what I draw next".
bool sketch_is_selecting() const { return m_sketch_tool.mode() == DesignSketchTool::Mode::Select; }
// Text / SVG art into the LIVE sketch, as ordinary editable lines. False = no session.
bool add_sketch_regions(const std::vector<std::vector<std::vector<Vec2d>>>& regions);
void set_sketch_polygon_sides(int n);
void set_sketch_polygon_circumscribed(bool c);
void finish_sketch();
bool is_sketching() const;
void refresh_bed(); // re-sync the bed to the current printer (call on tab activation)
// The Camera is Plater-owned and shared with Prepare/Preview/Assemble; GLCanvas3D has no
// per-canvas camera, so every orbit here would otherwise overwrite what the editor tabs
// show. Exactly one of the two views is live at a time, so entering and leaving are the
// same operation: trade the live camera for the parked one. That also keeps this canvas's
// own view across a tab switch.
void enter_viewport();
void leave_viewport();
void unbind_canvas_event_handlers(); // app close / language switch, from the plater's teardown
void reset_canvas_volumes();
void set_show_bed(bool b); // view option: draw the printer bed + plate grid, or not
// N: look straight down the sketch plane's normal, keeping the current zoom. A sketch drawn
// at an angle is a sketch drawn wrong, and no amount of orbiting by hand lands exactly square.
bool view_normal_to_sketch();
void cancel_sketch();
void set_on_sketch_commit(std::function<void(const SketchProfile&, const SketchPlane&)> cb);
void set_on_sketch_entities_commit(
std::function<void(const std::vector<SketchEntity>&,
const std::vector<SketchEntityConstraintDef>&,
const SketchPlane&)> cb);
// Line tool: pending-segment length entry + live readout (Phase 2).
void set_on_segment_drawn(std::function<void(double, double)> cb);
void set_on_cursor_metrics(std::function<void(double, double, bool)> cb);
void set_on_solve_state(std::function<void(int, bool, bool)> cb); // dof, ok, has_constraints
// Live per-step guidance from the armed sketch tool (mode, step, picks). 1c0c.
void set_on_sketch_step(std::function<void(DesignSketchTool::Mode, int, int)> cb);
void apply_segment_length(double len); // exact length, then commit & repaint
void keep_segment_as_drawn(); // commit as-drawn & repaint
// Sketch selection (Mode::Select).
void set_on_sketch_selection_changed(std::function<void(int)> cb);
void set_on_sketch_face_selected(std::function<void(int)> cb); // closed loop clicked: region index passed
void set_on_display_sketch_selected(std::function<void(int, int, int)> cb); // committed loop clicked: (feature, region, entity)
void set_on_display_sketch_activated(std::function<void(int)> cb); // committed sketch DOUBLE-clicked: edit it
std::vector<SketchEntity> selected_loop_entities() const; // entities of the click-selected loop
std::vector<std::vector<int>> region_entity_indices(const std::vector<SketchEntity>& ents) const;
// Like region_entity_indices, but each region's entry is its OWN entities followed by the
// entities of each of its holes — the same order selected_loop_entities() hands the kernel.
// A per-loop extrude of a region WITH holes stores exactly this, so this is the shape a
// consumed loop must be compared against.
std::vector<std::vector<int>> region_entity_indices_with_holes(const std::vector<SketchEntity>& ents) const;
void clear_loop_pick(); // drop the click-selected loop highlight (e.g. after extrude)
void set_loop_pick(int feature, int region); // adopt a loop pick made before the commit
void set_escalate_on_repick(bool on); // off while a card has armed a face/edge pick
// Solid whole/face/edge selection: point the tool at the bodies + concatenated
// tessellation (with per-triangle face & body ids), and a callback fired on each
// whole->face->edge cycle (level, body index, face id, edge id).
void set_solid_pick(const std::vector<CadBody>* bodies, const TriangleMesh* mesh,
const std::vector<int>* tri_face, const std::vector<int>* tri_body,
const std::vector<bool>* visible = nullptr,
const std::vector<Transform3d>* xform = nullptr);
void set_on_solid_selection_changed(std::function<void(int, int, int, int)> cb);
void set_on_place_on_face(std::function<bool()> cb); // F key: Place on Face
void select_body(int body); // Parts-list -> highlight a whole body by index
// Effective display colour of a body: the per-body override (Color tool) when set,
// otherwise the auto body-index palette. Single source of truth shared with reload().
ColorRGBA body_color(int body) const;
// Move-body gizmo (M5): three world-axis drag arrows on a body; drag fires the move
// callback with the body index + accumulated translation (display-only, host applies it).
// body_radius = bounding-sphere radius of the body in world mm; the gizmo scales with it so
// the rotation rings sit OUTSIDE the solid (Orca's Prepare gizmos do the same).
void begin_move_body(int body, const Vec3d& pivot, const Transform3d& base_xform,
double body_radius);
void clear_move_gizmo();
bool moving_body() const;
void set_on_body_move_changed(std::function<void(int, const Transform3d&)> cb);
// Visual Fillet/Chamfer radius gizmo: when a solid edge is picked, anchor a radius arrow on
// it; drag/edit fire the radius callback. Returns false if no edge is currently picked.
bool begin_fillet_gizmo(const Vec3d& body_centroid, double radius);
void clear_fillet_gizmo();
bool filleting() const;
void set_on_fillet_radius_changed(std::function<void(double)> cb);
// Visual Hole gizmo: the panel feeds the hole plane + position + diameter/depth/through while
// its Hole card is open; drag/edit fire the hole callback (x, y, diameter, depth).
void begin_hole_gizmo(const SketchPlane& plane, double x, double y,
double diameter, double depth, bool through);
void set_hole_face_bounds(bool has, double umin, double umax, double vmin, double vmax);
void clear_hole_gizmo();
bool holing() const;
void set_on_hole_changed(std::function<void(double, double, double, double)> cb);
// Visual Thread gizmo: footprint circle + radius/length arrows + draggable centre.
void begin_thread_gizmo(const SketchPlane& plane, double x, double y,
double radius, double height);
void clear_thread_gizmo();
bool threading() const;
void set_on_thread_changed(std::function<void(double, double, double, double)> cb);
// Visual Shell gizmo: inward thickness arrow at the picked open-face centroid.
void begin_shell_gizmo(const Vec3d& face_centroid, const Vec3d& inward_dir, double thickness);
void clear_shell_gizmo();
bool shelling() const;
void set_on_shell_thickness_changed(std::function<void(double)> cb);
// Visual Revolve angle-arc gizmo: the panel feeds the sketch plane + profile centroid + axis
// (0=plane X, 1=plane Y) + angle + flip while its Revolve card is open; drag/edit fire the
// angle callback.
void begin_revolve_gizmo(const SketchPlane& plane, const Vec2d& centroid,
int axis_sel, double angle, bool flip);
void clear_revolve_gizmo();
bool revolving() const;
void set_on_revolve_angle_changed(std::function<void(double)> cb);
// Visual Draft angle-arc gizmo: the panel feeds the face centroid + face normal + angle while
// its Draft card is open; drag/edit fire the angle callback.
void set_draft_gizmo(const Vec3d& face_centroid, const Vec3d& face_normal, double angle);
void clear_draft_gizmo();
bool drafting() const;
void set_on_draft_angle_changed(std::function<void(double)> cb);
// Visual Cut gizmo: plane-rectangle preview + draggable normal offset arrow while
// the Cut card is open; drag fires the offset callback.
void set_cut_gizmo(const SketchPlane& plane, double offset, const Vec3d& body_center, double half_extent);
void clear_cut_gizmo();
bool cutting() const;
void set_on_cut_offset_changed(std::function<void(double)> cb);
// Visual Pattern gizmo: the panel feeds the (world XY) plane + target body centroid + mode +
// count/dir/spacing/angle while its Pattern card is open; drag/edit fire the value callback.
void begin_pattern_gizmo(const SketchPlane& plane, const Vec3d& body_centroid, bool circular,
int count, int dir, double spacing, double angle);
void clear_pattern_gizmo();
bool patterning() const;
void set_on_pattern_changed(std::function<void(double)> cb);
// Visual Extrude depth-arrow gizmo (C5b): the panel feeds the profile plane + centroid +
// live depths/flags while its Extrude card is open; drag/edit fire the depth callback.
void set_extrude_gizmo(const SketchPlane& plane, const Vec2d& centroid,
double depth, double depth2, bool two_sided, bool flip);
void clear_extrude_gizmo();
void set_on_extrude_depth_changed(std::function<void(double, bool)> cb);
void set_datum_gizmo(const SketchPlane& plane, double usize, double vsize,
const Vec3d& base_origin, const Vec3d& base_normal,
double offset, bool offset_on); // C3 resize handles + offset arrow
void clear_datum_gizmo();
void set_on_datum_size_changed(std::function<void(double, double)> cb);
void set_on_datum_offset_changed(std::function<void(double)> cb);
void set_helix_gizmo(const SketchPlane& plane, double radius, double pitch, double height,
double taper, bool left_handed); // helix curve + 3 drag handles
void clear_helix_gizmo();
void set_on_helix_changed(std::function<void(double, double, double)> cb);
void set_rib_gizmo(const SketchPlane& plane, const Vec2d& p0, const Vec2d& p1, double thickness); // rib slab footprint + 2 thickness handles
void clear_rib_gizmo();
void set_on_rib_thickness_changed(std::function<void(double)> cb);
void set_base_pick(std::vector<SketchPlane> planes, std::vector<int> bases,
std::vector<std::string> labels = {}); // clickable labelled reference planes
void clear_base_pick();
void set_on_datum_base_picked(std::function<void(int)> cb);
void set_on_sketch_exit(std::function<void()> cb); // Esc -> exit the tool
void set_on_sketch_exit_refused(std::function<void()> cb); // Esc declined: sketch has work
void set_on_undo_redo(std::function<void(bool /*redo*/)> cb); // Ctrl+Z / Ctrl+Shift+Z
// Persistently draw committed sketches (un-consumed ones stay visible).
void set_display_sketches(std::vector<DesignSketchTool::DisplaySketch> ds);
void set_highlight_sketches(std::vector<std::pair<int, ColorRGBA>> hl);
void set_datum_planes(std::vector<SketchPlane> planes,
std::vector<Vec2d> sizes = {}); // draw datum/reference planes (u/v extents)
// Mate connectors, drawn as frames so their verse and polarity are visible (wgsc).
void set_mate_connectors(std::vector<DesignSketchTool::MateConnectorGlyph> g);
void set_mate_links(std::vector<std::pair<Vec3d, Vec3d>> l);
void set_body_highlight(bool on); // tint the solid when its feature is tree-selected
// The status line, shown along the BASE OF THE VIEWPORT rather than in the side panel:
// the panel clips it at ~73 characters with no warning (8cc), the viewport's
// bottom margin has the whole window width to spare. Empty text hides it.
void set_status_text(const wxString& text, const wxColour& colour);
// Take the status line down / bring it back when the Design page leaves and re-enters view.
// A popup is a TOP-LEVEL window: hiding the page it belongs to does not hide it. Keeps the
// text, so coming back needs no re-selection.
void show_status_hud(bool on);
void set_operand_bodies(int target_body, int tool_body); // -1,-1 clears
void set_body_translucent(bool on); // render the solid see-through (fillet/chamfer preview)
void set_xray_focus(int body); // >=0: fade+lock out every other body (CoordSys picking)
void set_body_hidden(bool on); // preview-only: hide base bodies, show only the result ghost
void set_on_move_exit(std::function<void()> cb); // right-click finished the move-body gizmo
// Right-click (or its platform equivalent) on the viewport with no tool running: open the
// object-driven offer there. Fires with SCREEN coordinates. Deliberately NOT fired while a
// tool is live — right-click already ends a polyline chain and finishes the move gizmo, and
// taking those over would break two working interactions in order to add a third.
void set_on_context_menu(std::function<void(const wxPoint&)> cb);
void delete_selected_sketch_entities();
bool inline_busy() const; // a sketch value field is open (guard keys)
bool inline_has_focus() const; // the field itself holds keyboard focus
void inline_commit(); // accept the typed value (Enter/Tab)
void inline_cancel(); // discard the typed value (Esc)
// The layered Esc: abandon the points of the gesture in progress, else drop the armed tool
// back to Select, else leave the sketch. Same call GLCanvas3D::on_char makes, exposed so the
// panel can do it when focus is not on the canvas.
void request_sketch_exit();
bool live_sketch_has_work() const; // the live sketch holds entities a cancel would destroy
bool undo_last_sketch_entity(); // Ctrl+Z in a sketch: drop the last entity
bool delete_selected_or_last_sketch_entity(); // Delete in a sketch: selected, else last
void clear_sketch_selection();
// View toggles (keys P / A): origin planes, world axis triad. Each returns the new on/off
// state so the caller can echo it in the status bar.
bool toggle_planes();
bool toggle_axes();
// Section views (non-destructive): the panel owns the named "Section View N" list; the canvas
// just applies/clears one horizontal clip at a time. model_mid_z() is the default cut height.
void set_section_plane(bool on, double z, bool keep_upper = false);
double model_mid_z() const;
// Dimension tool: act on the current sketch selection.
DesignSketchTool::DimType sketch_dimension_kind() const;
double sketch_dimension_current() const;
void apply_sketch_dimension(double v);
// Open the in-canvas value editor at the cursor for a host-driven value (the
// committed-feature Constrain path uses this instead of a docked numeric card).
void open_inline_value(double current, std::function<void(double)> commit,
std::function<void()> cancel = {});
// Dimension tool (Mode::Dimension): click-to-place quotes. The pick-complete
// callback lets the panel pop the value card; set/cancel apply or keep the value.
void set_on_dimension_pick_complete(std::function<void(double)> cb);
DesignSketchTool::DimType pending_dimension_type() const;
void set_sketch_dimension_value(double v);
void cancel_sketch_dimension();
// Constrain mode: load a committed profile for picking + constraint editing.
void begin_constrain(const SketchProfile& prof, const SketchPlane& plane);
// Leave constrain mode and clear any picked-entity highlight from the overlay.
void end_constrain();
bool is_constraining() const;
bool selected_segment(int& a, int& b) const;
void update_constrain_profile(const std::vector<Vec2d>& pts);
// Entity-aware Constrain (Fase 4.2): pick Line entities of a committed sketch.
void begin_constrain_entities(const std::vector<SketchEntity>& ents, const SketchPlane& plane);
bool is_constraining_entities() const;
// Sketch selection, for the offer menu: how many entities are selected and what the first
// one is. Returns 0 when nothing is selected.
int sketch_selection_count() const;
// Esc routing (DesignInteraction.hpp). The panel decides WHICH level one press belongs to;
// these are the levels it can act on inside the canvas. Each returns whether it did anything,
// so the panel can fall through to the next level without asking twice.
bool sketch_abort_gesture(); // CadLevel::Gesture — drop the entity being drawn
bool sketch_disarm_tool(); // CadLevel::Tool — armed sketch tool falls back to Select
bool drawing_in_progress() const;// an entity has clicks down but is not committed
bool has_any_selection() const; // model pick or sketch pick
bool clear_any_selection(); // CadLevel::Idle — drop both; true if anything was dropped
bool sketch_first_selected_type(SketchEntity::Type& out) const;
// Live sketch session (Fase 4.2 live constraint path): the panel reads the in-session
// selection and entities, and commits a planned constraint through the tool's
// append->solve->keep-or-rollback, rather than reaching into mcp_sketch_tool().
const std::vector<int>& sketch_selection() const;
const std::vector<SketchEntity>& sketch_entities() const;
// How many constraints the LIVE session holds. Only a count: the hint line needs to know
// whether any badge is on screen to talk about, nothing more.
int sketch_constraint_count() const;
const std::vector<SketchEntityConstraintDef>& sketch_constraints() const;
bool remove_sketch_constraint(int idx);
void set_on_sketch_constraints_changed(std::function<void()> cb);
bool try_add_sketch_constraints(const std::vector<SketchEntityConstraintDef>& defs);
// In-canvas bbox transform of imported Text/SVG art (replaces the Move/Scale dialog).
void begin_imported_transform(int feat,
const std::vector<std::vector<std::vector<Vec2d>>>& base_regions,
const SketchPlane& plane, const Vec2d& offset,
double scale_x, double scale_y);
void set_on_imported_transform(std::function<void(int, Vec2d, double, double)> cb);
bool selected_constrain_entities(int& e0, int& e1) const;
int selected_constrain_axis() const; // third pick slot (Symmetric axis), -1 if unset
bool pick0_point(Vec2d& out) const; // plane-coords of the slot-0 pick (trim/extend)
void update_constrain_entities(const std::vector<SketchEntity>& ents);
// Constraint manager (C3.4): highlight the entities referenced by a selected
// constraint (yellow tint in Constrain mode); empty clears the highlight.
void set_constraint_highlight(std::vector<int> entities);
// Constraint glyph badges (C3.4b): the feature's constraints, drawn as iconic
// marks near their entities in Constrain mode; empty clears them.
void set_constraint_glyphs(std::vector<SketchEntityConstraintDef> cons);
// Repaint the embedded canvas the right way for the active GL backend:
// hardware GL gets a scheduled wxEVT_PAINT (render() runs inside the paint
// cycle); software GL (llvmpipe etc.) gets a direct render() because a
// scheduled Refresh() is frequently dropped there. Backend cached on first use.
// Public: DesignPanel calls it after a tree edit to force a frame on software GL.
// Scripted (MCP) access to the live sketch. One accessor rather than a passthrough per
// verb: the MCP layer drives the SAME tool the mouse drives, which is the whole point of
// having it — a socket that talked to a private copy would prove nothing about the app.
DesignSketchTool& mcp_sketch_tool() { return m_sketch_tool; }
const DesignSketchTool& mcp_sketch_tool() const { return m_sketch_tool; }
void request_repaint();
// Repaint synchronously, once the pending show/resize has settled. Needed when the
// notebook re-shows the Design page: an invalidation issued while the page is still
// being shown is dropped on hardware GL and no wxEVT_PAINT ever follows, leaving the
// canvas blank until another tab switch forces an expose.
void force_repaint();
// Repaint synchronously, for use while a modal popup (the offer menu) owns the event loop:
// a queued Refresh() is not serviced until the popup closes, so a hover ghost drawn behind it
// would never appear. Mirrors DesignPanel's m_status->Update() flush.
void repaint_now();
private:
void reload(bool keep_view);
void swap_camera(); // enter_viewport / leave_viewport, in the one direction they share
wxGLCanvas* m_canvas_widget{nullptr};
GLCanvas3D* m_canvas{nullptr};
int m_sw_gl{-1}; // -1 unknown, 0 hardware GL, 1 software GL
std::function<void(const wxPoint&)> m_on_context_menu;
bool m_ctx_bound{false}; // bind the RIGHT_UP handler once, however often the cb is set
wxPoint m_ctx_press{0, 0}; // right-press origin: a right-DRAG orbits, it must not offer
long long m_ctx_press_ms{0}; // and a right-HOLD is navigation too, however still it is held
Bed3D m_bed;
// The half of the camera swap above that is NOT on screen: the editor tabs' view while
// Design is up, this canvas's view while it is not. Seeded in the constructor so the first
// entry inherits the view the user was already looking at.
Camera m_parked_camera;
bool m_camera_swapped{false}; // guards a leave without an enter, and the reverse
Model m_model;
bool m_first_frame{true};
bool m_body_selected{false}; // tree selected a body feature → tint the solid
int m_hl_body_target{-1};
int m_hl_body_tool{-1};
bool m_body_translucent{false};// fillet/chamfer preview → render the body see-through
int m_xray_focus{-1}; // >=0: only this body is opaque+clickable (CoordSys picking)
bool m_body_hidden{false}; // preview-only mode → hide base bodies, ghost = the result
std::vector<bool> m_body_visible; // per-body visibility (empty => all visible)
// Live pointer to the document's bodies (stable address: m_doc.bodies), stashed by
// set_solid_pick so reload()/body_color() can read each body's colour override.
const std::vector<CadBody>* m_color_bodies{nullptr};
DesignSketchTool m_sketch_tool;
// Section view: whether a horizontal clip is currently applied (guards Alt+Wheel). The cut
// height and the named-view list live in DesignPanel; the canvas is a dumb applier.
bool m_section_on{false};
std::unique_ptr<SketchInlineEditor> m_inline_editor; // floating in-canvas value editor
// Bottom-right viewport HUD: a borderless float label over the GL canvas showing the
// active tool's current values (fed by the tool's on_readout). Empty text hides it.
// A wxPopupWindow for the SAME reason as the status chip below, and it was a wxFrame until
// the reason was measured rather than assumed: "it appears mid-gesture and the next input is
// the mouse" is false. The chip keeps the last value on screen AFTER the gesture ends, and a
// frame holds the X input focus once it has it — so the next keystroke went to a 119x31
// window that has no use for it. Measured on :10: focus on the chip, `r` produced no
// CHAR_HOOK line at all; one bare canvas click moved focus back and the same key armed the
// tool. That is every sketch shortcut dead after every dimensioned entity.
wxPopupWindow* m_hud{nullptr};
wxStaticText* m_hud_label{nullptr};
std::string m_hud_last;
void set_readout(const std::string& text);
void place_readout_hud(); // anchor + show, using m_hud_last
void show_readout_hud(bool on); // iconise/deactivate: a popup would float on the desktop
// Bottom-LEFT viewport HUD: the selection / tool status line, written by DesignPanel.
// A wxPopupWindow, NOT the wxFrame the readout HUD uses: a frame accepts keyboard focus,
// and this one is on screen permanently and re-raised on every status change, so it stole
// the keyboard from the canvas and killed every sketch shortcut in the tab.
wxPopupWindow* m_status_hud{nullptr};
wxStaticText* m_status_hud_label{nullptr};
wxString m_status_hud_last;
wxColour m_status_hud_colour;
void place_status_hud(); // re-anchors to the canvas corner (also on resize)
void apply_status_label(); // SetLabel + Wrap to the canvas width + Fit, always together
// On the top-level frame, which outlives this canvas — members so they can be unbound.
void on_frame_iconize(wxIconizeEvent& e);
void on_frame_activate(wxActivateEvent& e);
void on_status_hud_reanchor(wxEvent& e); // frame wxEVT_MOVE and canvas wxEVT_SIZE
std::function<void(const SketchProfile&, const SketchPlane&)> m_on_sketch_commit;
std::function<void(const std::vector<SketchEntity>&,
const std::vector<SketchEntityConstraintDef>&,
const SketchPlane&)> m_on_sketch_entities_commit;
};
}} // namespace Slic3r::GUI
#endif // slic3r_DesignCanvas_hpp_
+59
View File
@@ -0,0 +1,59 @@
#ifndef slic3r_GUI_DesignInteraction_hpp_
#define slic3r_GUI_DesignInteraction_hpp_
namespace Slic3r { namespace GUI {
// The Design tab's interaction stack, and the ONE rule Esc obeys.
//
// Esc unwinds exactly one level per press, deepest first, and never more. The enum value IS
// the LIFO depth, so "which level does this press belong to" is a comparison, not a chain of
// special cases scattered over three files — which is what it was, and why two presses in a
// row could reach past a tool and destroy the sketch underneath it.
//
// STRICT INVARIANT (the bug this exists to make unrepresentable): no level of Esc deletes a
// feature, discards a sketch that holds geometry, or rolls history back. Destroying work needs
// a gesture that says so — Delete/Backspace on an explicit selection, the banner's Cancel, or
// Ctrl+Z. An Esc that can destroy is an Esc nobody can press with confidence, and being the
// safe key is the whole point of it.
enum class CadLevel : int {
Idle = 0, // nothing transient is up: Esc clears the selection
Tool = 1, // a feature card / armed sketch tool / constrain session: Esc exits it
Gesture = 2, // an uncommitted delta (entity being drawn, body being dragged): Esc reverts it
Transient = 3, // a value field or a popup menu: Esc closes just that
};
// What the tab is doing, reduced to the four bits the routing actually needs. Kept as a POD of
// answers rather than a pointer to the panel so the rule below is decidable — and checkable —
// without a window, a GL context or an event loop.
struct CadInteractionState {
bool value_field_open{false}; // in-canvas value field, or the panel's value card
bool gesture_active{false}; // in-progress entity points, or a body being moved
bool tool_armed{false}; // feature card open, sketch draw tool armed, constrain session
bool has_selection{false}; // something is picked (model or sketch)
};
// The whole routing rule. Deepest live level wins; Idle is the floor.
constexpr CadLevel cad_escape_level(const CadInteractionState& s)
{
if (s.value_field_open) return CadLevel::Transient;
if (s.gesture_active) return CadLevel::Gesture;
if (s.tool_armed) return CadLevel::Tool;
return CadLevel::Idle;
}
// The ordering is the entire contract, so it is checked where it is defined, at compile time.
static_assert(cad_escape_level({true, true, true, true}) == CadLevel::Transient, "value field is deepest");
static_assert(cad_escape_level({false, true, true, true}) == CadLevel::Gesture, "gesture beats tool");
static_assert(cad_escape_level({false, false, true, true}) == CadLevel::Tool, "tool beats idle");
static_assert(cad_escape_level({false, false, false, true}) == CadLevel::Idle, "selection is idle-level");
static_assert(cad_escape_level({false, false, false, false}) == CadLevel::Idle, "empty is idle");
// Right-click vs. right-hold-orbit. A press that stays put and is let go promptly is a click and
// summons the offer; anything longer or further was navigation, and navigation must never be
// rewarded with a menu over wherever the camera happened to stop.
inline constexpr int kCadRightClickMs = 200; // press->release budget
inline constexpr int kCadRightClickDriftPx = 3; // cursor drift budget, max(|dx|,|dy|)
}} // namespace Slic3r::GUI
#endif // slic3r_GUI_DesignInteraction_hpp_
+189
View File
@@ -0,0 +1,189 @@
// GENERATED FILE — DO NOT EDIT.
// Source: docs/ux/tool_atlas.json Generator: docs/ux/mockups/gen_offer_table.py
//
// The object-driven tool offer (charter 4.1): every verb has ONE row index, that index
// is the same in every selection it appears in, and verbs that do not apply are shown
// disabled in place with their reason rather than removed. Row order was ratified
// 2026-07-31; changing an index is a breaking change to every user's muscle memory.
#ifndef slic3r_GUI_DesignOffer_hpp_
#define slic3r_GUI_DesignOffer_hpp_
#include <cstdint>
namespace Slic3r { namespace GUI {
// What the viewport has selected. Ordered as in tool_atlas.json; the bitmask in
// OfferVerb::accepts indexes these.
enum class OfferSel : int {
None = 0,
FacePlanar = 1,
FaceCyl = 2,
FaceOther = 3,
EdgeStr = 4,
EdgeCirc = 5,
Vertex = 6,
BodySolid = 7,
BodySheet = 8,
Bodies2 = 9,
DatumPlane = 10,
DatumAxis = 11,
CoordSys = 12,
Art = 13,
SkLoop = 14,
SkNone = 15,
SkLine = 16,
SkArc = 17,
SkPoint = 18,
Sk2Ent = 19,
Count = 20
};
inline uint32_t offer_bit(OfferSel s) { return 1u << int(s); }
// One row of the offer. `action` routes to the code that already implements the verb:
// "key:S+E" -> m_keys_feature[SHIFT('E')]
// "key:L" -> m_keys_sketch['L']
// "fly:material#4" -> row 4 of the "material" feature flyout
// "btn:delete" -> a standalone toolbar button
// nullptr -> kernel support exists, no GUI path yet (row shows disabled)
struct OfferVerb {
const char* id;
const char* name; // drawing-office word (L10); translated at use with wxGetTranslation
int row; // 0..7, the ratified index — NEVER reorder
const char* key; // shortcut shown in the row, or nullptr
const char* action;
const char* refusal; // why this row is greyed, in the product's own words
uint32_t accepts; // bitmask over OfferSel
int need_bodies;
int need_sketches;
bool need_sheet;
bool sketch_mode; // belongs to the sketch-mode vocabulary, not the model one
// Second level INSIDE a row, for tools that come in variants: "Rectangle" holds corner,
// centre, oblique and rounded. nullptr = sits directly in the row. Keeps the row's own
// address fixed (L4.1) while the variants hang one level below it, mirroring the toolbar's
// grouping instead of flattening 19 create tools into one wall.
const char* family;
const char* icon; // resources/images name, or nullptr — the offer draws it beside the row
const char* hint; // what the verb does / what to click; shown on hover
};
// Row labels, in ratified order.
static const char* const kOfferRowNames[] = {
"Create",
"Add material",
"Remove",
"Fillet / chamfer / draft",
"Repeat",
"Transform",
"Reference",
"Modify",
};
static const int kOfferRowCount = 8;
static const OfferVerb kOfferVerbs[] = {
{"sketch", "Sketch", 0, "Shift+S", "key:S+S", "Click a face or a reference plane in the viewport, then a sketch tool", 0x00000403u, 0, 0, false, false, nullptr, "design_sketch", "Click a face or a reference plane, then pick a drawing tool"},
{"extrude", "Extrude", 1, "Shift+E", "key:S+E", "Create a sketch, or pick a solid face, first", 0x00004002u, 0, 0, false, false, nullptr, "design_extrude", "Extrude a sketch profile, or push/pull a picked face"},
{"revolve", "Revolve", 1, "Shift+R", "key:S+R", "Create a sketch profile to revolve first", 0x00004000u, 0, 0, false, false, nullptr, "design_revolve", "Revolve a profile about an axis"},
{"sweep", "Sweep", 1, "Shift+W", "key:S+W", "Create a profile sketch to sweep first", 0x00004000u, 0, 2, false, false, nullptr, "design_sweep", "Sweep a profile along a path"},
{"loft", "Loft", 1, "Shift+L", "key:S+L", "Create at least two profile sketches to loft", 0x00004000u, 0, 2, false, false, nullptr, "design_loft", "Loft (skin) between two or more profiles"},
{"thicken", "Thicken", 1, nullptr, "fly:material#4", "Thicken needs a solid body — add or import one first", 0x0000000au, 1, 0, false, false, nullptr, "design_thicken", "Offset a solid face into a thin plate (new body)"},
{"rib", "Rib", 1, nullptr, "fly:material#5", "Rib needs a solid body — add or import one first", 0x00010000u, 1, 0, false, false, nullptr, "design_rib", "Grow a thin wall from an open sketch line, fused to a body"},
{"boolean", "Union", 1, "Shift+B", "btn:bool#0", "Boolean needs two bodies — create or import a second solid", 0x00000200u, 2, 0, false, false, nullptr, "design_boolean", "Fuse the tool body into the target — one solid, no seam"},
{"bool_subtract", "Subtract", 1, nullptr, "btn:bool#1", "Boolean needs two bodies — create or import a second solid", 0x00000200u, 2, 0, false, false, nullptr, "design_boolean", "Cut the tool body out of the target"},
{"bool_intersect", "Intersect", 1, nullptr, "btn:bool#2", "Boolean needs two bodies — create or import a second solid", 0x00000200u, 2, 0, false, false, nullptr, "design_boolean", "Keep only where the two bodies overlap"},
{"surf_extrude", "Surface Extrude", 1, "Shift+G", "key:S+G", "Create a sketch first", 0x00004000u, 0, 0, false, false, nullptr, "design_extrude", "Extrude a sketch into a sheet body (no end caps)"},
{"surf_revolve", "Surface Revolve", 1, nullptr, "fly:surface#1", "Create a sketch profile to revolve first", 0x00004000u, 0, 0, false, false, nullptr, "design_revolve", "Revolve a sketch profile into a sheet body"},
{"surf_loft", "Surface Loft", 1, nullptr, "fly:surface#2", "Create at least two profile sketches to loft", 0x00004000u, 0, 2, false, false, nullptr, "design_loft", "Loft (skin) between 2+ profiles, open (no end caps)"},
{"surf_fill", "Surface Fill", 1, nullptr, "fly:surface#3", "Create a closed sketch first", 0x00004000u, 0, 0, false, false, nullptr, "design_surface", "Fill a sketch boundary with a smooth face"},
{"thicken_surf", "Thicken Surface", 1, nullptr, "fly:surface#5", "target is not a sheet body", 0x00000100u, 0, 0, true, false, nullptr, "design_thicken", "Thicken a sheet body into a solid"},
{"hole", "Hole", 2, "Shift+H", "key:S+H", "Pick a face or a plane to drill into", 0x00000402u, 1, 0, false, false, nullptr, "design_hole", "Drill a hole, centred on a picked face or placed on a plane"},
{"thread", "Thread", 2, "Shift+T", "key:S+T", "Pick a cylindrical surface (bore / outer) or a circular edge for a thread", 0x00000024u, 1, 0, false, false, nullptr, "design_thread", "Thread a cylindrical surface (inner bore / outer) or a circular edge"},
{"shell", "Shell", 2, "Shift+K", "key:S+K", "Shell needs a solid body", 0x00000082u, 1, 0, false, false, nullptr, "design_shell", "Hollow the body to a wall thickness, opening a picked face"},
{"cut", "Cut", 2, "Shift+X", "key:S+X", "Create a solid body to cut first", 0x000004feu, 1, 0, false, false, nullptr, "design_cut", "Trim the body with a plane — drag the offset arrow; keep one half or both"},
{"split", "Split", 2, nullptr, nullptr, "Split needs a solid body", 0x000000feu, 1, 0, false, false, nullptr, nullptr, "Split the body along a picked face into two solids"},
{"fillet", "Fillet", 3, "Shift+F", "btn:dress#0", "Pick an edge to round", 0x000000b2u, 1, 0, false, false, nullptr, "design_filletedge", "Pick an edge, then drag the radius arrow or type it"},
{"chamfer", "Chamfer", 3, nullptr, "btn:dress#1", "Pick an edge to bevel", 0x000000b2u, 1, 0, false, false, nullptr, "design_chamfer", "Pick an edge, then drag the distance arrow or type it"},
{"draft", "Draft", 3, "Shift+D", "key:S+D", "Pick a face to taper", 0x0000000au, 1, 0, false, false, nullptr, "design_draft", "Tilt a picked face by a draft angle"},
{"surf_offset", "Surface Offset", 3, nullptr, "fly:surface#4", "target is not a sheet body", 0x00000100u, 0, 0, true, false, nullptr, "design_offset", "Offset a sheet body's shell by a signed distance"},
{"pattern", "Linear pattern", 4, "Shift+N", "btn:pat#0", "Create a solid body to pattern first", 0x00006082u, 1, 0, false, false, nullptr, "design_array", "Repeat the body along a direction — drag the spacing, set the count"},
{"pattern_circular", "Circular pattern", 4, nullptr, "btn:pat#1", "Create a solid body to pattern first", 0x00006082u, 1, 0, false, false, nullptr, "design_polararray", "Repeat the body around an axis — set the count and sweep"},
{"mirror", "Mirror", 4, "Shift+Z", "key:S+Z", "Mirror needs a body — add or import one first", 0x000004feu, 1, 0, false, false, nullptr, "design_mirror", "Reflect a body about a plane"},
{"pat_curve", "Pattern on Curve", 4, nullptr, nullptr, "Pattern on curve needs a body and a curve", 0x00000090u, 1, 0, false, false, nullptr, nullptr, "Repeat the body along a picked curve"},
{"transform", "Move", 5, "Shift+Y", "key:S+Y", "Transform needs a body — add or import one first", 0x000021feu, 1, 0, false, false, nullptr, "design_move", "Move and/or rotate an existing body"},
{"mate", "Mate", 5, nullptr, "fly:placement#2", "A mate needs two coordinate systems", 0x00001202u, 2, 0, false, false, nullptr, "design_c_coincident", "Assembly: align two CoordSys features (fastened, planar, revolute, slider, cylindrical)"},
{"align", "Align to", 5, nullptr, nullptr, "Align needs a body", 0x00000002u, 1, 0, false, false, nullptr, nullptr, "Align the body to a picked face or plane"},
{"plane", "Plane", 6, "Shift+P", "key:S+P", nullptr, 0x00000453u, 0, 0, false, false, nullptr, "design_plane", "Reference plane (offset / tilt / midplane / tangent / two edges / coincident)"},
{"axis", "Axis", 6, "Shift+A", "key:S+A", nullptr, 0x00000057u, 0, 0, false, false, nullptr, "design_line", "Datum axis (two points, face normal, cylinder centerline, two planes, along edge)"},
{"coordsys_v", "Coord Sys", 6, "Shift+C", "key:S+C", nullptr, 0x00000043u, 0, 0, false, false, nullptr, "design_point", "Datum coordinate system (world point, or face + direction edge)"},
{"helix", "Helix", 6, nullptr, "fly:plane#3", nullptr, 0x00000405u, 0, 0, false, false, nullptr, "design_thread", "Helical curve (spring path) — use as a sweep path for coils / springs / augers"},
{"project", "Project", 6, nullptr, "fly:plane#4", "Project needs a body — add or import one first", 0x00000482u, 1, 0, false, false, nullptr, "design_sketch", "Project body edges onto a plane as sketch entities"},
{"measure", "Measure", 6, nullptr, nullptr, nullptr, 0x000b03feu, 0, 0, false, false, nullptr, nullptr, "Measure between the picked points, edges or faces"},
{"mass_props", "Mass", 6, nullptr, "btn:mass", nullptr, 0x000000feu, 1, 0, false, false, nullptr, "info", "Report the volume and surface area of the selected body"},
{"interference", "Interference", 6, nullptr, nullptr, nullptr, 0x00000200u, 2, 0, false, false, nullptr, nullptr, "Check whether two bodies overlap — reports, changes nothing"},
{"edit_feature", "Edit", 7, nullptr, "btn:edit", nullptr, 0x00007d8eu, 0, 0, false, false, nullptr, "design_edit", "Reopen the selected feature to change what it was made from"},
{"rename", "Rename…", 7, "F2", "btn:rename", "Select a feature, or a body, to rename it", 0x00004080u, 0, 0, false, false, nullptr, nullptr, "Give this feature a name you will recognise in the tree (a body takes its name from the feature that makes it)"},
{"delete_face", "Delete Face", 7, nullptr, "fly:dressup#3", "Delete Face needs a body — add or import one first", 0x0000000eu, 1, 0, false, false, nullptr, "design_delete", "Remove faces from a body and heal the solid"},
{"colour", "Colour", 7, nullptr, "btn:colour", nullptr, 0x000001feu, 1, 0, false, false, nullptr, "color_palette", "Set the selected body's display colour"},
{"delete", "Delete", 7, "Del", "btn:delete", nullptr, 0x000f7c00u, 0, 0, false, false, nullptr, "design_delete", "Delete what is selected"},
{"delete_body", "Delete Body", 7, nullptr, "btn:delete_body", nullptr, 0x000001feu, 1, 0, false, false, nullptr, "design_delete", "Delete this whole body — removes the feature it was made from"},
{"sk_line_t", "Line", 0, "L", "key:L", nullptr, 0x000f8000u, 0, 0, false, true, "Line", "design_line", "Line — click start, then end"},
{"sk_polyline", "Polyline", 0, nullptr, "fly:design_line#1", nullptr, 0x000f8000u, 0, 0, false, true, "Line", "design_polyline", "Click points; click the first point to close the loop, right-click to end it open"},
{"sk_rect", "Corner rectangle", 0, "R", "key:R", nullptr, 0x000f8000u, 0, 0, false, true, "Rectangle", "design_rect", "Rectangle — click two opposite corners"},
{"sk_rect_center", "Centre rectangle", 0, nullptr, "fly:design_rect#1", nullptr, 0x000f8000u, 0, 0, false, true, "Rectangle", "design_crect", "Click center, then a corner"},
{"sk_rect_oblique", "Oblique rectangle", 0, nullptr, "fly:design_rect#2", nullptr, 0x000f8000u, 0, 0, false, true, "Rectangle", "design_rect_oblique", "Click two corners of one edge, then a point for the width"},
{"sk_rect_rounded", "Rounded rectangle", 0, nullptr, "fly:design_rect#3", nullptr, 0x000f8000u, 0, 0, false, true, "Rectangle", "design_rect_rounded", "Click two opposite corners, then a point for the corner radius"},
{"sk_circle", "Centre circle", 0, "C", "key:C", nullptr, 0x000f8000u, 0, 0, false, true, "Circle", "design_circle", "Circle — click center, then radius"},
{"sk_circle_2pt", "2-point circle", 0, nullptr, "fly:design_circle#1", nullptr, 0x000f8000u, 0, 0, false, true, "Circle", "design_circle2pt", "Click two ends of the diameter"},
{"sk_circle_3pt", "3-point circle", 0, nullptr, "fly:design_circle#2", nullptr, 0x000f8000u, 0, 0, false, true, "Circle", "design_circle3pt", "Click three points on the circle"},
{"sk_arc_t", "3-point arc", 0, "A", "key:A", nullptr, 0x000f8000u, 0, 0, false, true, "Arc", "design_arc3pt", "Arc — click start, end, then a point"},
{"sk_arc_tangent", "Tangent arc", 0, nullptr, "fly:design_arc3pt#1", nullptr, 0x000f8000u, 0, 0, false, true, "Arc", "design_tangentarc", "Click start (on the last entity) then end"},
{"sk_arc_center", "Centre-point arc", 0, nullptr, "fly:design_arc3pt#2", nullptr, 0x000f8000u, 0, 0, false, true, "Arc", "design_arc_center", "Click center, then start, then a point for the end angle"},
{"sk_slot", "Slot", 0, "S", "key:S", nullptr, 0x000f8000u, 0, 0, false, true, "Slot", "design_slot", "Slot — two centerline ends, then end radius"},
{"sk_slot_arc", "Arc slot", 0, nullptr, "fly:design_slot#1", nullptr, 0x000f8000u, 0, 0, false, true, "Slot", "design_slot_arc", "Click center, start, end, then a point for the width"},
{"sk_ellipse", "Ellipse", 0, "E", "key:E", nullptr, 0x000f8000u, 0, 0, false, true, "Ellipse", "design_ellipse", "Ellipse — center, major end, minor point"},
{"sk_ellipse_arc", "Elliptical arc", 0, nullptr, "fly:design_ellipse#1", nullptr, 0x000f8000u, 0, 0, false, true, "Ellipse", "design_ellipse_arc", "Click center, major-axis end, minor point, then arc start and end"},
{"sk_spline", "Spline", 0, "B", "key:B", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_bspline", "Spline — click control points"},
{"sk_poly_3", "Triangle", 0, nullptr, "btn:poly#3", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Triangle — click centre, then a vertex"},
{"sk_poly_4", "Square", 0, nullptr, "btn:poly#4", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Square — click centre, then a vertex"},
{"sk_poly_5", "Pentagon", 0, nullptr, "btn:poly#5", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Pentagon — click centre, then a vertex"},
{"sk_polygon", "Hexagon", 0, "G", "btn:poly#6", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Hexagon — click centre, then a vertex"},
{"sk_poly_8", "Octagon", 0, nullptr, "btn:poly#8", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Octagon — click centre, then a vertex"},
{"sk_poly_12", "Dodecagon", 0, nullptr, "btn:poly#12", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Dodecagon — click centre, then a vertex"},
{"sk_poly_inscribed", "Inscribed", 0, nullptr, "btn:polyfit#0", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Measure the polygon to its corners (inscribed)"},
{"sk_poly_circumscribed", "Circumscribed", 0, nullptr, "btn:polyfit#1", nullptr, 0x000f8000u, 0, 0, false, true, "Polygon", "design_polygon", "Measure the polygon to its flats (circumscribed)"},
{"sk_point_t", "Point", 0, "P", "key:P", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_point", "Point — click to place"},
{"sk_text", "Text", 0, nullptr, "btn:text", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_text", "Type text; its outline is added to this sketch as editable lines"},
{"sk_svg", "SVG", 0, nullptr, "btn:svg", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_svg", "Import an SVG outline into this sketch as editable lines"},
{"sk_offset", "Offset", 1, "O", "key:O", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_offset", "Offset — pick an entity, drag the distance"},
{"sk_trim", "Trim", 2, "T", "key:T", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_trim", "Trim — click a segment to trim it"},
{"sk_fillet", "Fillet", 3, "F", "key:F", nullptr, 0x00090000u, 0, 0, false, true, nullptr, "design_filletedge", "Fillet — pick two lines, set the radius"},
{"sk_chamfer", "Chamfer", 3, "H", "key:H", nullptr, 0x00090000u, 0, 0, false, true, nullptr, "design_chamfer", "Chamfer — pick two lines, set the distance"},
{"sk_array", "Linear array", 4, nullptr, "fly:design_array#0", nullptr, 0x000b0000u, 0, 0, false, true, "Array", "design_array", "Pick entities, drag the spacing handle, click the count; click empty to apply"},
{"sk_array_polar", "Polar array", 4, nullptr, "fly:design_array#1", nullptr, 0x000b0000u, 0, 0, false, true, "Array", "design_polararray", "Pick entities, drag the sweep handle, click the count; click empty to apply"},
{"sk_mirror", "Mirror", 4, "M", "key:M", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_mirror", "Mirror — pick axis, then entities"},
{"sk_move", "Move", 5, nullptr, "fly:design_move#0", nullptr, 0x000f0000u, 0, 0, false, true, "Move", "design_move", "Pick entities, then drag the handle or click the distance; click empty to apply"},
{"sk_rotate", "Rotate", 5, nullptr, "fly:design_move#1", nullptr, 0x000f0000u, 0, 0, false, true, "Move", "design_rotate", "Pick entities, then drag around the pivot or click the angle; click empty to apply"},
{"sk_scale", "Scale", 5, nullptr, "fly:design_move#2", nullptr, 0x000f0000u, 0, 0, false, true, "Move", "design_scale", "Pick entities, then drag the handle or click the factor; click empty to apply"},
{"sk_dimension", "Dimension", 6, "D", "key:D", nullptr, 0x000f8000u, 0, 0, false, true, nullptr, "design_dimension", "Dimension — click 2 points or an entity"},
{"sk_constrain", "Constrain", 6, "K", "key:K", nullptr, 0x000f0000u, 0, 0, false, true, nullptr, "design_constrain", "Constrain the selected sketch entities to each other"},
// Same verb, model-mode vocabulary: offered when a SKETCH is selected (bit 14, SkLoop), the
// state a user is in right after finishing one. Without this row the only way in was the
// toolbar icon, and constraints read as absent — see the Onshape-comparison report.
{"constrain", "Constrain sketch", 7, nullptr, "btn:constrain", "Select a sketch to constrain it", 0x00004000u, 0, 1, false, false, nullptr, "design_constrain", "Add dimensions and relations (coincident, tangent, parallel...) to the selected sketch"},
{"sk_construct", "Construction", 6, "Q", "key:Q", nullptr, 0x000b8000u, 0, 0, false, true, nullptr, nullptr, "Toggle construction: geometry that guides but is never built"},
{"sk_extend", "Extend", 7, "X", "key:X", nullptr, 0x000b0000u, 0, 0, false, true, nullptr, "design_extend", "Extend — click a line/arc to extend it"},
{"sk_delete", "Delete", 7, "Del", "btn:sk_delete", nullptr, 0x000f0000u, 0, 0, false, true, nullptr, "design_delete", "Delete the selected sketch entities"},
// Typing the defining number of the element you pointed at. Three rows rather than one so
// each names the quantity in the drawing-office word for THAT element; all three land on
// the same handler, because dimension_kind() already resolves the quantity from the
// selection. Without these, an element's own numbers were reachable only by arming the
// Dimension tool and re-picking geometry that was already selected.
{"sk_length", "Length…", 7, "V", "key:V", nullptr, 0x00010000u, 0, 0, false, true, nullptr, "design_dimension", "Type the length of this line"},
{"sk_radius", "Radius / diameter…", 7, "V", "key:V", nullptr, 0x00020000u, 0, 0, false, true, nullptr, "design_dimension", "Type the radius of this arc, or the diameter of this circle"},
{"sk_angdist", "Angle / distance…", 7, "V", "key:V", nullptr, 0x00080000u, 0, 0, false, true, nullptr, "design_dimension", "Type the angle between two lines, or the distance between the two picks"},
};
static const int kOfferVerbCount = 92;
}} // namespace Slic3r::GUI
#endif // slic3r_GUI_DesignOffer_hpp_
File diff suppressed because it is too large Load Diff
+919
View File
@@ -0,0 +1,919 @@
#ifndef slic3r_DesignPanel_hpp_
#define slic3r_DesignPanel_hpp_
#include <wx/panel.h>
#include <wx/scrolwin.h>
#include <wx/treebase.h> // wxTreeItemId
#include <vector>
#include <memory>
#include <functional>
#include <map>
#include "libslic3r/CAD/CadDocument.hpp"
#include "slic3r/GUI/CAD/DesignInteraction.hpp" // CadLevel: what one Esc press means
class ComboBox; // Orca dropdown (Widgets/ComboBox.hpp) — replaces wxChoice everywhere here
class StaticBox; // Orca rounded card frame (Widgets/StaticBox.hpp)
class wxCheckBox;
class wxCheckListBox;
class wxSpinCtrl;
class wxSpinCtrlDouble;
class wxTreeCtrl;
class wxImageList;
class wxStaticText;
class wxStaticLine;
class Button; // Orca-styled button (Widgets/Button.hpp)
class CheckBox; // Orca teal checkbox (Widgets/CheckBox.hpp)
class wxSizer;
// wxBoxSizer, wxTextCtrl and wxListCtrl are used here as pointers only, so a forward
// declaration is enough — but they must be declared. Every ordinary build happened to pull
// them in transitively through the wx/panel.h + wx/scrolwin.h chain. The Snapmaker fork's
// Flatpak build does not, and it failed to compile this header with "'wxTextCtrl' does not
// name a type; did you mean 'wxTreeCtrl'?". Declaring them keeps the header self-contained
// instead of relying on whatever a particular wx configuration happens to include.
class wxBoxSizer;
class wxTextCtrl;
class wxListCtrl;
class wxButton;
class wxPanel;
class ScalableButton;
namespace Slic3r { namespace GUI {
class DesignCanvas;
// Design (CAD) tab: a sketch-first, Onshape-style form-driven CAD panel.
// Sketch and Extrude are independent tools: the user creates a Sketch first,
// then selects it and Extrudes to produce a solid.
class DesignPanel : public wxPanel
{
public:
explicit DesignPanel(wxWindow* parent);
void on_tab_shown(); // re-sync bed to the active printer when the Design tab is activated
void on_tab_hidden(); // another tab took over: take the viewport status line down with us
void unbind_canvas_event_handlers(); // app close / language switch, from the plater's teardown
void reset_canvas_volumes();
void clear_document(); // New Project / Open Project: drop the document with the project
// Rebuild off the UI thread (progress dialog only if it turns out to be slow), so a feature
// op on a heavy imported solid does not freeze the window. Returns m_doc.recompute()'s result.
// Push the document's recipe into the Model so ANY save path persists it (vjk5).
void sync_recipe_to_model();
bool recompute_guarded(const wxString& message);
// MCP control hooks: let the external control server (McpControl.cpp) drive and
// perceive the SAME kernel the GUI uses. Called only on the wx main thread.
CadDocument& mcp_doc() { return m_doc; } // live document (read + mutate)
void mcp_after_change() { after_tree_edit(true); } // refresh tree + viewport + status
DesignCanvas* mcp_viewport() { return m_viewport; } // live sketch + 3D view
// Put the PANEL into (or out of) sketch mode, not just the canvas tool. Measured on the
// rig: a sketch started straight through DesignCanvas::begin_sketch leaves m_ui_mode at
// Feature, and the keyboard map is dispatched on `m_ui_mode == UiMode::Sketch` while the
// offer menu is dispatched on the looser sketch_map_applies() — so the menu offered the
// line's verbs while every sketch shortcut was dead (KEYTRACE: key=81 ui_mode=0
// is_sketching=1). Half-entering a mode is worse than not entering it.
void mcp_set_sketch_mode(bool on)
{
set_ui_mode(on ? UiMode::Sketch : UiMode::Feature);
update_action_bar();
}
// The offer-table vocabulary without a right-click: the external controller asks which verbs
// exist (and which apply to the current selection) and fires one by id, so a deck key names a
// verb instead of spending a letter and every verb is reachable — including the rows with no
// keyboard shortcut, which are otherwise invisible to anything that parses key tables.
int mcp_offer_selection_kind() const { return offer_selection_kind(); } // OfferSel as int
void mcp_run_action(const char* action) { run_offer_action(action); } // dispatch an action string
// Defined out of line in DesignPanel.cpp: it needs kOfferVerbs, which this header deliberately
// does not include (the table is generated and belongs to the offer-menu code).
bool mcp_run_verb(const char* verb_id);
private:
enum class Tool { None, Sketch, Extrude, Dressup, Hole, Thread, Shell, Revolve, Sweep, Pattern, Plane, Loft, Draft, Boolean, Cut, Insert, Axis, CoordSys, SurfaceExtrude, SurfaceRevolve, SurfaceLoft, SurfaceFill, SurfaceOffset, ThickenSurface, Transform, Mirror, Thicken, Rib, Project, DeleteFace, Helix, Mate };
// Which numeric fields an expression can be bound to, per feature type. A member rather
// than a file-static helper so Tool — 32 values of purely internal card state — does not
// have to become part of this panel's public API just to be named in a signature.
static std::vector<std::string> fields_for_tool(Tool t);
// Plane tool: which datum reference the next solid pick fills (declared early so the
// method decls + card lambdas below can name it).
enum class PlanePick { None, FaceA, FaceB, EdgeA, EdgeB };
enum class AxisPick { None, Face, Edge };
enum class CoordSysPick { None, Face, Edge };
// Onshape-style contextual top toolbar: only the active mode's tool group is
// shown (Feature = sketch/extrude/dress/hole/thread; Sketch = entity tools;
// Constrain = constraints + edit ops). Replaces the old always-visible wall.
enum class UiMode { Feature, Sketch, Constrain };
void set_ui_mode(UiMode m);
void apply_dof_status(int dof, bool ok, bool has_constraints);
// Unified action-bar dispatch: one Confirm / one Cancel for every tool and mode.
void tool_confirm(); // ✓ : commit the active feature / sketch / constrain session
void tool_cancel(); // ✗ : cancel the active feature / discard / exit
// Esc. ONE press unwinds ONE level of the interaction stack (DesignInteraction.hpp), and no
// level of it destroys committed work. escape_level() answers which level the press belongs
// to; escape() acts on exactly that one. Every Esc in the tab routes through here — the key
// used to be handled in four places that could not see each other, and that is how two
// presses in a row reached past a tool and discarded the sketch under it.
CadLevel escape_level() const;
void escape();
void update_action_bar(); // show the ✓/✗ bar iff a tool or mode is active
void on_shape_changed();
void on_add_sketch();
void on_add_extrude();
void on_add_dressup();
void on_add_hole();
void on_add_thread();
void apply_thread_standard(); // fill pitch/depth/radius from m_thread_std selection
void infer_thread_spec(double diameter); // nearest M-standard from a picked cylinder diameter
void on_add_revolve();
void on_add_sweep();
void on_add_loft();
void on_add_pattern();
bool on_add_plane(); // false = refused, card stays open
void arm_plane_pick(PlanePick target); // Plane tool: next solid pick fills this reference
void apply_plane_refs(CadFeature& f) const; // copy type + face/edge refs + sizes from the card
void refresh_plane_labels(); // update the 4 pick labels from the captured refs
void reset_plane_refs(); // clear captured refs (fresh Plane add)
void on_add_shell();
void on_add_draft();
void on_add_boolean();
void on_add_cut(); // commit a plane Cut (split-by-plane)
void on_add_axis();
void arm_axis_pick(AxisPick target);
void apply_axis_refs(CadFeature& f) const;
void refresh_axis_labels();
void reset_axis_refs();
void on_add_coordsys();
void arm_coordsys_pick(CoordSysPick target);
void apply_coordsys_refs(CadFeature& f) const;
void refresh_coordsys_labels();
void refresh_cs_body_choice(); // fill the CoordSys body chooser from current document
void reset_coordsys_refs();
void on_add_surface_extrude();
void on_add_surface_revolve();
void on_add_surface_loft();
void on_add_surface_fill();
void on_add_surface_offset();
void on_add_thicken_surface();
void on_add_transform();
void xf_live_preview(); // typed Transform fields -> body display transform (live)
void xf_clear_preview(); // hand a previewed body back to its pre-card pose
void on_add_mirror();
void on_add_thicken();
void on_add_rib();
void on_add_project();
void on_add_delete_face();
void on_add_helix();
void on_add_mate();
void on_check_interference();
void on_mass_properties(); // read-only report on the selected solid; edits nothing
// Fill m_bool_target / m_bool_tool / m_cut_target. as_of_feature < 0 = current bodies (add);
// >= 0 = the bodies as they existed just before that feature index (Boolean re-edit, so a
// consumed tool body still appears and its saved selection round-trips).
// Which body a tool should act on when it opens: the one picked in the VIEWPORT, else
// the first. Selection comes first and the tool consumes it — every body combo used to
// default to index 0, so picking body 3 and opening Mirror silently mirrored body 1.
// Clamped to the list, so it is safe to hand straight to SetSelection. e1p.
int selected_body_default() const;
void populate_body_choices(int as_of_feature = -1);
// Fill `c` with the bodies as they existed just before `as_of_feature` and select
// `want`. Re-editing any feature that stores a body index needs this: the index was
// recorded against the body list at that point in the timeline, not the final one.
void fill_body_choice(ComboBox* c, int as_of_feature, int want);
void populate_sheet_body_choices(ComboBox* c) const; // bodies where is_sheet_shape() is true
// Rows of a sheet-filtered picker are not body indices; go through these two, never
// GetSelection()/SetSelection() directly.
static int sheet_choice_body(ComboBox* c); // real body index of the current row, or -1
static void select_sheet_choice(ComboBox* c, int body);// select the row holding this body index
// Import rigid 2D art (Text / SVG) as a new Sketch feature carrying
// imported_regions (no solver entities). on_add_text/on_import_svg gather
// input; add_imported_sketch builds the feature, refreshes tree + display.
void on_add_text();
void on_import_svg();
void on_import_step(); // STEP -> editable B-rep body (keeps the OCCT solid, not a mesh)
void on_import_mesh(); // STL/OBJ -> B-rep body via GeometryEngine::mesh_to_brep
bool place_on_face(); // Prepare's Place on Face (F): lay the selected body face on the bed
void add_imported_sketch(const std::vector<std::vector<std::vector<Vec2d>>>& regions,
const wxString& base_name);
// Imported Text/SVG art is placed/sized in-canvas then explicitly committed via a
// small Confirm/Cancel card (Onshape Button->Dialog->Preview->Confirm). The feature
// is added provisionally by add_imported_sketch; Confirm keeps it, Cancel undoes it.
void open_insert_card(const wxString& base_name);
void finalize_insert(); // Confirm: keep the placed art, leave the placement gizmo
void cancel_insert(); // Cancel: undo the provisional insert
// Move / enlarge / stretch (independent X/Y) an imported Text/SVG sketch:
// a modal dialog editing the feature's placement transform in place.
void on_transform_imported(int feat_idx);
void on_commit();
void on_export_step(); // write all bodies to a .step file (native B-rep)
// Rehydrate the parametric model from a project's saved recipe (3MF
// Metadata/orca_cad.bin): deserialize -> recompute -> refresh viewport + tree.
void load_recipe(const std::string& blob);
void refresh_tree();
void set_status_ok();
// Feature-tree editing (Onshape-style): act on the selected tree row.
void on_delete_feature();
// "Delete Body" — the geometry-first counterpart, reached by pointing at a body or any of
// its faces. Resolves the body to the feature that created it and removes THAT, because a
// body is a recomputed result and has nothing else to delete.
void on_delete_body();
void on_new_design();
void on_move_feature(int delta); // -1 = up, +1 = down
void on_toggle_visibility(); // show/hide the selected feature (CadFeature::enabled)
// Constrain mode: enter on the tree-selected sketch, then apply a geometric
// constraint to the in-canvas picked segment and re-solve in the kernel.
void on_begin_constrain(int sel_override = -1);
// Sketch-toolbar Constrain entry: commit the live sketch in place, then enter Constrain
// mode on it (so the constraint palette + Trim/Extend are reachable without leaving the
// sketch flow). Returns true if constrain mode was entered.
bool enter_constrain_inline();
void apply_constraint(SketchConstraintType type);
void apply_entity_constraint(SketchConstraintType type); // Fase 4.2 entity path
void apply_live_constraint(SketchConstraintType type); // Fase 4.2 live-sketch path (no commit needed)
enum class EditOp { Mirror, Offset, Fillet, Trim, Extend, Array, Move, Chamfer, Rotate, Scale, PolarArray }; // Fase 4.4/4.5/4.6 sketch edit ops
void apply_edit_op(EditOp op); // mutate selected sketch entities
// Onshape-style docked value entry (replaces wxGetTextFromUser popups for
// Angle/Radius/Diameter constraints + Offset/Fillet edit ops). request_value
// shows the card and stows a continuation run by confirm_value().
void request_value(const wxString& label, double def, double mn, double mx,
std::function<void(double)> cont,
std::function<void()> on_cancel = nullptr);
void confirm_value();
void cancel_value();
void commit_entity_constraints(const std::vector<SketchEntityConstraintDef>& defs); // multi-def (Symmetric)
// Constraint manager (C3.4): a docked list of the constrained sketch's
// entity-constraints with per-row select (highlight the referenced entities in
// the viewport) and delete (drop the constraint + re-solve). Shown in Constrain
// mode only; operates on m_doc.features[m_constrain_feat].entity_constraints.
// True when the constraint UI must address the LIVE sketch session rather than a committed
// feature. Same discriminator apply_constraint uses to choose apply_live_constraint: both
// Constrain modes set m_active too, so is_sketching() alone would claim the live scope while
// the committed manager is open.
bool live_constraint_scope() const;
void rebuild_constraint_list(); // refill m_constraint_rows
void delete_constraint(int idx); // erase + re-solve + refresh
void highlight_constraint_entities(int idx); // push referenced entities to viewport
void refresh_constrain_dof(); // re-solve feature, mirror DoF readout
wxString constraint_label(const SketchEntityConstraintDef& d) const; // human-readable row text
void after_edit_op(); // shared edit-op refresh tail
void on_edit_feature(); // reopen the selected feature's dialog populated
void after_tree_edit(bool ok); // shared post-op refresh of tree/viewport/status
void load_feature_into_dialog(const CadFeature& f);
void reset_edit_state(); // back to add-mode (m_edit_index = -1)
// Onshape loop: Button -> open_tool (show dialog) -> refresh_preview (ghost) ->
// confirm_tool (commit) / cancel_tool (abort).
void open_tool(Tool t);
void close_tool();
void refresh_preview();
void confirm_tool();
void cancel_tool();
// Ctrl+Z / Ctrl+Shift+Z (Ctrl+Y) from the viewport. With a tool/dialog open it
// cancels that (Esc-like); otherwise it undoes/redoes the committed feature history.
void do_undo_redo(bool redo);
// The plane the Hole tool drills on: a picked face (inward, centred) or the dropdown.
SketchPlane hole_plane() const;
// The plane the Thread tool builds on: a picked cylindrical face (axis) or the dropdown.
SketchPlane thread_plane() const;
// Name the geometry the card has LATCHED, so it never has to be inferred from the viewport.
// Pass -1 for "none, falling back to the plane dropdown". See 200.
void set_hole_target_label(int face);
void set_thread_target_label(int face, int edge);
CadFeature build_candidate(Tool t) const;
// Merge per-body ghost meshes with the per-body display transforms applied. The kernel builds
// a ghost from the untransformed bodies, so without this it floats back at the origin once a
// body has been moved.
TriangleMesh ghost_from_bodies(const std::vector<TriangleMesh>& per_body) const;
// A mate makes no new geometry but it MOVES a body, and the moved assembly is the ghost worth
// showing. Used both by the Mate card and by hovering a row of the offer's mate palette.
bool show_mate_ghost(int kind, int cs_a, int cs_b,
double offset, double angle_deg, bool flip, std::string& err);
int resolve_extrude_sketch() const;
// Plane pickers: fill a choice with XY/XZ/YZ + the document's datum planes, and
// map a choice row back to the actual SketchPlane (rows 0-2 base, 3+ datum).
void populate_plane_choices(ComboBox* c) const;
wxString ref_plane_name(int row) const; // "XY" / a datum's name, for the on-geometry hint
SketchPlane plane_from_choice(int row) const;
// Where a new sketch goes, resolved from what is SELECTED IN THE VIEWPORT rather than from a
// list: a picked planar face wins, otherwise the reference plane last clicked in 3D. `what`
// comes back as something to show the user, so the choice is visible without a combo.
SketchPlane sketch_plane_from_selection(wxString& what) const;
// Whether that resolution has anything the USER picked behind it, rather than the default
// reference plane. Lets a caller say "sketching on XZ" only when it is actually true.
bool sketch_plane_target(wxString& what) const;
// True when Extrude should build only the click-selected loop (a region of the
// resolved sketch is selected and it carries entities).
bool extrude_uses_loop() const;
void sync_sketch_display(); // push un-consumed committed sketches to the viewport
// Feed the viewport's visual Extrude depth-arrow gizmo (C5b) with the current profile
// plane + centroid + live depths while the Extrude card is open (self-gates on m_active).
void update_extrude_gizmo();
void update_fillet_gizmo(); // edge-anchored radius arrow (Dressup card)
void sync_dressup_target(); // Dressup card: show picked edge vs group, gate the combo
void update_hole_gizmo(); // footprint circle + diameter/depth arrows (Hole card)
// A FEATURE button whose tool needs bodies it may not have yet. Greyed with an explanatory
// tooltip below min_bodies, rather than accepting the click and refusing afterwards.
struct BodyGate { wxWindow* btn{nullptr}; int min_bodies{1}; wxString tip_live, tip_gated; };
std::vector<BodyGate> m_body_gates;
void update_body_gates(); // re-evaluate them against the current body count
void update_thread_gizmo(); // footprint circle + radius/length arrows (Thread card)
void update_shell_gizmo(); // inward thickness arrow on the picked face (Shell card)
void update_revolve_gizmo(); // angle-arc around the axis (Revolve card)
void update_draft_gizmo(); // angle-arc around the face centroid (Draft card)
void update_cut_gizmo(); // plane-rectangle + offset arrow (Cut card)
void update_operand_highlight(); // Boolean/Sweep/Loft operand tinting on the canvas
void update_pattern_gizmo(); // linear spacing arrow / circular angle-arc (Pattern card)
void update_datum_gizmo(); // resize handles on the datum plane being created/edited (C3)
void update_helix_gizmo(); // live helix curve + radius/height/pitch handles (Helix card)
void update_rib_gizmo(); // in-plane slab footprint + thickness handles (Rib card)
void refresh_datum_planes(); // push resolved datum frames + per-plane u/v extents to viewport
void refresh_mate_connectors(); // push connector frames so verse + polarity are visible
void update_reference_planes(); // persistent XY/XZ/YZ reference planes (fallback when no object)
CadDocument m_doc;
Tool m_active{Tool::None};
// Keyboard shortcuts (Onshape-style, three scoped layers). Keys are encoded as the
// upper-cased letter, OR'd with 0x10000 when Shift is required. m_keys_sketch fires only
// while a sketch is open (single letters = sketch tools); m_keys_feature fires only when
// no sketch is open (Shift+letter = feature tools; single letters = view toggles/section).
static constexpr int SC_SHIFT = 0x10000;
// ...and with 0x20000 when Ctrl is required too. The Shift+letter space is full, so an
// action that arrives late lives on Ctrl+Shift; plain Ctrl-combos are still passed
// straight through, which is what leaves this layer free.
static constexpr int SC_CTRL = 0x20000;
std::map<int, std::function<void()>> m_keys_sketch;
std::map<int, std::function<void()>> m_keys_feature;
StaticBox* m_tree_box{nullptr}; // framed feature-tree section
StaticBox* m_parts_box{nullptr}; // framed bodies section (hidden while empty)
StaticBox* m_cards{nullptr}; // one framed panel holding every tool dialog (one visible at a time)
void update_cards_frame(); // show that frame iff some card inside it is visible
void show_move_card(bool show);
void apply_move_card(); // numeric move/rotate -> same xform the gizmo builds
void push_polygon_params();
wxSizer* m_tb_commit{nullptr}; // far-right Commit to Plate, beside Confirm/Cancel
wxSizer* m_tb_doc{nullptr}; // toolbar document/view actions (new, commit, export, section, place)
CheckBox* m_show_bed{nullptr}; // view option: draw the printer bed + plate grid, or not
wxSizer* m_box_move{nullptr}; // Move/Rotate numeric options (distance, axis, angle)
wxSizer* m_box_sketch{nullptr};
wxSizer* m_box_extrude{nullptr};
wxSizer* m_box_dressup{nullptr};
wxSizer* m_box_hole{nullptr};
wxSizer* m_box_thread{nullptr};
wxSizer* m_box_shell{nullptr};
wxSizer* m_box_revolve{nullptr};
wxSizer* m_box_sweep{nullptr};
wxSizer* m_box_pattern{nullptr};
wxSizer* m_box_plane{nullptr};
wxSizer* m_box_loft{nullptr};
wxSizer* m_box_draft{nullptr};
wxSizer* m_box_boolean{nullptr};
wxSizer* m_box_cut{nullptr};
wxSizer* m_box_axis{nullptr};
wxSizer* m_box_coordsys{nullptr};
wxSizer* m_box_surf_extrude{nullptr};
wxSizer* m_box_surf_revolve{nullptr};
wxSizer* m_box_surf_loft{nullptr};
wxSizer* m_box_surf_fill{nullptr};
wxSizer* m_box_surf_offset{nullptr};
wxSizer* m_box_surf_thicken{nullptr};
wxSizer* m_box_transform{nullptr};
wxSizer* m_box_mirror{nullptr};
wxSizer* m_box_thicken{nullptr};
wxSizer* m_box_rib{nullptr};
wxSizer* m_box_project{nullptr};
wxSizer* m_box_delete_face{nullptr};
wxSizer* m_box_helix{nullptr};
wxSizer* m_box_mate{nullptr};
wxSizer* m_box_insert{nullptr}; // Confirm/Cancel card for placing Text/SVG art
wxSizer* m_box_expr{nullptr}; // expression binding card (visible during edit only)
int m_insert_feat{-1}; // provisional imported-art feature awaiting Confirm
// Move-body gizmo runs through the unified action bar too: Confirm keeps the placement,
// Cancel reverts to the pose captured when the move started.
int m_move_body{-1};
Transform3d m_move_prev{Transform3d::Identity()};
// Set while the move gizmo is serving the Transform CARD rather than the Move button.
// Both use the same gizmo; only this says which card owns the numbers it reports.
int m_xf_gizmo_body{-1};
Transform3d m_xf_gizmo_base{Transform3d::Identity()}; // pose when Transform armed it
// Which body the Transform card's typed fields are currently previewing on, and the pose to
// hand it back to. Separate from the gizmo pair because the card can retarget its Body combo.
int m_xf_prev_body{-1};
Transform3d m_xf_prev_base{Transform3d::Identity()};
// Onshape-style dialog-card title rows (icon + bold feature name), retitled
// per tool in open_tool() (edit-mode shows the feature's actual name).
wxStaticText* m_hdr_move{nullptr};
wxStaticText* m_hdr_sketch{nullptr};
// Onshape sketch-entry card (plane/orientation) that opens on "New sketch" and
// persists until Finish (Phase 3).
wxSizer* m_box_sketch_session{nullptr};
wxStaticText* m_hdr_sketch_session{nullptr};
wxStaticText* m_sketch_hint{nullptr}; // "click a plane" / "drawing on X" — must match the status
wxStaticText* m_hdr_extrude{nullptr};
wxStaticText* m_hdr_dressup{nullptr};
wxStaticText* m_hdr_hole{nullptr};
wxStaticText* m_hdr_thread{nullptr};
wxStaticText* m_hdr_shell{nullptr};
wxStaticText* m_hdr_revolve{nullptr};
wxStaticText* m_hdr_sweep{nullptr};
wxStaticText* m_hdr_pattern{nullptr};
wxStaticText* m_hdr_plane{nullptr};
wxStaticText* m_hdr_loft{nullptr};
wxStaticText* m_hdr_draft{nullptr};
wxStaticText* m_hdr_boolean{nullptr};
wxStaticText* m_hdr_cut{nullptr};
wxStaticText* m_hdr_axis{nullptr};
wxStaticText* m_hdr_coordsys{nullptr};
wxStaticText* m_hdr_surf_extrude{nullptr};
wxStaticText* m_hdr_surf_revolve{nullptr};
wxStaticText* m_hdr_surf_loft{nullptr};
wxStaticText* m_hdr_surf_fill{nullptr};
wxStaticText* m_hdr_surf_offset{nullptr};
wxStaticText* m_hdr_surf_thicken{nullptr};
wxStaticText* m_hdr_transform{nullptr};
wxStaticText* m_hdr_mirror{nullptr};
wxStaticText* m_hdr_thicken{nullptr};
wxStaticText* m_hdr_rib{nullptr};
wxStaticText* m_hdr_project{nullptr};
wxStaticText* m_hdr_delete_face{nullptr};
wxStaticText* m_hdr_helix{nullptr};
wxStaticText* m_hdr_mate{nullptr};
wxStaticText* m_hdr_insert{nullptr};
wxScrolledWindow* m_form{nullptr};
DesignCanvas* m_viewport{nullptr};
// Top contextual toolbar (parented to the panel, above the form/viewport row).
UiMode m_ui_mode{UiMode::Feature};
// Sketch environment banner: a strip across the top of the viewport saying, in words, that
// this is a sketch and which one. The mode used to be legible only from the toolbar and the
// left card — both of which look like the rest of the app — so a sketch session and plate
// preparation were one glance apart. Indicator only: Finish/Cancel stay on the ONE ribbon
// action bar (the Design UX contract), and the banner never grows a second pair.
wxPanel* m_sketch_banner{nullptr};
wxStaticText* m_sketch_banner_txt{nullptr};
wxScrolledWindow* m_toolbar{nullptr}; // horizontally scrollable so the action bar stays reachable on narrow windows
wxSizer* m_tb_feature{nullptr};
wxSizer* m_tb_sketch{nullptr};
// The 20 constraint icon buttons, shown during BOTH Sketch and Constrain (Fase 4.2 live
// path: a constraint must be applicable while drawing, not only after committing).
wxSizer* m_tb_relations{nullptr};
// Unified Confirm/Cancel action bar (right end of the ribbon). Shown whenever any
// tool or mode is active; the single confirm/cancel surface for the whole tab.
wxSizer* m_tb_action{nullptr};
// Persistent Undo/Redo group at the left of the ribbon — always visible, independent
// of the mode-gated tool groups. The buttons are greyed per the document history and
// the do_undo_redo gate (see update_undo_redo_buttons).
wxSizer* m_tb_history{nullptr};
ScalableButton* m_btn_undo{nullptr};
ScalableButton* m_btn_redo{nullptr};
void update_undo_redo_buttons(); // enable/disable Undo/Redo from can_undo/can_redo + gate
// All tool buttons, for the active-tool teal highlight (Onshape-style).
std::vector<ScalableButton*> m_tool_btns;
ScalableButton* m_active_tool_btn{nullptr};
void set_active_tool_btn(ScalableButton* b); // nullptr clears the highlight
// Owns the themed DropDown flyouts (and the item vectors they hold by ref).
std::vector<std::shared_ptr<void>> m_flyout_keepalive;
wxCheckBox* m_construction{nullptr}; // sketch-mode construction toggle
wxSpinCtrlDouble* m_move_dx{nullptr}; // Move/Rotate card: world translation
wxSpinCtrlDouble* m_move_dy{nullptr};
wxSpinCtrlDouble* m_move_dz{nullptr};
ComboBox* m_move_axis{nullptr}; // rotation axis: X/Y/Z
wxSpinCtrlDouble* m_move_angle{nullptr}; // rotation angle (deg)
// Polygon's two parameters are chosen FROM THE TOOL, in the offer's Polygon submenu, not
// from a card on the left: the side count cannot be edited after drawing (the inline editor
// offers Side and Angle only), so it has to be settled at the moment the tool is armed —
// which is exactly where the offer already is. e1p.
int m_poly_sides{6}; // 3..64; the submenu names the common ones
bool m_poly_circumscribed{false};
// Which reference plane a sketch falls back to when no face is picked: 0/1/2 = XY/XZ/YZ,
// >=3 indexes resolve_datum_planes(). Set by CLICKING a ghost plane in the viewport — there is
// deliberately no dropdown for it. e1p.
int m_ref_plane{0};
// m_ref_plane is always a VALID plane, so it cannot itself distinguish "the user chose XY"
// from "nobody has chosen anything yet". This does.
bool m_plane_picked{false};
ComboBox* m_shape{nullptr};
ComboBox* m_mode{nullptr};
wxSpinCtrlDouble* m_width{nullptr};
wxSpinCtrlDouble* m_height{nullptr};
wxSpinCtrlDouble* m_radius{nullptr};
wxSpinCtrlDouble* m_distance{nullptr};
ComboBox* m_extrude_end{nullptr}; // Blind/Symmetric/TwoSided/ThroughAll/UpTo*
wxSpinCtrlDouble* m_distance2{nullptr}; // second-side depth (Two-sided)
wxSpinCtrlDouble* m_taper{nullptr}; // draft angle (deg)
CheckBox* m_flip{nullptr}; // reverse extrude direction
wxStaticText* m_extrude_sketch_label{nullptr};
int m_extrude_sketch_ref{-1};
// Revolve controls (sweep a sketch profile about an in-plane axis).
wxStaticText* m_revolve_sketch_label{nullptr};
wxSpinCtrlDouble* m_revolve_angle{nullptr};
ComboBox* m_revolve_axis{nullptr}; // 0 = plane X, 1 = plane Y
ComboBox* m_revolve_mode{nullptr}; // New/Add/Cut/Intersect
CheckBox* m_revolve_flip{nullptr};
int m_revolve_sketch_ref{-1};
// Sweep controls (sweep a profile sketch along a path sketch).
wxStaticText* m_sweep_profile_label{nullptr};
ComboBox* m_sweep_path{nullptr}; // path Sketch picker (feature index in client data)
ComboBox* m_sweep_mode{nullptr}; // New/Add/Cut/Intersect
int m_sweep_profile_ref{-1};
int m_sweep_path_ref{-1}; // path Sketch feature index (for re-edit pre-select)
// Loft controls (skin a solid through 2+ ordered profile Sketches).
wxCheckListBox* m_loft_list{nullptr}; // every Sketch; check 2+ in list order = profiles
CheckBox* m_loft_ruled{nullptr}; // ruled (straight) vs smooth sections
ComboBox* m_loft_mode{nullptr}; // New/Add/Cut/Intersect
std::vector<int> m_loft_sketch_idx; // feature index for each row in m_loft_list
std::vector<int> m_loft_refs; // chosen profile refs (for re-edit pre-check)
// Surface Extrude controls (sheet from sketch profile).
wxStaticText* m_surf_extrude_sketch_label{nullptr};
wxSpinCtrlDouble* m_surf_extrude_distance{nullptr};
int m_surf_extrude_sketch_ref{-1};
// Surface Revolve controls (sheet from sketch about axis).
wxStaticText* m_surf_revolve_sketch_label{nullptr};
wxSpinCtrlDouble* m_surf_revolve_angle{nullptr};
ComboBox* m_surf_revolve_axis{nullptr}; // 0 = plane X, 1 = plane Y
CheckBox* m_surf_revolve_flip{nullptr};
int m_surf_revolve_sketch_ref{-1};
// Surface Loft controls (skin a sheet through 2+ ordered profile sketches).
wxCheckListBox* m_surf_loft_list{nullptr}; // every Sketch; check 2+ in list order = profiles
CheckBox* m_surf_loft_ruled{nullptr}; // ruled (straight) vs smooth sections
std::vector<int> m_surf_loft_sketch_idx; // feature index for each row
std::vector<int> m_surf_loft_refs; // chosen profile refs (for re-edit pre-check)
// Surface Fill controls (one-face sheet from a sketch boundary).
wxStaticText* m_surf_fill_sketch_label{nullptr};
int m_surf_fill_sketch_ref{-1};
// Surface Offset controls (offset a SHEET body).
ComboBox* m_surf_offset_body{nullptr}; // sheet-body picker
wxSpinCtrlDouble* m_surf_offset_distance{nullptr};
// Thicken Surface controls (thicken a SHEET body into a solid).
ComboBox* m_surf_thicken_body{nullptr}; // sheet-body picker
wxSpinCtrlDouble* m_surf_thicken_thickness{nullptr};
CheckBox* m_surf_thicken_flip{nullptr};
// Transform controls (rigid move/rotate of a body).
ComboBox* m_xf_body{nullptr}; // body to transform
wxSpinCtrlDouble* m_xf_dx{nullptr}; // translate X
wxSpinCtrlDouble* m_xf_dy{nullptr}; // translate Y
wxSpinCtrlDouble* m_xf_dz{nullptr}; // translate Z
ComboBox* m_xf_axis{nullptr}; // rotation axis: X/Y/Z
wxSpinCtrlDouble* m_xf_angle{nullptr}; // rotation angle (deg)
wxSpinCtrlDouble* m_xf_pivot_x{nullptr}; // pivot X
wxSpinCtrlDouble* m_xf_pivot_y{nullptr}; // pivot Y
wxSpinCtrlDouble* m_xf_pivot_z{nullptr}; // pivot Z
CheckBox* m_xf_copy{nullptr}; // keep original (make a copy)
// Mirror controls (reflect a body about a plane).
ComboBox* m_mirror_body{nullptr}; // body to mirror
ComboBox* m_mirror_plane{nullptr}; // mirror plane (XY/XZ/YZ + datums)
CheckBox* m_mirror_keep{nullptr}; // keep original body
// Thicken controls (offset one solid face into a thin plate).
ComboBox* m_thicken_body{nullptr}; // source body
wxStaticText* m_thicken_face_label{nullptr}; // picked face
wxSpinCtrlDouble* m_thicken_thickness{nullptr};
CheckBox* m_thicken_flip{nullptr}; // flip direction
// Rib controls (thin wall from an open sketch line).
ComboBox* m_rib_body{nullptr}; // target body
ComboBox* m_rib_sketch{nullptr}; // sketch holding the open line (feature index in client data)
wxSpinCtrl* m_rib_entity{nullptr}; // entity index within the sketch
wxSpinCtrlDouble* m_rib_thickness{nullptr};
wxSpinCtrlDouble* m_rib_depth{nullptr};
// Project controls (project body edges onto a plane as sketch entities).
ComboBox* m_proj_source_body{nullptr}; // source body
wxStaticText* m_proj_face_label{nullptr}; // picked face (or "all edges")
ComboBox* m_proj_plane{nullptr}; // target plane
// Delete Face controls (remove faces, heal the solid).
ComboBox* m_del_face_body{nullptr}; // target body
wxButton* m_del_face_add_btn{nullptr}; // "Add picked face" button
wxStaticText* m_del_face_list{nullptr}; // shows the accumulated face ids
std::vector<int> m_del_faces; // accumulated face list
// Helix controls (helical curve).
ComboBox* m_helix_plane{nullptr}; // axis plane (XY/XZ/YZ + datums)
wxSpinCtrlDouble* m_helix_radius{nullptr};
wxSpinCtrlDouble* m_helix_pitch{nullptr};
wxSpinCtrlDouble* m_helix_height{nullptr};
CheckBox* m_helix_left_handed{nullptr};
wxSpinCtrlDouble* m_helix_taper{nullptr};
// Mate (assembly) controls
ComboBox* m_mate_kind{nullptr};
ComboBox* m_mate_cs_a{nullptr};
ComboBox* m_mate_cs_b{nullptr};
wxSpinCtrlDouble* m_mate_offset{nullptr};
wxSpinCtrlDouble* m_mate_angle{nullptr};
CheckBox* m_mate_flip{nullptr};
wxStaticText* m_offset_label{nullptr};
wxStaticText* m_angle_label{nullptr};
// Expression binding (per-feature, visible during edit only)
ComboBox* m_expr_field{nullptr}; // field-name picker (editable)
wxTextCtrl* m_expr_text{nullptr}; // expression string
wxButton* m_expr_set_btn{nullptr}; // Apply / bind
wxButton* m_expr_clear_btn{nullptr}; // Remove binding
wxStaticText* m_expr_status{nullptr}; // shows current bindings for the edited feature
void populate_expr_fields(Tool t); // fill m_expr_field from feature-type fields
void on_set_expr(); // checkpoint + write -> recompute -> undo on fail
void on_clear_expr(); // remove selected binding
// Document variables panel (below the feature tree / parts)
StaticBox* m_var_box{nullptr};
wxListCtrl* m_var_list{nullptr};
wxButton* m_btn_add_var{nullptr};
wxButton* m_btn_edit_var{nullptr};
wxButton* m_btn_del_var{nullptr};
void refresh_variables(); // rebuild m_var_list from m_doc.variables
void on_add_variable();
void on_edit_variable();
void on_remove_variable();
// Feature-tree button
ScalableButton* m_btn_interfere{nullptr};
// Pattern controls (replicate the target body: linear or circular).
ComboBox* m_pattern_type{nullptr}; // 0 = Linear, 1 = Circular
wxSpinCtrlDouble* m_pattern_count{nullptr}; // total instances incl. seed
wxSpinCtrlDouble* m_pattern_spacing{nullptr}; // linear step (mm)
ComboBox* m_pattern_dir{nullptr}; // linear direction: 0 = plane X, 1 = plane Y
wxSpinCtrlDouble* m_pattern_angle{nullptr}; // circular total angle (deg)
// Boolean controls (combine two existing bodies).
ComboBox* m_bool_op{nullptr}; // 0 = Union, 1 = Subtract, 2 = Intersect
ComboBox* m_bool_target{nullptr}; // body that survives (selection == body index)
ComboBox* m_bool_tool{nullptr}; // body consumed (selection == body index)
// Which operand the NEXT viewport body pick fills: 0 = target, 1 = tool. Reset when the
// card opens, so the first two clicks in the viewport always mean "keep this, cut with
// that" in that order. The combos remain the typed half and mirror whatever is picked.
int m_bool_next_slot{0};
CheckBox* m_bool_keep{nullptr}; // keep the tool body after the op
wxSpinCtrlDouble* m_bool_tol{nullptr}; // OCCT fuzzy tolerance (mm); robust cut on near-coincident faces
// Plane Cut (split-by-plane): a reference plane + offset splits the target body into
// two separate bodies (both pieces kept).
ComboBox* m_cut_plane{nullptr}; // XY/XZ/YZ + datum planes (cut plane)
ComboBox* m_cut_target{nullptr}; // body to cut (selection == body index)
wxSpinCtrlDouble* m_cut_offset{nullptr}; // offset along the plane normal (mm)
// Datum plane controls (derive a selectable sketch plane: offset + tilt from a base).
ComboBox* m_plane_base{nullptr}; // 0=XY,1=XZ,2=YZ, 3+N = Nth datum plane
wxSpinCtrlDouble* m_plane_offset{nullptr}; // offset along base normal (mm)
wxSpinCtrlDouble* m_plane_tilt{nullptr}; // tilt about a base axis (deg) / Angle / Tangent angle
ComboBox* m_plane_tilt_axis{nullptr}; // 0 = base X, 1 = base Y
// Plane construction method + contextual face/edge reference picks (Onshape/Fusion parity).
ComboBox* m_plane_type{nullptr}; // PlaneType: Offset/Angle/Midplane/Tangent/TwoEdges/Coincident
wxButton* m_plane_pick_faceA{nullptr}; wxStaticText* m_plane_faceA_lbl{nullptr};
wxButton* m_plane_pick_faceB{nullptr}; wxStaticText* m_plane_faceB_lbl{nullptr};
wxButton* m_plane_pick_edgeA{nullptr}; wxStaticText* m_plane_edgeA_lbl{nullptr};
wxButton* m_plane_pick_edgeB{nullptr}; wxStaticText* m_plane_edgeB_lbl{nullptr};
wxSpinCtrlDouble* m_plane_usize{nullptr}; // datum rectangle extent u (mm) — also driven by drag handles
wxSpinCtrlDouble* m_plane_vsize{nullptr}; // datum rectangle extent v (mm)
// Captured references for the candidate datum (body index + face/edge index, -1 = none).
int m_pl_faceA_body{-1}, m_pl_faceA{-1};
int m_pl_faceB_body{-1}, m_pl_faceB{-1};
int m_pl_edgeA_body{-1}, m_pl_edgeA{-1};
int m_pl_edgeB_body{-1}, m_pl_edgeB{-1};
PlanePick m_plane_pick{PlanePick::None}; // which ref the next solid pick fills
// Plate loop selection (click a committed sketch loop): the Sketch feature + the
// clicked closed-region index, so Extrude builds just that one loop. -1 = none.
int m_sel_sketch_feat{-1};
int m_sel_sketch_region{-1};
// Click-selected solid topology (whole/face/edge cycle): face id for up-to-face / dress-up.
int m_sel_solid_body{-1}; // which body the face/edge selection is on
int m_sel_solid_face{-1};
int m_sel_solid_edge{-1};
bool m_sel_solid_vertex{false}; // a corner is picked (body+point, no face/edge)
// The face actually under the last solid click, INDEPENDENT of the whole/face/edge cycle level.
// The first click on a solid selects the WHOLE body, but the ray has already resolved which face
// it hit and the callback passes it. "Sketch on the face I clicked" must not require discovering
// that a second click refines the selection, so keep it instead of throwing it away. 3a2.
int m_pick_face_body{-1};
int m_pick_face{-1};
// What the live sketch was actually opened on ("the picked face", "XY", a datum's name), so the
// hint can say it. Resolved from the selection at begin_sketch, not read back from a combo.
wxString m_sketch_on;
// --- the object-driven offer (charter 4.1) ---------------------------------------------
// Right-click the geometry -> a vertical list in ratified row order, verbs that do not
// apply disabled IN PLACE with their reason. The rows come from the generated table in
// DesignOffer.hpp; this map is how a row reaches the code that already implements it, for
// the verbs that have no keyboard shortcut to route through.
std::map<std::string, std::function<void()>> m_verb_actions;
// Append an offer row with its toolbar glyph. The bitmap must be set BEFORE Append —
// wxGTK builds the GtkMenuItem there and only makes an image item if one is present.
// Every status write goes through here so long hints wrap instead of clipping.
void set_status(const wxString& text);
wxString idle_hint() const; // what to say when nothing is selected
// Reason detect_mate_conflicts() recorded for a feature, or nullptr. Marks the tree row and
// feeds the status line; a conflict is a diagnostic, not a document error.
const std::string* mate_conflict_reason(int feature) const;
wxMenuItem* append_offer_item(wxMenu* menu, int id, const wxString& text,
const struct OfferVerb& v);
void show_offer_menu(const wxPoint& screen_pos);
// Where the offer opens when no mouse press anchors it: the keyboard route, and the automatic
// open on entering Sketch. The pointer if it is over the viewport, else the viewport's centre.
// A raw wxGetMousePosition() can be sitting on the toolbar, on the card column or on another
// monitor, and the menu would map there — detached from the geometry it is about.
wxPoint offer_anchor() const;
int offer_selection_kind() const; // an OfferSel, as int to keep the header light
// Does the SKETCH half of the map apply? A mode question, not a session one: begin_sketch
// does not run until the first tool is armed, so between "press Sketch" and "pick a tool"
// is_sketching() is still false — precisely when the drawing tools must be on offer. The
// is_sketching() arm covers re-opening a committed sketch, which enters the session first.
bool sketch_map_applies() const;
void run_offer_action(const char* action);
// Face-as-profile extrude (Onshape): when Extrude is opened on a picked solid face with
// no sketch source, this carries that global face id so the kernel extrudes the face.
// -1 = ordinary sketch/loop extrude. Set when opening the Extrude card, consumed on add.
int m_extrude_face_src{-1};
ComboBox* m_dressup_type{nullptr};
ComboBox* m_face_group{nullptr};
wxSpinCtrlDouble* m_dressup_size{nullptr};
wxStaticText* m_dressup_edge_label{nullptr}; // shows the picked edge, or the group fallback
ComboBox* m_hole_plane{nullptr};
wxSpinCtrlDouble* m_hole_diameter{nullptr};
wxSpinCtrlDouble* m_hole_depth{nullptr};
CheckBox* m_hole_through{nullptr};
wxSpinCtrlDouble* m_hole_x{nullptr};
wxSpinCtrlDouble* m_hole_y{nullptr};
// #2: when the Hole tool is opened on a picked solid face, drill on that face centred
// on it (origin = face centroid, normal = inward). m_hole_x/y then read as the offset
// from the face centre. Falls back to the m_hole_plane dropdown when no face is picked.
bool m_hole_on_face{false};
SketchPlane m_hole_face_plane;
int m_hole_face_body{-1};
// #2 Part B: the picked face's (u,v) bounds in m_hole_face_plane, so the hole's construction
// dims read as distance from the face sides (umin/vmin edges) rather than from the centre.
bool m_hole_has_bounds{false};
double m_hole_umin{0}, m_hole_umax{0}, m_hole_vmin{0}, m_hole_vmax{0};
// Says which face the latch above is holding. Thicken/Shell/Draft show theirs because their
// face IS the live selection; this one has to be shown precisely BECAUSE it is not, and the
// status line goes on saying "Nothing selected" while the ghost keeps drilling. 200.
wxStaticText* m_hole_target_label{nullptr};
ComboBox* m_thread_plane{nullptr};
ComboBox* m_thread_std{nullptr}; // standard designation (M6, 1/4-20 UNC, ...)
wxSpinCtrlDouble* m_thread_radius{nullptr};
wxSpinCtrlDouble* m_thread_pitch{nullptr};
wxSpinCtrlDouble* m_thread_height{nullptr};
wxSpinCtrlDouble* m_thread_depth{nullptr};
CheckBox* m_thread_internal{nullptr};
wxSpinCtrlDouble* m_thread_x{nullptr};
wxSpinCtrlDouble* m_thread_y{nullptr};
// #3: when the Thread tool is opened on a picked cylindrical face (a hole bore or a
// cylinder), thread that surface — plane on its axis, radius/internal derived from it.
bool m_thread_on_face{false};
SketchPlane m_thread_face_plane;
int m_thread_face_body{-1};
wxStaticText* m_thread_target_label{nullptr}; // the latched face/edge — see m_hole_target_label
wxSpinCtrlDouble* m_shell_thickness{nullptr};
wxStaticText* m_shell_face_label{nullptr}; // shows the picked face to remove
// Draft controls (taper a single picked solid face about the body bottom).
wxSpinCtrlDouble* m_draft_angle{nullptr};
wxStaticText* m_draft_face_label{nullptr}; // shows the picked face to draft
// Axis controls (datum axis: line through two points or derived from geometry).
ComboBox* m_axis_type{nullptr}; // AxisType: TwoPoints/FaceNormal/CylinderCenterline/PlaneIntersection/AlongEdge
wxButton* m_axis_pick_face{nullptr}; wxStaticText* m_axis_face_lbl{nullptr};
wxButton* m_axis_pick_edge{nullptr}; wxStaticText* m_axis_edge_lbl{nullptr};
ComboBox* m_axis_plane_a{nullptr};
ComboBox* m_axis_plane_b{nullptr};
wxSpinCtrlDouble* m_axis_p1x{nullptr}; wxSpinCtrlDouble* m_axis_p1y{nullptr}; wxSpinCtrlDouble* m_axis_p1z{nullptr};
wxSpinCtrlDouble* m_axis_p2x{nullptr}; wxSpinCtrlDouble* m_axis_p2y{nullptr}; wxSpinCtrlDouble* m_axis_p2z{nullptr};
int m_ax_face_body{-1}, m_ax_face{-1};
int m_ax_edge_body{-1}, m_ax_edge{-1};
AxisPick m_axis_pick{AxisPick::None};
// CoordSys controls (datum coordinate system: point + orthonormal frame).
ComboBox* m_coordsys_type{nullptr}; // CoordSysType: PointWorld/FaceAndDirection
ComboBox* m_cs_body{nullptr}; // body-focus chooser: restrict picking to one body
wxSpinCtrlDouble* m_cs_x{nullptr}; wxSpinCtrlDouble* m_cs_y{nullptr}; wxSpinCtrlDouble* m_cs_z{nullptr};
wxButton* m_cs_pick_face{nullptr}; wxStaticText* m_cs_face_lbl{nullptr};
wxButton* m_cs_pick_edge{nullptr}; wxStaticText* m_cs_edge_lbl{nullptr};
wxSpinCtrlDouble* m_cs_hx{nullptr}; wxSpinCtrlDouble* m_cs_hy{nullptr}; wxSpinCtrlDouble* m_cs_hz{nullptr};
int m_cs_face_body{-1}, m_cs_face{-1};
int m_cs_edge_body{-1}, m_cs_edge{-1};
CoordSysPick m_coordsys_pick{CoordSysPick::None};
// Onshape-style docked value-entry card (Angle/Radius/Diameter/Offset/Fillet).
wxSizer* m_box_value{nullptr};
wxStaticText* m_value_label{nullptr};
wxTextCtrl* m_value_input{nullptr}; // plain text field: forces en ('.') decimals
double m_value_min{0.0}; // range for confirm-time clamping
double m_value_max{0.0};
std::function<void(double)> m_value_cont; // deferred apply, run on Confirm
std::function<void()> m_value_cancel; // optional action when the card is cancelled
// Feature tree: a wxTreeCtrl with per-feature-type icons. Callers keep using
// integer row indices via tree_selection()/set_tree_selection(); m_tree_items
// maps feature order -> tree node, rebuilt by refresh_tree().
wxTreeCtrl* m_tree{nullptr};
wxTreeCtrl* m_parts{nullptr}; // Bodies list under the feature tree
wxStaticText* m_parts_label{nullptr}; // its "Bodies" caption (hidden when empty)
wxBoxSizer* m_parts_hdr{nullptr}; // Bodies card header (icon + title)
wxStaticLine* m_parts_rule{nullptr}; // rule under that header
wxBoxSizer* m_hdr_tree_row{nullptr}; // Feature tree header: title + row actions
wxStaticText* m_hdr_tree{nullptr}; // its title label
wxImageList* m_tree_images{nullptr};
std::vector<wxTreeItemId> m_tree_items;
// Parts list: tree rows for each body (parallel to m_doc.bodies). Selecting one
// highlights that body and makes it the target for the next op.
std::vector<wxTreeItemId> m_tree_body_items;
// Section views (non-destructive): named "Section View N" entries listed in the tree, each a
// horizontal clip height. View-only — NOT bodies/features, never serialized. Key X adds one;
// clicking a row activates it (again = off); Delete removes it; Alt+Wheel moves the active one.
// Section view (single, non-destructive): ONE horizontal clip that hides half the model to
// inspect inside — solid, no ghost of the hidden half. Toggled on/off; Flip shows the other
// half. Never a body, no tree entry.
bool m_section_on{false};
double m_section_cut_z{0.0};
bool m_section_upper{false}; // false = keep lower half, true = upper
ScalableButton* m_section_flip_btn{nullptr}; // toolbar action; enabled only while the section is on
void toggle_section_view(); // Section View button / X: on <-> off
void flip_section_view(); // Flip button / F: opposite half
void update_section_flip_btn(); // enable the Flip button iff the section is on
// Per-body visibility (parallel to m_doc.bodies; index stable across recompute since
// bodies are appended in feature order). Empty/grown to all-visible by sync_body_visible().
std::vector<bool> m_body_visible;
void sync_body_visible(); // grow/shrink m_body_visible to bodies.size()
// Per-body display translation (Move-body, M5). Parallel to m_doc.bodies; default
// identity. Applied to the display/pick meshes only — the OCCT shape (and face/edge
// global ids) is never touched, so dress-up targeting stays stable across a move.
std::vector<Transform3d> m_body_xform;
std::vector<TriangleMesh> m_disp_body_meshes; // display_body_meshes with m_body_xform applied
TriangleMesh m_disp_pick_mesh; // combined pick mesh with m_body_xform applied
void sync_body_xform(); // grow m_body_xform to bodies.size() (identity)
void rebuild_disp_meshes(); // recompute m_disp_* from m_doc + m_body_xform
void feed_bodies(); // push m_disp_* + visibility/xform to the viewport
void on_move_body(); // start the move gizmo on the selected body
void arm_transform_gizmo(); // arm the move gizmo on the Transform card's body (add mode only)
void on_set_body_color(); // Color tool: pick a per-body display colour override
void on_boolean_tool(); // Boolean (combine bodies): needs two solids, then opens the tool
int tree_selection() const; // selected feature row, or wxNOT_FOUND
int tree_body_selection() const; // selected Parts-list body index, or -1
void refresh_parts(); // rebuild the Bodies list under the feature tree
void sync_sidebar_width(); // keep the panel as wide as Prepare's sidebar
void set_tree_selection(int row);
static int tree_icon_for(CadFeatureType t);
wxStaticText* m_status{nullptr};
// m_status's foreground as created, captured before any caller touches it. Callers signal
// "no opinion" by setting wxNullColour, which restores exactly this — so it is the only
// reliable way to tell a chosen colour (the error red) from the default. See set_status().
wxColour m_status_default_fg;
// The guidance sentence for the step the armed sketch tool is on, kept so a transient
// readout (the live length/angle while a segment is being dragged) can be appended to it
// instead of replacing it — the guidance used to vanish on the first mouse move after a
// click, which is precisely when it is needed. 1c0c.
wxString m_sketch_step;
// mode is a DesignSketchTool::Mode; passed as an int because this header deliberately does
// not include the tool's, and the .cpp (which does) casts it back.
void on_sketch_step(int mode, int step, int picks);
wxStaticText* m_dof_status{nullptr}; // DoF / constraint-state readout (P3)
// Last live-solve result, so entering Constrain can restore the readout without a solve.
int m_dof_last{-1};
bool m_dof_last_ok{true};
bool m_dof_last_has{false};
int m_feature_counter{0};
std::vector<wxButton*> m_confirm_btns;
// Edit-in-place state: add-mode is m_edit_index == -1. Single-feature edit
// (Sketch or Extrude independently) uses only m_edit_index as the row to replace.
int m_edit_index{-1};
// Tree row of the sketch currently being constrained (-1 = not constraining).
int m_constrain_feat{-1};
// Constraint-manager card (C3.4): header + a rebuildable list of constraint rows.
wxSizer* m_box_constraints{nullptr};
wxStaticText* m_hdr_constraints{nullptr};
wxSizer* m_constraint_rows{nullptr};
int m_constraint_sel{-1}; // highlighted constraint row, or -1
};
}} // namespace Slic3r::GUI
#endif // slic3r_DesignPanel_hpp_
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
#ifndef slic3r_GUI_McpControl_hpp_
#define slic3r_GUI_McpControl_hpp_
// MCP control surface (slice 1): a local JSON-RPC 2.0 server, line-delimited over a
// Unix domain socket, that lets an external MCP bridge drive and perceive the Design
// tab. Off unless the env var ORCA_CAD_MCP is set:
// ORCA_CAD_MCP=1 -> socket at /tmp/orca-cad-mcp.sock
// ORCA_CAD_MCP=/path/to.sock -> socket at that path
// All CAD work is marshalled onto the wx main thread and runs through the SAME
// CadDocument kernel the GUI uses (no parallel engine). Slice-1 methods:
// describe_tools, describe_scene, extrude.
//
// ponytail: Unix-socket only (POSIX). Windows compiles this to a no-op; add a named
// pipe transport when a Windows agent actually needs it.
namespace Slic3r { namespace GUI {
// Start the server thread iff ORCA_CAD_MCP is set. Safe to call once after the
// MainFrame + DesignPanel exist. No-op when the env var is unset or on Windows.
void start_mcp_control_if_enabled();
}} // namespace Slic3r::GUI
#endif // slic3r_GUI_McpControl_hpp_
+203
View File
@@ -0,0 +1,203 @@
#include "slic3r/GUI/CAD/SketchInlineEditor.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "libslic3r/Color.hpp"
#include <imgui/imgui.h>
#include <imgui/imgui_internal.h> // BringWindowToDisplayFront / GetCurrentWindow
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
namespace Slic3r {
namespace GUI {
namespace {
// Numbers are typed and shown with a POINT, whatever the locale: this field feeds a CAD kernel,
// and a decimal comma reaching it as a thousands separator is a silent order-of-magnitude error.
// Parsing accepts either separator because a keyboard's numeric pad may only offer one.
std::string fmt_value(double v, int digits = 2)
{
char fmt[16];
std::snprintf(fmt, sizeof(fmt), "%%.%df", digits);
char buf[64];
std::snprintf(buf, sizeof(buf), fmt, v);
for (char* c = buf; *c; ++c)
if (*c == ',') *c = '.';
return std::string(buf);
}
bool parse_value(const char* text, double& out)
{
if (text == nullptr) return false;
std::string t(text);
for (char& c : t)
if (c == ',') c = '.';
// strtod, not std::stod: no exceptions, and `end` tells us whether the WHOLE field was a
// number. "12mm" must be refused, not silently read as 12.
const char* b = t.c_str();
char* end = nullptr;
const double v = std::strtod(b, &end);
if (end == b) return false;
while (*end == ' ' || *end == '\t') ++end;
if (*end != '\0') return false;
out = v;
return true;
}
// One machine-readable line per event of the click-edit contract, for the UX check that runs
// after every build (scripts/CAD/check-gui-click-edit.py). Deliberately NOT the same switch as
// ORCA_CAD_KEYTRACE: that one is a debugging firehose, this one is an assertion surface and its
// format is a contract the script parses.
//
// The pair that matters is `open` vs `commit`: the check always types a value DIFFERENT from the
// prefill, so a field that is on screen but not editable commits its prefill and the two lines
// disagree. A focus flag cannot show that — it read 0 even when typing worked — but the number
// the user actually gets can.
void ux_trace(const char* event, const std::string& title, const std::string& detail)
{
if (!std::getenv("ORCA_CAD_UXTRACE")) return;
std::fprintf(stderr, "[UX] %s title=%s %s\n", event, title.c_str(), detail.c_str());
std::fflush(stderr);
}
} // namespace
void SketchInlineEditor::open(const wxPoint& canvas_px, double value, const std::string& title,
std::function<void(double)> on_commit,
std::function<void()> on_cancel)
{
m_anchor = canvas_px;
m_title = title;
m_err.clear();
m_commit = std::move(on_commit);
m_cancel = std::move(on_cancel);
const std::string v = fmt_value(value);
std::snprintf(m_buf, sizeof(m_buf), "%s", v.c_str());
m_open = true;
// ImGui takes keyboard focus for one frame on request; asking on the frame the field first
// appears is what makes typing land without a click. There is no window manager to consult.
m_focus_pending = true;
ux_trace("open", m_title, "prefill=" + v);
}
void SketchInlineEditor::close()
{
m_open = false;
m_focus_pending = false;
m_commit = nullptr;
m_cancel = nullptr;
m_err.clear();
}
void SketchInlineEditor::cancel()
{
if (m_open) do_cancel();
}
void SketchInlineEditor::commit()
{
if (m_open) do_commit();
}
void SketchInlineEditor::do_cancel()
{
ux_trace("cancel", m_title, "");
auto cb = m_cancel;
close();
if (cb) cb();
}
void SketchInlineEditor::do_commit()
{
double v = 0.0;
if (!parse_value(m_buf, v)) {
// Refusing input in silence is indistinguishable from the app having frozen: the field
// just sits there and the user has no idea what it wants. Say so in the title line and
// keep editing.
ux_trace("refused", m_title, std::string("typed=") + m_buf);
m_err = (m_buf[0] == '\0') ? _u8L("Enter a number") : _u8L("Not a number");
m_focus_pending = true;
return;
}
ux_trace("commit", m_title, std::string("typed=") + m_buf + " value=" + fmt_value(v, 4));
auto cb = m_commit;
close();
// AFTER close(): the callback may open the next queued dimension (a rectangle queues Width
// then Height), and doing that into a field that still believes it is open would drop the
// second one's prefill on the floor.
if (cb) cb(v);
}
bool SketchInlineEditor::render(ImGuiWrapper& imgui, float scale)
{
if (!m_open) return false;
ImGuiWrapper::push_common_window_style(scale);
imgui.set_next_window_pos((float) m_anchor.x, (float) m_anchor.y, ImGuiCond_Always, 0.5f, 0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3.0f);
// NoInputs is what every other sketch overlay sets and is exactly what this one must not:
// it is the only overlay in the tab that the user types into.
imgui.begin(std::string("##sketchvalue"),
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration
| ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings);
ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow());
if (!m_title.empty() || !m_err.empty()) {
if (m_err.empty()) {
imgui.text(m_title);
} else {
ImGui::PushStyleColor(ImGuiCol_Text, ImGuiWrapper::to_ImVec4(ColorRGBA(0.91f, 0.42f, 0.42f, 1.0f)));
imgui.text(m_err);
ImGui::PopStyleColor();
}
}
if (m_focus_pending) {
ImGui::SetKeyboardFocusHere();
m_focus_pending = false;
}
ImGui::PushItemWidth(90.0f * scale);
// EnterReturnsTrue so Enter commits from inside the widget; AutoSelectAll so the prefill is
// replaced by the first digit typed, which is what "pre-selected" meant when this was a
// wxTextCtrl and is what makes typing a value a single gesture.
const bool entered = ImGui::InputText("##sketchvalue_in", m_buf, sizeof(m_buf),
ImGuiInputTextFlags_EnterReturnsTrue
| ImGuiInputTextFlags_AutoSelectAll
| ImGuiInputTextFlags_CharsDecimal);
// MEASUREMENT, not a fix: one line per frame saying whether ImGui believes it owns the
// keyboard and whether our widget is the active one. "Typing does not arrive" has two very
// different causes — no FRAMES (this canvas repaints on demand only, so an idle canvas never
// processes ImGui's queued characters) versus frames that run while the input is not active —
// and they are indistinguishable from outside.
if (std::getenv("ORCA_CAD_UXTRACE")) {
const ImGuiIO& io = ImGui::GetIO();
std::fprintf(stderr, "[UX] frame title=%s want_text=%d want_kb=%d active=%d buf=%s\n",
m_title.c_str(), (int) io.WantTextInput, (int) io.WantCaptureKeyboard,
(int) ImGui::IsItemActive(), m_buf);
std::fflush(stderr);
}
ImGui::PopItemWidth();
imgui.end();
ImGui::PopStyleVar();
ImGuiWrapper::pop_common_window_style();
// Keep the frames coming while the field is up — see request_frame's note in the header.
if (m_open && request_frame)
request_frame();
// Act AFTER end(): do_commit can reopen the field for the next queued dimension, and that
// must not happen inside this frame's window.
if (entered)
do_commit();
else if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Escape)))
do_cancel();
return true;
}
}} // namespace Slic3r::GUI
+95
View File
@@ -0,0 +1,95 @@
#ifndef slic3r_SketchInlineEditor_hpp_
#define slic3r_SketchInlineEditor_hpp_
#include <functional>
#include <string>
#include <wx/gdicmn.h>
namespace Slic3r {
namespace GUI {
class ImGuiWrapper;
// Onshape-style in-canvas value editor.
//
// IT IS NOT A WINDOW. It used to be a borderless top-level wxFrame holding a wxTextCtrl, and
// that is the whole history of this file: a separate top-level window can only receive typing
// if the window manager grants it focus, and whether it does is not ours to decide. openbox
// grants it; mutter's focus-stealing prevention refuses it, so on a GNOME desktop the field
// appeared, showed its value selected, and silently ignored every keystroke — Enter then
// committed the number it opened with. Seven workarounds were tried against that (a real X11
// server timestamp for gtk_window_present, re-asserted SetFocus, dropping the _UTILITY hint,
// keeping the frame mapped between two queued fields, forwarding keys from the panel's
// CHAR_HOOK), one of them caused a macOS regression, and the test harness ended up clicking the
// field before typing — which is the workaround a user cannot be asked to perform, and is
// exactly the "label value not editable" report.
//
// So the field stops asking. It is now drawn INSIDE the GL canvas as an ImGui overlay, at the
// same screen point as before, and its keys arrive through the canvas's own key events, which
// GLCanvas3D already feeds to ImGui (see GLCanvas3D::on_key / on_char -> update_key_data). The
// canvas is part of the main window and already has focus, so there is no second window, no
// second focus, and no window manager in the path. The dimension labels next to it are already
// ImGui overlays (DesignSketchTool::draw_dim_label), so this is the same vocabulary, not a new
// one.
//
// Ownership: DesignCanvas owns it; DesignSketchTool::render() calls render() once per frame.
class SketchInlineEditor
{
public:
SketchInlineEditor() = default;
// Open the field anchored at `canvas_px` (canvas DEVICE pixels, the coordinate space the
// sketch tool works in), pre-filled with `value` and pre-selected. on_commit(parsed) fires
// on Enter with a valid number; on_cancel() on Esc.
void open(const wxPoint& canvas_px, double value, const std::string& title,
std::function<void(double)> on_commit,
std::function<void()> on_cancel);
void close(); // drop it with neither callback
void cancel(); // if open, run the registered cancel (keep-as-drawn)
void commit(); // if open, run the registered commit (accept the typed value)
bool is_open() const { return m_open; }
// Draw it, and let ImGui do the editing. Called from DesignSketchTool::render() inside the
// frame's ImGui pass; `scale` is the tool's m_render_scale. Returns true if it drew.
bool render(ImGuiWrapper& imgui, float scale);
// Ask for another frame. THE FIELD DOES NOT WORK WITHOUT THIS, and the reason is a deadlock
// that only a per-frame trace shows:
//
// [UX] frame want_text=0 want_kb=0 active=0 <- frame 1: the widget is not active yet
// [UX] frame want_text=0 want_kb=0 active=1 <- frame 2: it is now
// (nothing further) <- the canvas has nothing to redraw, so it stops
//
// This canvas repaints ON DEMAND. ImGui decides whether it wants the keyboard at the END of a
// frame, from the active item, and GLCanvas3D::on_char only calls render() when
// update_key_data() says ImGui wants it. No frames -> WantTextInput never turns on -> no
// render on a keystroke -> still no frames. The characters sit in ImGui's queue and the field
// looks exactly as deaf as the window it replaced. One repaint per frame while it is open
// breaks the circle.
std::function<void()> request_frame;
// Kept because callers ask them, but there is no longer any difference to report: with no
// window there is no state where the field is on screen but logically closed, and no state
// where it is open but somebody else holds the keyboard.
bool is_mapped() const { return m_open; }
bool has_focus() const { return m_open; }
void dismiss() { close(); }
private:
void do_commit();
void do_cancel();
std::function<void(double)> m_commit;
std::function<void()> m_cancel;
bool m_open{false};
bool m_focus_pending{false}; // one frame of SetKeyboardFocusHere after opening
wxPoint m_anchor{0, 0}; // canvas device px
std::string m_title;
std::string m_err; // why the last value was refused, shown in the title line
char m_buf[64]{}; // the edited text; ImGui::InputText writes into it
};
}} // namespace Slic3r::GUI
#endif // slic3r_SketchInlineEditor_hpp_
+3 -2
View File
@@ -395,8 +395,9 @@ bool confirm_create_decompose_missing_components(wxWindow* parent, const std::ve
missing_text += missing[i].display_name;
}
wxString message = _L("The current filament list does not contain ") + missing_text +
_L(". A project filament required by the mixed filament will be created automatically after decomposition.");
wxString message = wxString::Format(_L("The current filament list does not contain %s. A project filament required by "
"the mixed filament will be created automatically after decomposition."),
missing_text);
MessageDialog dlg(parent, message, _L("Tip"), wxOK | wxCANCEL | wxICON_INFORMATION);
dlg.show_dsa_button();
+9 -2
View File
@@ -1041,10 +1041,12 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
for (auto el : {"wipe_tower_rotation_angle", "wipe_tower_cone_angle",
"wipe_tower_extra_spacing", "wipe_tower_max_purge_speed",
"wipe_tower_bridging", "wipe_tower_extra_flow",
"wipe_tower_no_sparse_layers"})
"wipe_tower_bridging", "wipe_tower_extra_flow"})
toggle_line(el, have_prime_tower && supports_wipe_tower_2);
// Orca: both tower generators skip sparse layers, so this is not a wipe tower 2 exclusive.
toggle_line("wipe_tower_no_sparse_layers", have_prime_tower);
WipeTowerWallType wipe_tower_wall_type = config->opt_enum<WipeTowerWallType>("wipe_tower_wall_type");
bool have_rib_wall = (wipe_tower_wall_type == WipeTowerWallType::wtwRib)&&have_prime_tower;
toggle_line("wipe_tower_cone_angle", have_prime_tower && supports_wipe_tower_2 && wipe_tower_wall_type == WipeTowerWallType::wtwCone);
@@ -1055,6 +1057,10 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
toggle_line("single_extruder_multi_material_priming", !bSEMM && have_prime_tower && supports_wipe_tower_2);
bool use_cyclic_ordering = config->opt_enum<ToolChangeOrderingType>("toolchange_ordering") == ToolChangeOrderingType::Cyclic;
toggle_line("toolchange_cyclic_order", use_cyclic_ordering);
toggle_line("toolchange_cyclic_first_layer", use_cyclic_ordering);
toggle_line("prime_volume",have_prime_tower && (!purge_in_primetower || !bSEMM));
for (auto el : {"flush_into_infill", "flush_into_support", "flush_into_objects"})
@@ -1128,6 +1134,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
bool has_detect_overhang_wall = config->opt_bool("detect_overhang_wall");
bool has_overhang_reverse = config->opt_bool("overhang_reverse");
bool allow_overhang_reverse = !has_spiral_vase;
toggle_line("unsupported_wall_last", has_detect_overhang_wall);
toggle_line("overhang_reverse", allow_overhang_reverse);
toggle_line("overhang_reverse_internal_only", allow_overhang_reverse && has_overhang_reverse);
bool has_overhang_reverse_internal_only = config->opt_bool("overhang_reverse_internal_only");
+11
View File
@@ -188,6 +188,17 @@ wxString get_string_value(const std::string& opt_key, const DynamicPrintConfig&
out = double_to_string(opt->value) + (opt->percent ? "%" : "");
return out;
}
case coFloatsOrPercents: {
const auto* values = static_cast<const ConfigOptionVector<FloatOrPercent>*>(option);
// Orca: Preset comparison may request the entire vector instead of an indexed entry.
if (orig_opt_idx < 0)
return from_u8(option->serialize());
if (opt_idx < values->size()) {
const FloatOrPercent& value = values->get_at(opt_idx);
return double_to_string(value.value) + (value.percent ? "%" : "");
}
return _L("Undefined");
}
case coEnum: {
return get_string_from_enum(pure_key, config,
pure_key == "top_surface_pattern" ||
+37 -17
View File
@@ -17,6 +17,7 @@
#include "Plater.hpp"
#include "Camera.hpp"
#include "I18N.hpp"
#include "format.hpp"
#include "GUI_Utils.hpp"
#include "GUI.hpp"
#include "GLCanvas3D.hpp"
@@ -996,13 +997,17 @@ void GCodeViewer::SequentialView::GCodeWindow::stop_mapping_file()
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": finished mapping file " << m_filename;
}
}
void GCodeViewer::SequentialView::render(const bool has_render_path, float legend_height, const libvgcode::Viewer* viewer, uint32_t gcode_id, int canvas_width, int canvas_height, int right_margin, const libvgcode::EViewType& view_type)
void GCodeViewer::SequentialView::render_marker(const bool has_render_path, int canvas_width, int canvas_height, const libvgcode::EViewType& view_type)
{
if (has_render_path && m_show_marker) {
if (has_render_path && m_show_marker)
// marker.set_world_offset(current_offset);
marker.render(canvas_width, canvas_height, view_type);
}
void GCodeViewer::SequentialView::render_overlay(const bool has_render_path, float legend_height, const libvgcode::Viewer* viewer, uint32_t gcode_id, int canvas_width, int canvas_height, int right_margin, const libvgcode::EViewType& view_type)
{
if (has_render_path && m_show_marker)
marker.render_position_window(viewer, canvas_width, canvas_height, view_type);
}
//float bottom = wxGetApp().plater()->get_current_canvas3D()->get_canvas_size().get_height();
// BBS
@@ -1617,7 +1622,7 @@ void GCodeViewer::reset()
}
//BBS: GUI refactor: add canvas width and height
void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
void GCodeViewer::render_scene(int canvas_width, int canvas_height)
{
glsafe(::glEnable(GL_DEPTH_TEST));
render_shells(canvas_width, canvas_height);
@@ -1627,6 +1632,20 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
render_toolpaths();
auto current = m_viewer.get_view_visible_range();
auto endpoints = m_viewer.get_view_full_range();
m_sequential_view.m_show_marker = m_sequential_view.m_show_marker || (current.back() != endpoints.back() && !m_no_render_path);
const libvgcode::PathVertex& curr_vertex = m_viewer.get_current_vertex();
m_sequential_view.marker.set_world_position(libvgcode::convert(curr_vertex.position));
m_sequential_view.marker.set_z_offset(m_z_offset + 0.5f);
m_sequential_view.render_marker(!m_no_render_path, canvas_width, sequential_view_height(canvas_height), m_viewer.get_view_type());
}
void GCodeViewer::render_overlay(int canvas_width, int canvas_height, int right_margin)
{
if (m_viewer.get_extrusion_roles().empty())
return;
float legend_height = 0.0f;
render_legend(legend_height, canvas_width, canvas_height, right_margin);
@@ -1635,16 +1654,7 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
m_user_mode = wxGetApp().get_mode();
}
//BBS fixed bottom_margin for space to render horiz slider
int bottom_margin = SLIDER_BOTTOM_MARGIN * GCODE_VIEWER_SLIDER_SCALE;
auto current = m_viewer.get_view_visible_range();
auto endpoints = m_viewer.get_view_full_range();
m_sequential_view.m_show_marker = m_sequential_view.m_show_marker || (current.back() != endpoints.back() && !m_no_render_path);
const libvgcode::PathVertex& curr_vertex = m_viewer.get_current_vertex();
m_sequential_view.marker.set_world_position(libvgcode::convert(curr_vertex.position));
m_sequential_view.marker.set_z_offset(m_z_offset + 0.5f);
// BBS fixed buttom margin. m_moves_slider.pos_y
m_sequential_view.render(!m_no_render_path, legend_height, &m_viewer, m_viewer.get_current_vertex().gcode_id, canvas_width, canvas_height - bottom_margin * m_scale, right_margin * m_scale, m_viewer.get_view_type());
m_sequential_view.render_overlay(!m_no_render_path, legend_height, &m_viewer, m_viewer.get_current_vertex().gcode_id, canvas_width, sequential_view_height(canvas_height), right_margin * m_scale, m_viewer.get_view_type());
#if VGCODE_ENABLE_COG_AND_TOOL_MARKERS
if (is_legend_shown()) {
@@ -1685,6 +1695,14 @@ void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
render_slider(canvas_width, canvas_height);
}
int GCodeViewer::sequential_view_height(int canvas_height) const
{
//BBS fixed bottom_margin for space to render horiz slider
const int bottom_margin = SLIDER_BOTTOM_MARGIN * GCODE_VIEWER_SLIDER_SCALE;
// BBS fixed buttom margin. m_moves_slider.pos_y
return canvas_height - bottom_margin * m_scale;
}
#define ENABLE_CALIBRATION_THUMBNAIL_OUTPUT 0
#if ENABLE_CALIBRATION_THUMBNAIL_OUTPUT
static void debug_calibration_output_thumbnail(const ThumbnailData& thumbnail_data)
@@ -3436,16 +3454,18 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
return ret;
};
// Whole sentences: the bare "up to"/"above"/"from"/"to" these used to be glued from gave a
// translator no context, and left the unit and the numbers stuck in English word order.
auto upto_label = [](double z) {
char buf[64];
::sprintf(buf, "%.2f", z);
return _u8L("up to") + " " + std::string(buf) + " " + _u8L("mm");
return format(_u8L("up to %1% mm"), buf);
};
auto above_label = [](double z) {
char buf[64];
::sprintf(buf, "%.2f", z);
return _u8L("above") + " " + std::string(buf) + " " + _u8L("mm");
return format(_u8L("above %1% mm"), buf);
};
auto fromto_label = [](double z1, double z2) {
@@ -3453,7 +3473,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
::sprintf(buf1, "%.2f", z1);
char buf2[64];
::sprintf(buf2, "%.2f", z2);
return _u8L("from") + " " + std::string(buf1) + " " + _u8L("to") + " " + std::string(buf2) + " " + _u8L("mm");
return format(_u8L("from %1% to %2% mm"), buf1, buf2);
};
auto role_time_and_percent = [this, total_estimated_time](libvgcode::EGCodeExtrusionRole role) {
+10 -2
View File
@@ -153,7 +153,10 @@ public:
GCodeWindow gcode_window;
float m_scale = 1.0;
bool m_show_marker = false;
void render(const bool has_render_path, float legend_height, const libvgcode::Viewer* viewer, uint32_t gcode_id, int canvas_width, int canvas_height, int right_margin, const libvgcode::EViewType& view_type);
// The tool marker at the current move, drawn in 3D.
void render_marker(const bool has_render_path, int canvas_width, int canvas_height, const libvgcode::EViewType& view_type);
// The marker's position window and the G-code window, both ImGui.
void render_overlay(const bool has_render_path, float legend_height, const libvgcode::Viewer* viewer, uint32_t gcode_id, int canvas_width, int canvas_height, int right_margin, const libvgcode::EViewType& view_type);
};
struct ExtruderFilament
{
@@ -272,7 +275,10 @@ public:
//BBS: add all plates filament statistics
void render_all_plates_stats(const std::vector<const GCodeProcessorResult*>& gcode_result_list, bool show = true) const;
//BBS: GUI refactor: add canvas width and height
void render(int canvas_width, int canvas_height, int right_margin);
// Shells, toolpaths and the sequential marker, drawn in 3D.
void render_scene(int canvas_width, int canvas_height);
// Legend, sliders, the marker's position window and the G-code window, all ImGui.
void render_overlay(int canvas_width, int canvas_height, int right_margin);
//BBS
// void _render_calibration_thumbnail_internal(ThumbnailData& thumbnail_data, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager);
// void _render_calibration_thumbnail_framebuffer(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager);
@@ -362,6 +368,8 @@ public:
private:
//BBS: always load shell at preview
//void load_shells(const Print& print);
// Canvas height minus the room the horizontal slider takes.
int sequential_view_height(int canvas_height) const;
void render_toolpaths();
void render_shells(int canvas_width, int canvas_height);
File diff suppressed because it is too large Load Diff
+82 -2
View File
@@ -5,6 +5,7 @@
#include <memory>
#include <chrono>
#include <cstdint>
#include <optional>
#include "GLToolbar.hpp"
#include "Event.hpp"
@@ -17,6 +18,7 @@
#include "GCodeViewer.hpp"
#include "Camera.hpp"
#include "SceneRaycaster.hpp"
#include "SceneCache.hpp"
#include "IMToolbar.hpp"
#include "slic3r/GUI/3DBed.hpp"
#include "libslic3r/Slicing.hpp"
@@ -33,6 +35,7 @@ class wxTimerEvent;
class wxPaintEvent;
class wxGLCanvas;
class wxGLContext;
struct ImDrawData;
// Support for Retina OpenGL on Mac OS.
// wxGTK3 seems to simulate OSX behavior in regard to HiDPI scaling support, enable it as well.
@@ -57,6 +60,9 @@ namespace GUI {
class Bed3D;
class PartPlateList;
#ifdef SLIC3R_CAD
class DesignSketchTool; // Design tab: interactive 2D sketch tool
#endif
#if ENABLE_RETINA_GL
class RetinaHelper;
@@ -166,7 +172,6 @@ wxDECLARE_EVENT(EVT_GLCANVAS_ORIENT_PARTPLATE, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_SELECT_CURR_PLATE_ALL, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_SELECT_ALL, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_QUESTION_MARK, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_OPEN_SPEED_DIAL, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event<int>); // data: +1 => increase, -1 => decrease
wxDECLARE_EVENT(EVT_GLCANVAS_INSTANCE_MOVED, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_FORCE_UPDATE, SimpleEvent);
@@ -400,16 +405,23 @@ class GLCanvas3D
std::chrono::time_point<std::chrono::high_resolution_clock> m_measuring_start;
int m_fps_out = -1;
int m_fps_running = 0;
// Frames that redrew the 3D scene rather than reusing the cached one.
int m_scene_fps_out = 0;
int m_scene_fps_running = 0;
public:
void increment_fps_counter() { ++m_fps_running; }
void increment_scene_fps_counter() { ++m_scene_fps_running; }
int get_fps() { return m_fps_out; }
int get_scene_fps() const { return m_scene_fps_out; }
int get_fps_and_reset_if_needed() {
auto cur_time = std::chrono::high_resolution_clock::now();
int elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(cur_time-m_measuring_start).count();
if (elapsed_ms > 1000 || m_fps_out == -1) {
m_measuring_start = cur_time;
m_fps_out = int (1000. * m_fps_running / elapsed_ms);
m_scene_fps_out = int (1000. * m_scene_fps_running / elapsed_ms);
m_fps_running = 0;
m_scene_fps_running = 0;
}
return m_fps_out;
}
@@ -531,6 +543,10 @@ private:
bool m_in_render;
wxTimer m_timer;
wxTimer m_timer_set_color;
// Armed by each frame that draws the FPS overlay; its tick requests an overlay-only frame.
wxTimer m_fps_overlay_timer;
// True during the frame the timer requested, which is not counted.
bool m_fps_overlay_tick{ false };
LayersEditing m_layers_editing;
Mouse m_mouse;
GLGizmosManager m_gizmos;
@@ -543,6 +559,27 @@ private:
mutable Vec2i32 m_canvas_toolbar_pos = {140, 5};
mutable float m_sc{1};
mutable float m_paint_toolbar_width;
bool m_collapse_toolbar_enabled{true};
bool m_plate_chrome_enabled{true};
// Design tab: render the world-axis triad at the bed centre (= modeling origin) instead of
// the bed corner. Default false preserves the main editor's corner triad.
bool m_axes_at_bed_center{false};
// Design tab: draw the printer bed and its plate grid at all. Default true, so the
// main editor is untouched; the Design tab lets the user hide it to model without a bed.
bool m_show_bed{true};
// Design tab: CAD grid drawn on the bed plane in place of the plate's corner-origin grid.
// Two GLModels (10 mm minor / 50 mm major) generated from the bed centre so a line passes
// exactly through the modeling origin; built once and rebuilt only when the bed shape changes.
GLModel m_cad_grid_minor;
GLModel m_cad_grid_major;
// Geometry the CAD grid models were last built from, so they are rebuilt on bed-shape change
// rather than every frame.
BoundingBoxf m_cad_grid_bb;
Vec2d m_cad_grid_center;
bool m_cad_grid_valid{false};
#ifdef SLIC3R_CAD
DesignSketchTool* m_design_sketch_tool{nullptr};
#endif
//BBS: add canvas type for assemble view usage
ECanvasType m_canvas_type;
@@ -570,9 +607,15 @@ private:
std::array<unsigned int, 2> m_old_size{ 0, 0 };
bool m_is_touchpad_navigation{ false };
// CAD navigation (Design tab only): left-drag is a selection rubber band, so orbit moves
// to middle-drag and pan to right-drag — the Onshape/SolidWorks mapping. Off everywhere
// else, so Prepare/Preview keep the mouse the user already learned.
bool m_cad_navigation{ false };
// Screen is only refreshed from the OnIdle handler if it is dirty.
bool m_dirty;
// A frame is needed, and only for the overlay.
bool m_overlay_dirty{ false };
bool m_initialized;
//BBS: add flag to controll rendering
bool m_render_preview{ true };
@@ -728,6 +771,11 @@ public:
unsigned int m_ssao_color_texture_id{ 0 };
unsigned int m_ssao_depth_texture_id{ 0 };
std::array<unsigned int, 2> m_ssao_texture_size{ { 0, 0 } };
// The last scene pass, for frames that only rebuild the overlay.
SceneCache m_scene_cache;
// Signature of the overlay on screen; empty after render(), a paint request or a frame drawn but
// not shown, so the next frame is presented regardless.
std::optional<size_t> m_presented_signature;
GLModel m_plate_shadow_mask;
std::string m_plate_shadow_mask_key;
// Depth-based shadow map used to cast object shadows onto other objects and themselves.
@@ -883,6 +931,15 @@ public:
void enable_assemble_view_toolbar(bool enable);
void enable_return_toolbar(bool enable);
void enable_separator_toolbar(bool enable);
void enable_collapse_toolbar(bool enable);
void enable_plate_chrome(bool enable);
void set_axes_at_bed_center(bool b) { m_axes_at_bed_center = b; }
void set_show_bed(bool b) { m_show_bed = b; }
bool get_show_bed() const { return m_show_bed; }
#ifdef SLIC3R_CAD
void set_design_sketch_tool(DesignSketchTool* tool) { m_design_sketch_tool = tool; }
DesignSketchTool* get_design_sketch_tool() const { return m_design_sketch_tool; }
#endif
void enable_dynamic_background(bool enable) { m_dynamic_background_enabled = enable; }
void enable_labels(bool enable) { m_labels.enable(enable); }
void enable_slope(bool enable) { m_slope.enable(enable); }
@@ -1041,6 +1098,7 @@ public:
void on_timer(wxTimerEvent& evt);
void on_render_timer(wxTimerEvent& evt);
void on_set_color_timer(wxTimerEvent& evt);
void on_fps_overlay_timer(wxTimerEvent& evt);
void on_mouse(wxMouseEvent& evt);
void on_gesture(wxGestureEvent& evt);
void on_paint(wxPaintEvent& evt);
@@ -1052,6 +1110,7 @@ public:
bool clicked_button_matches_action(const wxMouseEvent& evt, MouseAction action, const std::map<MouseButton, MouseAction>& mappings) const;
bool is_camera_rotate(const wxMouseEvent& evt, const std::map<MouseButton, MouseAction>& mappings) const;
bool is_camera_pan(const wxMouseEvent& evt, const std::map<MouseButton, MouseAction>& mappings) const;
void set_cad_navigation(bool b) { m_cad_navigation = b; }
Size get_canvas_size() const;
Vec2d get_local_mouse_position() const;
@@ -1191,6 +1250,8 @@ public:
bool can_sequential_clearance_show_in_gizmo();
void update_sequential_clearance();
// Orca: by-layer counterpart, for a prime tower compacted by "No sparse layers".
void update_compacted_wipe_tower_clearance();
const Print* fff_print() const;
const SLAPrint* sla_print() const;
@@ -1235,7 +1296,7 @@ private:
void _zoom_to_box(const BoundingBoxf3& box, double margin_factor = DefaultCameraZoomToBoxMarginFactor);
void _update_camera_zoom(double zoom);
void _refresh_if_shown_on_screen();
void _refresh_if_shown_on_screen(bool scene_dirty = true);
void _picking_pass();
void _rectangular_selection_picking_pass();
@@ -1243,9 +1304,21 @@ private:
bool _is_ssao_enabled() const;
int _get_effective_fps_cap() const;
bool _is_fps_overlay_enabled() const;
bool _is_scene_cache_enabled() const;
bool _is_scene_cacheable() const;
bool _is_frame_skipping_enabled() const;
void _render_fps_overlay(int fps) const;
void _render_fxaa_pass(unsigned int width, unsigned int height);
void _render_ssao_pass(unsigned int width, unsigned int height);
// scene_dirty is false only for a frame that its requester knows to be overlay-only.
void _render_frame(bool scene_dirty, bool only_init = false);
void _render_scene(const Camera& camera, const Size& cnv_size);
// Request a frame that only rebuilds the overlay.
void _set_overlay_as_dirty() { m_overlay_dirty = true; }
// These read the hover state _picking_pass() sets.
SceneCache::Key _scene_cache_key(const Camera& camera) const;
bool _can_reuse_cached_scene(const Camera& camera) const;
void _capture_scene_cache(const Camera& camera);
void _render_background();
void _render_bed(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool show_axes);
// Build the light-space depth shadow map (consumed by gouraud/phong for object & self shadows)
@@ -1253,11 +1326,16 @@ private:
void _render_shadows(const Transform3d& view_matrix, const Transform3d& projection_matrix);
//BBS: add part plate related logic
void _render_platelist(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true);
// Design tab: draw the CAD grid (minor 10 mm + major 50 mm) in place of the plate's
// corner-origin grid when the axes sit at the bed centre (modeling origin). Rebuilds its
// GLModels lazily, only when the bed shape changed.
void _render_cad_grid(const Transform3d& view_matrix, const Transform3d& projection_matrix);
//BBS: add outline drawing logic
void _render_objects(GLVolumeCollection::ERenderType type, bool with_outline = true);
void _render_wireframe_overlay();
//BBS: GUI refactor: add canvas size as parameters
void _render_gcode(int canvas_width, int canvas_height);
void _render_gcode_overlay(int canvas_width, int canvas_height);
//BBS: render a plane for assemble
void _render_plane() const;
void _render_selection();
@@ -1267,6 +1345,8 @@ private:
#endif // ENABLE_RENDER_SELECTION_CENTER
void _check_and_update_toolbar_icon_scale();
void _render_overlays();
void _render_overlay_toolbars();
size_t _overlay_signature(const ImDrawData* draw_data) const;
void _render_style_editor();
void _render_volumes_for_picking(const Camera& camera) const;
void _render_current_gizmo() const;
+23
View File
@@ -707,6 +707,29 @@ void GLTexture::render_sub_texture(unsigned int tex_id, float left, float right,
glsafe(::glDisable(GL_BLEND));
}
void GLTexture::copy_from_framebuffer(unsigned int& tex_id, std::array<unsigned int, 2>& tex_size, unsigned int width, unsigned int height, int filter)
{
if (tex_id == 0) {
glsafe(::glGenTextures(1, &tex_id));
glsafe(::glBindTexture(GL_TEXTURE_2D, tex_id));
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter));
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter));
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE));
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE));
}
else
glsafe(::glBindTexture(GL_TEXTURE_2D, tex_id));
if (tex_size[0] != width || tex_size[1] != height) {
glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr));
tex_size = { width, height };
}
// Copying from the default framebuffer resolves its multisampling.
glsafe(::glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, width, height));
glsafe(::glBindTexture(GL_TEXTURE_2D, 0));
}
static bool to_squared_power_of_two(const std::string& filename, int max_size_px, int& w, int& h)
{
auto is_power_of_two = [](int v) { return v != 0 && (v & (v - 1)) == 0; };
+4
View File
@@ -4,6 +4,7 @@
#include <atomic>
#include <string>
#include <vector>
#include <array>
#include <thread>
#include <wx/colour.h>
@@ -132,6 +133,9 @@ namespace GUI {
static void render_texture(unsigned int tex_id, float left, float right, float bottom, float top);
static void render_sub_texture(unsigned int tex_id, float left, float right, float bottom, float top, const Quad_UVs& uvs);
// Copies the bound read framebuffer into an RGBA texture, creating it on first use and
// reallocating it when the size changes.
static void copy_from_framebuffer(unsigned int& tex_id, std::array<unsigned int, 2>& tex_size, unsigned int width, unsigned int height, int filter);
private:
bool load_from_png(const std::string& filename, bool use_mipmaps, ECompressionType compression_type, bool apply_anisotropy);
+47 -15
View File
@@ -8,6 +8,8 @@
#include "slic3r/GUI/Camera.hpp"
#include "slic3r/GUI/Plater.hpp"
#include <boost/functional/hash.hpp>
#include <wx/event.h>
#include <wx/bitmap.h>
#include <wx/dcmemory.h>
@@ -199,7 +201,10 @@ void GLToolbarItem::render(unsigned int tex_id, float left, float right, float b
};
GLTexture::render_sub_texture(tex_id, left, right, bottom, top, uvs(tex_width, tex_height, icon_size));
}
void GLToolbarItem::render_window(float left, float right, float bottom, float top) const
{
if (is_pressed())
{
if ((m_last_action_type == Left) && m_data.left.can_render())
@@ -215,13 +220,6 @@ void GLToolbarItem::render_image(unsigned int tex_id, float left, float right, f
//GLTexture::Quad_UVs image_uvs = { { 0.0f, 1.0f }, { 1.0f, 1.0f }, { 1.0f, 0.0f }, { 0.0f, 0.0f } };
GLTexture::render_sub_texture(tex_id, left, right, bottom, top, image_uvs);
if (is_pressed()) {
if ((m_last_action_type == Left) && m_data.left.can_render())
m_data.left.render_callback(left, right, bottom, top);
else if ((m_last_action_type == Right) && m_data.right.can_render())
m_data.right.render_callback(left, right, bottom, top);
}
}
BackgroundTexture::Metadata::Metadata()
@@ -544,11 +542,35 @@ void GLToolbar::render(const GLCanvas3D& parent,GLToolbarItem::EType type)
switch (m_layout.type)
{
default:
case Layout::Horizontal: { render_horizontal(parent,type); break; }
case Layout::Vertical: { render_vertical(parent); break; }
case Layout::Horizontal: { render_horizontal(parent, type, true); break; }
case Layout::Vertical: { render_vertical(parent, true); break; }
}
}
void GLToolbar::render_item_windows(const GLCanvas3D& parent)
{
if (!m_enabled || m_items.empty())
return;
switch (m_layout.type)
{
default:
case Layout::Horizontal: { render_horizontal(parent, GLToolbarItem::Action, false); break; }
case Layout::Vertical: { render_vertical(parent, false); break; }
}
}
size_t GLToolbar::get_state_hash() const
{
size_t hash = 0;
boost::hash_combine(hash, m_enabled);
for (const GLToolbarItem* item : m_items) {
boost::hash_combine(hash, (int)item->get_state());
boost::hash_combine(hash, item->is_visible());
}
return hash;
}
bool GLToolbar::on_mouse(wxMouseEvent& evt, GLCanvas3D& parent)
{
if (!m_enabled)
@@ -1354,7 +1376,7 @@ void GLToolbar::render_arrow(const GLCanvas3D& parent, GLToolbarItem* highlighte
}
}
void GLToolbar::render_horizontal(const GLCanvas3D& parent,GLToolbarItem::EType type)
void GLToolbar::render_horizontal(const GLCanvas3D& parent, GLToolbarItem::EType type, bool draw_icons)
{
const Size cnv_size = parent.get_canvas_size();
const float cnv_w = (float)cnv_size.get_width();
@@ -1385,7 +1407,8 @@ void GLToolbar::render_horizontal(const GLCanvas3D& parent,GLToolbarItem::EType
right = left + width * 0.5;
const float bottom = top - height;
render_background(left, top, right, bottom, border_w, border_h);
if (draw_icons)
render_background(left, top, right, bottom, border_w, border_h);
left += border_w;
top -= border_h;
@@ -1400,7 +1423,9 @@ void GLToolbar::render_horizontal(const GLCanvas3D& parent,GLToolbarItem::EType
else {
//BBS GUI refactor
item->render_left_pos = left;
if (!item->is_action_with_text_image()) {
if (!draw_icons)
item->render_window(left, left + icons_size_x, top - icons_size_y, top);
else if (!item->is_action_with_text_image()) {
unsigned int tex_id = m_icons_texture.get_id();
int tex_width = m_icons_texture.get_width();
int tex_height = m_icons_texture.get_height();
@@ -1412,7 +1437,8 @@ void GLToolbar::render_horizontal(const GLCanvas3D& parent,GLToolbarItem::EType
if (item->is_action_with_text())
{
float scaled_text_size = item->get_extra_size_ratio() * icons_size_x;
item->render_text(left + icons_size_x, left + icons_size_x + scaled_text_size, top - icons_size_y, top);
if (draw_icons)
item->render_text(left + icons_size_x, left + icons_size_x + scaled_text_size, top - icons_size_y, top);
left += scaled_text_size;
}
left += icon_stride;
@@ -1420,7 +1446,7 @@ void GLToolbar::render_horizontal(const GLCanvas3D& parent,GLToolbarItem::EType
}
}
void GLToolbar::render_vertical(const GLCanvas3D& parent)
void GLToolbar::render_vertical(const GLCanvas3D& parent, bool draw_icons)
{
const Size cnv_size = parent.get_canvas_size();
const float cnv_w = (float)cnv_size.get_width();
@@ -1449,7 +1475,8 @@ void GLToolbar::render_vertical(const GLCanvas3D& parent)
const float right = left + width;
const float bottom = top - height;
render_background(left, top, right, bottom, border_w, border_h);
if (draw_icons)
render_background(left, top, right, bottom, border_w, border_h);
left += border_w;
top -= border_h;
@@ -1462,6 +1489,11 @@ void GLToolbar::render_vertical(const GLCanvas3D& parent)
if (item->is_separator())
top -= separator_stride;
else {
if (!draw_icons) {
item->render_window(left, left + icons_size_x, top - icons_size_y, top);
top -= icon_stride;
continue;
}
unsigned int tex_id;
int tex_width, tex_height;
if (item->is_action_with_text_image()) {
+8 -2
View File
@@ -231,6 +231,8 @@ public:
int generate_image_texture();
void render(unsigned int tex_id, float left, float right, float bottom, float top, unsigned int tex_width, unsigned int tex_height, unsigned int icon_size) const;
// The ImGui window a pressed item shows, given the icon's rectangle.
void render_window(float left, float right, float bottom, float top) const;
void render_image(unsigned int tex_id, float left, float right, float bottom, float top, unsigned int tex_width, unsigned int tex_height, unsigned int icon_size) const;
private:
void set_visible(bool visible) { m_data.visible = visible; }
@@ -410,6 +412,10 @@ public:
bool update_items_state();
void render(const GLCanvas3D& parent,GLToolbarItem::EType type = GLToolbarItem::Action);
// The ImGui windows of pressed items, built with the same layout as render().
void render_item_windows(const GLCanvas3D& parent);
// Hash of the state render() draws from: enabled, and each item's state and visibility.
size_t get_state_hash() const;
void render_arrow(const GLCanvas3D& parent, GLToolbarItem* highlighted_item);
bool on_mouse(wxMouseEvent& evt, GLCanvas3D& parent);
@@ -438,8 +444,8 @@ private:
int contains_mouse_vertical(const Vec2d& mouse_pos, const GLCanvas3D& parent) const;
void render_background(float left, float top, float right, float bottom, float border_w, float border_h) const;
void render_horizontal(const GLCanvas3D &parent, GLToolbarItem::EType type);
void render_vertical(const GLCanvas3D& parent);
void render_horizontal(const GLCanvas3D &parent, GLToolbarItem::EType type, bool draw_icons);
void render_vertical(const GLCanvas3D& parent, bool draw_icons);
bool generate_icons_texture();
+100 -10
View File
@@ -2608,6 +2608,10 @@ void GUI_App::init_app_config()
}
#endif // _WIN32
}
// Speed Dial opens on a bare Space from any page by default. Seed the flag so Preferences and the
// MainFrame shortcut read the same value; an existing config (true or false) is left untouched.
if (app_config->get("enable_speed_dial").empty())
app_config->set_bool("enable_speed_dial", true);
set_logging_level(Slic3r::level_string_to_boost(app_config->get("log_severity_level")));
}
@@ -4586,6 +4590,12 @@ void GUI_App::recreate_GUI(const wxString &msg_name)
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "recreate_GUI enter";
m_is_recreating_gui = true;
// The palette injects its translated strings once, at creation; drop the cached dialog so the
// next open rebuilds it in the current locale (and can't outlive the old mainframe).
if (m_speed_dial_dialog) {
m_speed_dial_dialog->Destroy();
m_speed_dial_dialog = nullptr;
}
mainframe->shutdown();
ProgressDialog dlg(msg_name, msg_name, 100, nullptr, wxPD_AUTO_HIDE);
@@ -8156,6 +8166,22 @@ void GUI_App::save_mode(const /*ConfigOptionMode*/int mode)
update_mode();
}
void GUI_App::set_mode(ConfigOptionMode mode)
{
const bool was_developer = app_config->get_bool("developer_mode");
if (was_developer)
app_config->set_bool("developer_mode", false);
save_mode(mode);
if (was_developer)
app_config->save();
}
void GUI_App::enable_developer_mode()
{
app_config->set_bool("developer_mode", true);
update_mode();
}
// Update view mode according to selected menu
void GUI_App::update_mode()
{
@@ -8304,6 +8330,65 @@ void GUI_App::open_plugins_dialog(size_t open_on_tab, const std::string& highlig
}
}
void GUI_App::refresh_plugins()
{
// The metadata refresh blocks on disc discovery and a cloud round-trip, so run it on a worker
// and report completion through the notification manager -- the speed dial needs no dialog.
std::thread([]() {
wxString error;
try {
refresh_plugin_metadata_blocking(/*fetch_cloud=*/true);
} catch (const std::exception& ex) {
error = from_u8(ex.what());
} catch (...) {
error = "Unknown error"; // plain literal: wx translation isn't safe off the UI thread
}
if (!wxTheApp)
return;
wxTheApp->CallAfter([error]() {
if (wxGetApp().is_closing())
return;
Plater* plater = wxGetApp().plater();
if (plater == nullptr)
return;
if (error.IsEmpty())
plater->get_notification_manager()->push_notification(
NotificationType::CustomNotification,
NotificationManager::NotificationLevel::RegularNotificationLevel,
into_u8(_L("Plugins refreshed.")));
else
plater->get_notification_manager()->push_notification(
NotificationType::CustomNotification,
NotificationManager::NotificationLevel::ErrorNotificationLevel,
into_u8(wxString::Format(_L("Failed to refresh plugins: %s"), error)));
});
}).detach();
}
void GUI_App::install_local_plugin()
{
if (mainframe == nullptr)
return;
wxFileDialog dialog(mainframe, _L("Select plugin package"), wxEmptyString, wxEmptyString, _L("Plugin files (*.py;*.whl)|*.py;*.whl"),
wxFD_OPEN | wxFD_FILE_MUST_EXIST);
if (dialog.ShowModal() != wxID_OK)
return;
wxString message;
const bool ok = install_local_plugin_package(boost::filesystem::path(dialog.GetPath().ToUTF8().data()), mainframe, message);
if (message.IsEmpty())
return; // user cancelled the overwrite prompt
Plater* plater = this->plater();
if (plater == nullptr)
return;
plater->get_notification_manager()->push_notification(
NotificationType::CustomNotification,
ok ? NotificationManager::NotificationLevel::RegularNotificationLevel : NotificationManager::NotificationLevel::ErrorNotificationLevel,
into_u8(message));
}
void GUI_App::open_terminal_dialog()
{
// Reached from the plugins dialog's webview ("open_terminal" command), i.e. from
@@ -8367,12 +8452,14 @@ void GUI_App::open_exportpresetbundledialog(size_t open_on_tab, const std::strin
void GUI_App::open_preferences(size_t open_on_tab, const std::string& highlight_option)
{
static constexpr const char* opengl_fxaa_setting_key = "opengl_fxaa_enabled";
static constexpr const char* opengl_fps_cap_setting_key = "opengl_fps_cap";
static constexpr const char* opengl_show_fps_overlay_setting_key = "opengl_show_fps_overlay";
const std::string previous_opengl_fxaa = app_config->get(opengl_fxaa_setting_key);
const std::string previous_opengl_fps_cap = app_config->get(opengl_fps_cap_setting_key);
const std::string previous_opengl_show_fps_overlay = app_config->get(opengl_show_fps_overlay_setting_key);
// Render settings the canvas reads every frame; a change needs one redraw to show.
static constexpr const char* opengl_render_setting_keys[] = {
SETTING_OPENGL_FXAA_ENABLED, SETTING_OPENGL_FPS_CAP, SETTING_OPENGL_SHOW_FPS_OVERLAY, SETTING_OPENGL_SCENE_CACHE,
SETTING_OPENGL_SKIP_IDENTICAL_FRAMES
};
std::vector<std::string> previous_opengl_render_settings;
for (const char* key : opengl_render_setting_keys)
previous_opengl_render_settings.emplace_back(app_config->get(key));
bool need_recreate_gui = false;
std::string pending_language;
@@ -8412,10 +8499,10 @@ void GUI_App::open_preferences(size_t open_on_tab, const std::string& highlight_
}
}
const bool opengl_fxaa_changed = app_config->get(opengl_fxaa_setting_key) != previous_opengl_fxaa;
const bool opengl_fps_cap_changed = app_config->get(opengl_fps_cap_setting_key) != previous_opengl_fps_cap;
const bool opengl_show_fps_overlay_changed = app_config->get(opengl_show_fps_overlay_setting_key) != previous_opengl_show_fps_overlay;
if ((opengl_fxaa_changed || opengl_fps_cap_changed || opengl_show_fps_overlay_changed) && !need_recreate_gui && this->plater_ != nullptr) {
bool opengl_render_settings_changed = false;
for (size_t i = 0; i < previous_opengl_render_settings.size(); ++i)
opengl_render_settings_changed |= app_config->get(opengl_render_setting_keys[i]) != previous_opengl_render_settings[i];
if (opengl_render_settings_changed && !need_recreate_gui && this->plater_ != nullptr) {
this->plater_->set_current_canvas_as_dirty();
this->plater_->get_current_canvas3D()->force_set_focus();
}
@@ -8429,6 +8516,9 @@ void GUI_App::open_preferences(size_t open_on_tab, const std::string& highlight_
this->plater_->get_current_canvas3D()->force_set_focus();
return;
}
// Built-in Speed Dial command titles are copied from the catalog at init and don't follow a
// live locale switch; rebuild them in the new language before the GUI (and palette) rebuilds.
m_action_registry.relocalize_builtins();
}
if (need_recreate_gui)
+13
View File
@@ -347,6 +347,11 @@ public:
int OnExit() override;
bool initialized() const { return m_initialized; }
inline bool is_enable_multi_machine() { return this->app_config&& this->app_config->get("enable_multi_machine") == "true"; }
#ifdef SLIC3R_CAD
inline bool is_enable_cad_feature() { return this->app_config && this->app_config->get_bool("enable_cad_feature"); }
inline bool is_auto_close_sketch_loops() { return !this->app_config
|| this->app_config->get_bool("auto_close_sketch_loops"); }
#endif
std::map<std::string, bool> test_url_state;
@@ -591,6 +596,11 @@ public:
std::string get_saved_mode_str();
std::string get_mode_str();
void save_mode(const /*ConfigOptionMode*/int mode) ;
// Switch to `mode` from the Speed Dial: a developer-mode override hides the saved mode
// (get_mode returns comDevelop), so clear it first and persist the choice.
void set_mode(ConfigOptionMode mode);
// Turn the developer-mode override on and refresh the UI (used before jumping to a Developer setting).
void enable_developer_mode();
void update_mode();
void update_internal_development();
void show_ip_address_enter_dialog(wxString title = wxEmptyString);
@@ -629,6 +639,9 @@ public:
void open_preferences(size_t open_on_tab = 0, const std::string& highlight_option = std::string());
void open_presetbundledialog(size_t open_on_tab = 0, const std::string& highlight_option = std::string());
void open_plugins_dialog(size_t open_on_tab = 0, const std::string& highlight_option = std::string());
// Dialog-free plugin actions used by the speed dial: they never require the Plugins dialog to be open.
void refresh_plugins();
void install_local_plugin();
void open_terminal_dialog();
void open_speed_dial();
ActionRegistry& action_registry() { return m_action_registry; }
+116 -98
View File
@@ -578,113 +578,131 @@ wxMenu* MenuFactory::append_submenu_add_generic(wxMenu* menu, ModelVolumeType ty
return sub_menu;
}
// Orca: handy models shipped under <resources>/handy_models. Defining everything in one table keeps
// the menu label, the files to load and the per-model behavior in a single place. Labels are wrapped
// in L() so they are picked up for translation. Shared with the command palette.
const std::vector<MenuFactory::HandyModel>& MenuFactory::handy_models()
{
static const std::vector<HandyModel> models = {
{"orca_cube", L("Orca Cube"), {"OrcaCube_v2.drc", "OrcaPlug_v2.drc"}, true},
{"orcasliced_combo", L("OrcaSliced Combo"), {"OrcaSliced.3mf", "OrcaCube_v2.drc", "OrcaPlug_v2.drc"}, true},
{"orca_badge", L("Orca Badge"), {"OrcaBadge.3mf"}},
{"orca_tolerance_test", L("Orca Tolerance Test"), {"OrcaToleranceTest.drc"}},
{"3dbenchy", L("3DBenchy"), {"3DBenchy.drc"}},
{"cali_cat", L("Cali Cat"), {"calicat.drc"}},
{"autodesk_fdm_test", L("Autodesk FDM Test"), {"ksr_fdmtest_v4.drc"}},
{"voron_cube", L("Voron Cube"), {"Voron_Design_Cube_v7.drc"}},
{"stanford_bunny", L("Stanford Bunny"), {"Stanford_Bunny.drc"}},
{"orca_string_hell", L("Orca String Hell"), {"Orca_stringhell.drc"}, false, true},
};
return models;
}
void MenuFactory::load_handy_model(std::size_t index)
{
const std::vector<HandyModel>& models = handy_models();
if (index >= models.size())
return;
const HandyModel& model = models[index];
std::vector<boost::filesystem::path> input_files;
input_files.reserve(model.file_names.size());
for (const auto& file_name : model.file_names)
input_files.push_back((boost::filesystem::path(Slic3r::resources_dir()) / "handy_models" / file_name));
Plater* pl = plater();
if (!pl)
return;
pl->load_files(input_files, LoadStrategy::LoadModel);
if (model.arrange_after_import) {
pl->set_prepare_state(Job::PREPARE_STATE_MENU);
pl->arrange();
}
// Suggest to change settings for stringhell
// This serves as mini tutorial for new users
if (model.is_stringhell) {
wxGetApp().CallAfter([=] {
DynamicPrintConfig* m_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config;
bool is_only_one_wall_top = m_config->opt_bool("only_one_wall_top");
auto min_width_top_surface = m_config->option<ConfigOptionFloatOrPercent>("min_width_top_surface")->value;
if (is_only_one_wall_top && min_width_top_surface > 0) {
wxString msg_text = _L("This model features text embossment on the top surface. For optimal results, it is "
"advisable to set the 'One Wall Threshold (min_width_top_surface)' "
"to 0 for the 'Only One Wall on Top Surfaces' to work best.\n"
"Yes - Change these settings automatically\n"
"No - Do not change these settings for me");
MessageDialog dialog(wxGetApp().plater(), msg_text, _L("Suggestion"), wxICON_WARNING | wxYES | wxNO);
if (dialog.ShowModal() == wxID_YES) {
m_config->set_key_value("min_width_top_surface", new ConfigOptionFloatOrPercent(0, false));
wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty();
wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config();
}
wxGetApp().plater()->update();
}
});
}
}
// Orca: add submenu for adding handy models
wxMenu* MenuFactory::append_submenu_add_handy_model(wxMenu* menu, ModelVolumeType type) {
auto sub_menu = new wxMenu;
// Orca: handy models shipped under <resources>/handy_models. Defining everything in one table
// keeps the menu label, the files to load and the per-model behavior in a single place and
// avoids repeating the label strings (and the value-vs-pointer comparison pitfalls that come
// with that). Labels are wrapped in L() so they are picked up for translation.
struct HandyModel
{
const char* label;
std::vector<std::string> file_names;
bool arrange_after_import = false;
bool is_stringhell = false;
};
static const std::vector<HandyModel> handy_models = {
{L("Orca Cube"), {"OrcaCube_v2.drc", "OrcaPlug_v2.drc"}, true},
{L("OrcaSliced Combo"), {"OrcaSliced.3mf", "OrcaCube_v2.drc", "OrcaPlug_v2.drc"}, true},
{L("Orca Badge"), {"OrcaBadge.3mf"}},
{L("Orca Tolerance Test"), {"OrcaToleranceTest.drc"}},
{L("3DBenchy"), {"3DBenchy.drc"}},
{L("Cali Cat"), {"calicat.drc"}},
{L("Autodesk FDM Test"), {"ksr_fdmtest_v4.drc"}},
{L("Voron Cube"), {"Voron_Design_Cube_v7.drc"}},
{L("Stanford Bunny"), {"Stanford_Bunny.drc"}},
{L("Orca String Hell"), {"Orca_stringhell.drc"}, false, true},
};
for (const auto& model : handy_models) {
append_menu_item(
sub_menu, wxID_ANY, _(model.label), "",
[&model](wxCommandEvent&) {
std::vector<boost::filesystem::path> input_files;
input_files.reserve(model.file_names.size());
for (const auto& file_name : model.file_names)
input_files.push_back((boost::filesystem::path(Slic3r::resources_dir()) / "handy_models" / file_name));
plater()->load_files(input_files, LoadStrategy::LoadModel);
if (model.arrange_after_import) {
plater()->set_prepare_state(Job::PREPARE_STATE_MENU);
plater()->arrange();
}
// Suggest to change settings for stringhell
// This serves as mini tutorial for new users
if (model.is_stringhell) {
wxGetApp().CallAfter([=] {
DynamicPrintConfig* m_config = &wxGetApp().preset_bundle->prints.get_edited_preset().config;
bool is_only_one_wall_top = m_config->opt_bool("only_one_wall_top");
auto min_width_top_surface = m_config->option<ConfigOptionFloatOrPercent>("min_width_top_surface")->value;
if (is_only_one_wall_top && min_width_top_surface > 0) {
wxString msg_text = _L("This model features text embossment on the top surface. For optimal results, it is "
"advisable to set the 'One Wall Threshold (min_width_top_surface)' "
"to 0 for the 'Only One Wall on Top Surfaces' to work best.\n"
"Yes - Change these settings automatically\n"
"No - Do not change these settings for me");
MessageDialog dialog(wxGetApp().plater(), msg_text, _L("Suggestion"), wxICON_WARNING | wxYES | wxNO);
if (dialog.ShowModal() == wxID_YES) {
m_config->set_key_value("min_width_top_surface", new ConfigOptionFloatOrPercent(0, false));
wxGetApp().get_tab(Preset::TYPE_PRINT)->update_dirty();
wxGetApp().get_tab(Preset::TYPE_PRINT)->reload_config();
}
wxGetApp().plater()->update();
}
});
}
},
"", menu);
const std::vector<HandyModel>& models = handy_models();
for (std::size_t i = 0; i < models.size(); ++i) {
append_menu_item(sub_menu, wxID_ANY, _(models[i].label), "",
[i](wxCommandEvent&) { MenuFactory::load_handy_model(i); }, "", menu);
}
return sub_menu;
}
// Create a Text/SVG volume through the matching gizmo. `type == INVALID` means "create a new object".
// Shared by the add menu and the command palette.
static void add_volume_with_gizmo(GLGizmosManager::EType gizmo_type, ModelVolumeType type)
{
Plater* pl = plater();
if (!pl)
return;
const GLCanvas3D* canvas = pl->canvas3D();
if (!canvas)
return;
GLGizmoBase* gizmo_base = canvas->get_gizmos_manager().get_gizmo(gizmo_type);
if (!gizmo_base)
return;
ModelVolumeType volume_type = type;
// no selected object means create new object
if (volume_type == ModelVolumeType::INVALID)
volume_type = ModelVolumeType::MODEL_PART;
auto screen_position = canvas->get_popup_menu_position();
if (gizmo_type == GLGizmosManager::Emboss) {
auto* emboss = dynamic_cast<GLGizmoEmboss*>(gizmo_base);
if (emboss == nullptr)
return;
if (screen_position.has_value())
emboss->create_volume(volume_type, *screen_position);
else
emboss->create_volume(volume_type);
} else if (gizmo_type == GLGizmosManager::Svg) {
auto* svg = dynamic_cast<GLGizmoSVG*>(gizmo_base);
if (svg == nullptr)
return;
if (screen_position.has_value())
svg->create_volume(volume_type, *screen_position);
else
svg->create_volume(volume_type);
}
}
void MenuFactory::add_text_volume(ModelVolumeType type) { add_volume_with_gizmo(GLGizmosManager::Emboss, type); }
void MenuFactory::add_svg_volume(ModelVolumeType type) { add_volume_with_gizmo(GLGizmosManager::Svg, type); }
static void append_menu_itemm_add_(const wxString& name, GLGizmosManager::EType gizmo_type, wxMenu *menu, ModelVolumeType type, bool is_submenu_item) {
auto add_ = [type, gizmo_type](const wxCommandEvent & /*unnamed*/) {
const GLCanvas3D *canvas = plater()->canvas3D();
const GLGizmosManager &mng = canvas->get_gizmos_manager();
GLGizmoBase *gizmo_base = mng.get_gizmo(gizmo_type);
ModelVolumeType volume_type = type;
// no selected object means create new object
if (volume_type == ModelVolumeType::INVALID)
volume_type = ModelVolumeType::MODEL_PART;
auto screen_position = canvas->get_popup_menu_position();
if (gizmo_type == GLGizmosManager::Emboss) {
auto emboss = dynamic_cast<GLGizmoEmboss *>(gizmo_base);
assert(emboss != nullptr);
if (emboss == nullptr) return;
if (screen_position.has_value()) {
emboss->create_volume(volume_type, *screen_position);
} else {
emboss->create_volume(volume_type);
}
} else if (gizmo_type == GLGizmosManager::Svg) {
auto svg = dynamic_cast<GLGizmoSVG *>(gizmo_base);
assert(svg != nullptr);
if (svg == nullptr) return;
if (screen_position.has_value()) {
svg->create_volume(volume_type, *screen_position);
} else {
svg->create_volume(volume_type);
}
}
};
auto add_ = [type, gizmo_type](const wxCommandEvent & /*unnamed*/) { add_volume_with_gizmo(gizmo_type, type); };
if (type == ModelVolumeType::MODEL_PART || type == ModelVolumeType::NEGATIVE_VOLUME || type == ModelVolumeType::PARAMETER_MODIFIER ||
type == ModelVolumeType::INVALID // cannot use gizmo without selected object
+18
View File
@@ -4,6 +4,7 @@
#include <map>
#include <vector>
#include <array>
#include <cstddef>
#include <wx/bitmap.h>
@@ -51,6 +52,23 @@ public:
static std::vector<wxBitmap> get_text_volume_bitmaps();
static std::vector<wxBitmap> get_svg_volume_bitmaps();
// Orca: handy models shipped under <resources>/handy_models. The menu and the command palette
// share this table so the model list and its per-model behavior live in one place.
struct HandyModel
{
const char* key;
const char* label;
std::vector<std::string> file_names;
bool arrange_after_import = false;
bool is_stringhell = false;
};
static const std::vector<HandyModel>& handy_models();
static void load_handy_model(std::size_t index);
// Add a Text/SVG volume through the Emboss/SVG gizmo. Shared by the add menu and the palette.
static void add_text_volume(ModelVolumeType type);
static void add_svg_volume(ModelVolumeType type);
MenuFactory();
~MenuFactory() = default;
+2
View File
@@ -478,6 +478,8 @@ int get_dpi_for_window(const wxWindow *window);
#ifdef __WXOSX__
void dataview_remove_insets(wxDataViewCtrl* dv);
void staticbox_remove_margin(wxStaticBox* sb);
// Clip a top-level window (and its webview) to a rounded rect with a native layer.
void set_window_corner_radius(wxWindow* win, int radius);
#endif
#ifdef __WXGTK__
+17
View File
@@ -1,5 +1,6 @@
#include <unistd.h>
#include <sys/sysctl.h>
#import <Cocoa/Cocoa.h>
#import <wx/osx/cocoa/dataview.h>
#import "GUI_Utils.hpp"
@@ -21,6 +22,22 @@ void staticbox_remove_margin(wxStaticBox* sb) {
[nativeBox setBorderWidth:0];
}
// wxOSX SetShape only clears the window background; it cannot clip to a region. Clipping the
// window's view layer to a rounded rect is what actually rounds the opaque webview inside.
void set_window_corner_radius(wxWindow* win, int radius) {
if (!win)
return;
NSView* view = (NSView*)win->GetHandle();
if (!view)
return;
NSWindow* window = [view window];
[window setOpaque:NO];
[window setBackgroundColor:[NSColor clearColor]];
[view setWantsLayer:YES];
[[view layer] setCornerRadius:radius];
[[view layer] setMasksToBounds:YES];
}
bool is_debugger_present()
// Returns true if the current process is being debugged (either
// running under the debugger or has a debugger attached post facto).
@@ -69,6 +69,7 @@ bool GLGizmoAssembly::on_is_activable() const
void GLGizmoAssembly::on_render_input_window(float x, float y, float bottom_limit)
{
render_dimensioning_if_scene_reused();
static std::optional<Measure::SurfaceFeature> last_feature;
static EMode last_mode = EMode::FeatureSelection;
static SelectedFeatures last_selected_features;
+2
View File
@@ -179,6 +179,8 @@ public:
bool is_selectable() const { return on_is_selectable(); }
CommonGizmosDataID get_requirements() const { return on_get_requirements(); }
virtual bool wants_enter_leave_snapshots() const { return false; }
// True when what on_render() draws would change if the cursor moved.
virtual bool render_follows_cursor() const { return false; }
virtual std::string get_gizmo_entering_text() const { assert(false); return ""; }
virtual std::string get_gizmo_leaving_text() const { assert(false); return ""; }
virtual std::string get_action_snapshot_name() const;
@@ -167,6 +167,8 @@ protected:
std::string on_get_name() const override;
bool on_is_activable() const override;
// The preview ear is drawn only while the cursor is on the model.
bool render_follows_cursor() const override { return render_hover_point.has_value(); }
//bool on_is_selectable() const override;
virtual CommonGizmosDataID on_get_requirements() const override;
void on_load(cereal::BinaryInputArchive& ar) override;
+61 -28
View File
@@ -563,8 +563,30 @@ void GLGizmoMeasure::init_plane_glmodel(GripperType gripper_type, const Measure:
}
}
bool GLGizmoMeasure::render_follows_cursor() const
{
// The two raycasts on_render() starts with, without their side effects.
if (m_editing_distance)
return false;
const Vec2d mouse_position = m_parent.get_local_mouse_position();
const Camera& camera = wxGetApp().plater()->get_camera();
Vec3f hit = Vec3f::Zero();
Vec3f normal = Vec3f::Zero();
for (const auto& item : m_gripper_id_raycast_map) {
if (item.second->get_id() > 0 && item.second->get_raycaster()->closest_hit(mouse_position, item.second->get_transform(), camera, hit, normal))
return true;
}
for (const auto& item : m_mesh_raycaster_map) {
if (item.second->get_raycaster()->unproject_on_mesh(mouse_position, item.second->get_transform(), camera, hit, normal))
return true;
}
return false;
}
void GLGizmoMeasure::on_render()
{
m_rendered_this_frame = true;
#if ENABLE_MEASURE_GIZMO_DEBUG
render_debug_dialog();
#endif // ENABLE_MEASURE_GIZMO_DEBUG
@@ -701,35 +723,36 @@ void GLGizmoMeasure::on_render()
reset_gripper_pick(GripperType::UNDEFINE, true);
m_curr_feature = curr_feature;
if (!m_curr_feature.has_value())
return;
m_curr_feature->volume = m_last_hit_volume;
m_curr_feature->world_tran = m_mesh_raycaster_map[m_last_hit_volume]->get_transform();
// The selected features are drawn below whether or not one is hovered.
if (m_curr_feature.has_value()) {
m_curr_feature->volume = m_last_hit_volume;
m_curr_feature->world_tran = m_mesh_raycaster_map[m_last_hit_volume]->get_transform();
switch (m_curr_feature->get_type()) {
default: { assert(false); break; }
case Measure::SurfaceFeatureType::Point:
{
m_gripper_id_raycast_map[GripperType::POINT] = std::make_shared<PickRaycaster>(POINT_ID, *m_sphere.mesh_raycaster);
break;
}
case Measure::SurfaceFeatureType::Edge:
{
m_gripper_id_raycast_map[GripperType::EDGE] = std::make_shared<PickRaycaster>(EDGE_ID, *m_cylinder.mesh_raycaster);
break;
}
case Measure::SurfaceFeatureType::Circle: {
m_curr_circle.last_circle_feature = nullptr;
m_curr_circle.inv_zoom = 0;
init_circle_glmodel(GripperType::CIRCLE, *m_curr_feature, m_curr_circle,inv_zoom);
break;
}
case Measure::SurfaceFeatureType::Plane: {
update_world_plane_features(m_curr_measuring.get(), *m_curr_feature);
m_curr_plane.plane_idx = -1;
init_plane_glmodel(GripperType::PLANE, *m_curr_feature, m_curr_plane);
break;
}
switch (m_curr_feature->get_type()) {
default: { assert(false); break; }
case Measure::SurfaceFeatureType::Point:
{
m_gripper_id_raycast_map[GripperType::POINT] = std::make_shared<PickRaycaster>(POINT_ID, *m_sphere.mesh_raycaster);
break;
}
case Measure::SurfaceFeatureType::Edge:
{
m_gripper_id_raycast_map[GripperType::EDGE] = std::make_shared<PickRaycaster>(EDGE_ID, *m_cylinder.mesh_raycaster);
break;
}
case Measure::SurfaceFeatureType::Circle: {
m_curr_circle.last_circle_feature = nullptr;
m_curr_circle.inv_zoom = 0;
init_circle_glmodel(GripperType::CIRCLE, *m_curr_feature, m_curr_circle,inv_zoom);
break;
}
case Measure::SurfaceFeatureType::Plane: {
update_world_plane_features(m_curr_measuring.get(), *m_curr_feature);
m_curr_plane.plane_idx = -1;
init_plane_glmodel(GripperType::PLANE, *m_curr_feature, m_curr_plane);
break;
}
}
}
}
}
@@ -2136,8 +2159,18 @@ void GLGizmoMeasure::init_render_input_window()
m_same_model_object = is_two_volume_in_same_model_object();
}
void GLGizmoMeasure::render_dimensioning_if_scene_reused()
{
// The labels are ImGui and live one frame; the lines drawn with them land under the cached
// scene, which is drawn after the overlay is built.
if (!m_rendered_this_frame)
render_dimensioning();
m_rendered_this_frame = false;
}
void GLGizmoMeasure::on_render_input_window(float x, float y, float bottom_limit)
{
render_dimensioning_if_scene_reused();
static std::optional<Measure::SurfaceFeature> last_feature;
static EMode last_mode = EMode::FeatureSelection;
static SelectedFeatures last_selected_features;
+6
View File
@@ -228,6 +228,9 @@ protected:
void restore_scene_raycasters_state();
void render_dimensioning();
// Builds the dimension labels on a frame that reused the cached scene and so skipped on_render().
void render_dimensioning_if_scene_reused();
bool m_rendered_this_frame{ false };
#if ENABLE_MEASURE_GIZMO_DEBUG
void render_debug_dialog();
@@ -255,6 +258,9 @@ protected:
bool on_init() override;
std::string on_get_name() const override;
bool on_is_activable() const override;
// The hover is resolved inside on_render(), against the mesh and against the grippers of the
// selected features, which sit off the mesh.
bool render_follows_cursor() const override;
void on_render() override;
void on_set_state() override;
+21 -4
View File
@@ -131,15 +131,12 @@ void GLGizmoPainterBase::render_triangles(const Selection& selection) const
}
}
void GLGizmoPainterBase::render_cursor()
std::vector<Transform3d> GLGizmoPainterBase::mesh_trafo_matrices() const
{
// First check that the mouse pointer is on an object.
const ModelObject* mo = m_c->selection_info()->model_object();
const Selection& selection = m_parent.get_selection();
const ModelInstance* mi = mo->instances[selection.get_instance_idx()];
const Camera& camera = wxGetApp().plater()->get_camera();
// Precalculate transformations of individual meshes.
std::vector<Transform3d> trafo_matrices;
for (const ModelVolume* mv : mo->volumes) {
if (mv->is_model_part())
@@ -154,6 +151,26 @@ void GLGizmoPainterBase::render_cursor()
}
}
}
return trafo_matrices;
}
bool GLGizmoPainterBase::render_follows_cursor() const
{
// The brush is drawn only where the cursor meets the model. update_raycast_cache() keeps the
// answer for render_cursor().
if (m_c->selection_info() == nullptr || m_c->selection_info()->model_object() == nullptr)
return false;
update_raycast_cache(m_parent.get_local_mouse_position(), wxGetApp().plater()->get_camera(), mesh_trafo_matrices());
return m_rr.mesh_id != -1;
}
void GLGizmoPainterBase::render_cursor()
{
// First check that the mouse pointer is on an object.
const Camera& camera = wxGetApp().plater()->get_camera();
// Precalculate transformations of individual meshes.
const std::vector<Transform3d> trafo_matrices = mesh_trafo_matrices();
// Raycast and return if there's no hit.
update_raycast_cache(m_parent.get_local_mouse_position(), camera, trafo_matrices);
if (m_rr.mesh_id == -1)
@@ -318,6 +318,8 @@ private:
std::vector<ProjectedHeightRange> get_projected_height_range(const Vec2d& mouse_position, double resolution, const std::vector<const ModelVolume*>& part_volumes, const std::vector<Transform3d>& trafo_matrices) const;
bool is_mesh_point_clipped(const Vec3d& point, const Transform3d& trafo) const;
// World transforms of the model parts, in mo->volumes order.
std::vector<Transform3d> mesh_trafo_matrices() const;
void update_raycast_cache(const Vec2d& mouse_position,
const Camera& camera,
const std::vector<Transform3d>& trafo_matrices) const;
@@ -370,6 +372,7 @@ protected:
virtual PainterGizmoType get_painter_type() const = 0;
bool on_is_activable() const override;
bool render_follows_cursor() const override;
bool on_is_selectable() const override;
void on_load(cereal::BinaryInputArchive& ar) override;
void on_save(cereal::BinaryOutputArchive& ar) const override {}
+186
View File
@@ -0,0 +1,186 @@
#include "GLGizmoPrimitive.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/NotificationManager.hpp"
#include "libslic3r/Model.hpp"
#ifndef IMGUI_DEFINE_MATH_OPERATORS
#define IMGUI_DEFINE_MATH_OPERATORS
#endif
#include <imgui/imgui_internal.h>
namespace Slic3r {
namespace GUI {
GLGizmoPrimitive::GLGizmoPrimitive(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id)
: GLGizmoBase(parent, icon_filename, sprite_id) {}
bool GLGizmoPrimitive::on_init() { return true; }
std::string GLGizmoPrimitive::on_get_name() const { return _u8L("Primitive"); }
bool GLGizmoPrimitive::on_is_activable() const { return true; }
void GLGizmoPrimitive::on_render() {}
void GLGizmoPrimitive::on_set_state()
{ if (m_state == EState::On) { m_params = PrimitiveParams{}; m_preview_dirty = true; } }
bool GLGizmoPrimitive::on_mouse(const wxMouseEvent&) { return false; }
CommonGizmosDataID GLGizmoPrimitive::on_get_requirements() const
{ return CommonGizmosDataID(int(CommonGizmosDataID::SelectionInfo) | int(CommonGizmosDataID::InstancesHider)); }
void GLGizmoPrimitive::on_load(cereal::BinaryInputArchive& ar)
{ ar(m_params); m_preview_dirty = true; }
void GLGizmoPrimitive::on_save(cereal::BinaryOutputArchive& ar) const
{ ar(m_params); }
void GLGizmoPrimitive::apply_preset(const char*, double w, double h, double d)
{
m_params.type = PrimitiveType::Box;
m_params.box_w = w; m_params.box_h = h; m_params.box_d = d;
m_preview_dirty = true;
}
static void gen_mesh_and_add(PrimitiveParams& p, const char* snap_name)
{
TopoDS_Solid solid = GeometryEngine::make_primitive(p);
TopoDS_Shape shape = solid;
if (p.dressup_enabled) {
if (p.dressup_type == DressUpType::Fillet)
shape = GeometryEngine::apply_fillet(shape, p.dressup_radius, p.dressup_faces);
else
shape = GeometryEngine::apply_chamfer(shape, p.dressup_chamfer_dist, p.dressup_faces);
}
TriangleMesh mesh = GeometryEngine::tessellate(shape, p.linear_deflection, p.angular_deflection);
if (mesh.its.indices.empty()) {
wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::WarningNotificationLevel, _u8L("Empty mesh generated"));
return;
}
wxGetApp().plater()->take_snapshot(snap_name);
ModelObject* mo = wxGetApp().model().add_object();
std::string name = GeometryEngine::primitive_name(p.type);
if (p.dressup_enabled && p.dressup_type == DressUpType::Fillet) name += " (Fillet)";
else if (p.dressup_enabled) name += " (Chamfer)";
mo->name = name;
mo->add_volume(std::move(mesh))->set_new_unique_id();
mo->ensure_on_bed();
wxGetApp().plater()->update();
}
void GLGizmoPrimitive::apply_primitive() { gen_mesh_and_add(m_params, "Add Primitive"); }
void GLGizmoPrimitive::on_render_input_window(float x, float y, float bottom_limit)
{
y = std::min(y, bottom_limit - ImGui::GetWindowHeight());
const float scale = m_parent.get_scale();
ImGuiWrapper::push_toolbar_style(scale);
GizmoImguiSetNextWIndowPos(x, y, ImGuiCond_Always, 0.0f, 0.0f);
GizmoImguiBegin("Primitive", ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove
| ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse
| ImGuiWindowFlags_NoTitleBar);
if (ImGui::CollapsingHeader("Shape", ImGuiTreeNodeFlags_DefaultOpen)) {
static const char* names[] = {"Box", "Cylinder", "Sphere", "Cone", "Torus"};
int cur = (int)m_params.type;
if (ImGui::Combo("##type", &cur, names, (int)PrimitiveType::COUNT)) {
m_params.type = (PrimitiveType)cur;
m_preview_dirty = true;
}
ImGui::Text("Quick:");
ImGui::SameLine();
if (ImGui::SmallButton("10mm")) apply_preset("10mm cube", 10, 10, 10);
ImGui::SameLine();
if (ImGui::SmallButton("20mm")) apply_preset("20mm cube", 20, 20, 20);
ImGui::SameLine();
if (ImGui::SmallButton("50mm")) apply_preset("50mm cube", 50, 50, 50);
}
ImGui::Separator();
if (ImGui::CollapsingHeader("Dimensions", ImGuiTreeNodeFlags_DefaultOpen)) {
auto dim = [&](const char* label, double& val, double step=0.5, double fast=5.0) {
ImGui::SetNextItemWidth(130);
if (ImGui::InputDouble(label, &val, step, fast, "%.1f mm")) m_preview_dirty = true;
if (val < 0.5) val = 0.5;
};
switch (m_params.type) {
case PrimitiveType::Box:
dim("Width (X)", m_params.box_w);
dim("Depth (Y)", m_params.box_d);
dim("Height (Z)", m_params.box_h);
break;
case PrimitiveType::Cylinder:
dim("Radius", m_params.cyl_radius);
dim("Height", m_params.cyl_height);
break;
case PrimitiveType::Sphere:
dim("Radius", m_params.sph_radius);
break;
case PrimitiveType::Cone:
dim("Bottom R", m_params.cone_r1);
dim("Top R", m_params.cone_r2);
dim("Height", m_params.cone_height);
break;
case PrimitiveType::Torus:
dim("Major R", m_params.torus_r1);
dim("Minor R", m_params.torus_r2, 0.1, 1.0);
break;
default: break;
}
}
ImGui::Separator();
if (ImGui::CollapsingHeader("Fillet / Chamfer")) {
ImGui::Checkbox("Enable", &m_params.dressup_enabled);
if (m_params.dressup_enabled) {
static const char* dn[] = {"Fillet", "Chamfer"};
int du = (int)m_params.dressup_type;
ImGui::SetNextItemWidth(100);
if (ImGui::Combo("##dtype", &du, dn, 2)) { m_params.dressup_type = (DressUpType)du; m_preview_dirty = true; }
static const char* fn[] = {"All edges", "Top edges", "Bottom edges", "Lateral edges"};
int fg = (int)m_params.dressup_faces;
ImGui::SetNextItemWidth(140);
if (ImGui::Combo("Edges", &fg, fn, 4)) { m_params.dressup_faces = (FaceGroup)fg; m_preview_dirty = true; }
if (m_params.dressup_type == DressUpType::Fillet) {
ImGui::SetNextItemWidth(100);
if (ImGui::InputDouble("Radius", &m_params.dressup_radius, 0.1, 1.0, "%.1f mm")) {
if (m_params.dressup_radius < 0.1) m_params.dressup_radius = 0.1;
m_preview_dirty = true;
}
} else {
ImGui::SetNextItemWidth(100);
if (ImGui::InputDouble("Distance", &m_params.dressup_chamfer_dist, 0.1, 1.0, "%.1f mm")) {
if (m_params.dressup_chamfer_dist < 0.1) m_params.dressup_chamfer_dist = 0.1;
m_preview_dirty = true;
}
}
}
}
ImGui::Separator();
if (ImGui::CollapsingHeader("Quality")) {
ImGui::SetNextItemWidth(130);
if (ImGui::InputDouble("Mesh resolution", &m_params.linear_deflection, 0.001, 0.1, "%.3f mm")) {
if (m_params.linear_deflection < 0.001) m_params.linear_deflection = 0.001;
if (m_params.linear_deflection > 1.0) m_params.linear_deflection = 1.0;
m_preview_dirty = true;
}
}
ImGui::Separator();
if (ImGui::Button("Add Shape", {-1, 28}))
apply_primitive();
if (ImGui::Button("Close", {-1, 0}))
m_parent.reset_all_gizmos();
GizmoImguiEnd();
ImGuiWrapper::pop_toolbar_style();
}
} // namespace GUI
} // namespace Slic3r
@@ -0,0 +1,43 @@
#ifndef slic3r_GLGizmoPrimitive_hpp_
#define slic3r_GLGizmoPrimitive_hpp_
#include "GLGizmoBase.hpp"
#include "GLGizmosCommon.hpp"
#include "libslic3r/CAD/GeometryEngine.hpp"
namespace Slic3r {
namespace GUI {
class GLGizmoPrimitive : public GLGizmoBase
{
public:
GLGizmoPrimitive(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id);
~GLGizmoPrimitive() = default;
bool on_mouse(const wxMouseEvent& mouse_event) override;
protected:
bool on_init() override;
std::string on_get_name() const override;
bool on_is_activable() const override;
void on_render() override;
void on_set_state() override;
CommonGizmosDataID on_get_requirements() const override;
void on_render_input_window(float x, float y, float bottom_limit) override;
void on_load(cereal::BinaryInputArchive& ar) override;
void on_save(cereal::BinaryOutputArchive& ar) const override;
private:
void apply_primitive();
void apply_preset(const char* name, double w, double h, double d);
PrimitiveParams m_params;
TriangleMesh m_preview_mesh;
bool m_preview_dirty{true};
};
} // namespace GUI
} // namespace Slic3r
#endif // slic3r_GLGizmoPrimitive_hpp_
+458
View File
@@ -0,0 +1,458 @@
#include "GLGizmoSketch.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/NotificationManager.hpp"
#include "libslic3r/Model.hpp"
#include <BRepBuilderAPI_MakeFace.hxx>
#include <BRepPrimAPI_MakeRevol.hxx>
#include <BRepAlgoAPI_Fuse.hxx>
#ifndef IMGUI_DEFINE_MATH_OPERATORS
#define IMGUI_DEFINE_MATH_OPERATORS
#endif
#include <imgui/imgui_internal.h>
#define UL(s) Slic3r::GUI::I18N::translate_utf8((s)).c_str()
namespace Slic3r {
namespace GUI {
GLGizmoSketch::GLGizmoSketch(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id)
: GLGizmoBase(parent, icon_filename, sprite_id) {}
bool GLGizmoSketch::on_init() { return true; }
std::string GLGizmoSketch::on_get_name() const { return _u8L("Sketch"); }
bool GLGizmoSketch::on_is_activable() const { return true; }
void GLGizmoSketch::on_render() {}
void GLGizmoSketch::on_set_state() { if (m_state == EState::On) clear_all(); }
bool GLGizmoSketch::on_mouse(const wxMouseEvent&) { return false; }
CommonGizmosDataID GLGizmoSketch::on_get_requirements() const
{ return CommonGizmosDataID(int(CommonGizmosDataID::SelectionInfo)); }
void GLGizmoSketch::on_load(cereal::BinaryInputArchive& ar)
{
ar(m_tool, m_profiles, m_plane, m_sp, m_rect_w, m_rect_h, m_circle_r, m_poly_sides, m_poly_r, m_snap_grid, m_grid_step);
m_active_profile = -1;
}
void GLGizmoSketch::on_save(cereal::BinaryOutputArchive& ar) const
{
ar(m_tool, m_profiles, m_plane, m_sp, m_rect_w, m_rect_h, m_circle_r, m_poly_sides, m_poly_r, m_snap_grid, m_grid_step);
}
SketchProfile& GLGizmoSketch::active_profile()
{
if (m_active_profile < 0 || m_active_profile >= (int)m_profiles.size()) {
m_profiles.emplace_back();
m_active_profile = (int)m_profiles.size() - 1;
}
return m_profiles[m_active_profile];
}
bool GLGizmoSketch::has_closed_profile() const
{
for (auto& p : m_profiles) if (p.closed && p.points.size() >= 3) return true;
return false;
}
void GLGizmoSketch::clear_all()
{
m_profiles.clear();
m_canvas_points.clear();
m_active_profile = -1;
}
void GLGizmoSketch::add_closed_profile()
{
auto& ap = active_profile();
if (ap.points.size() >= 3) {
ap.closed = true;
m_active_profile = -1;
}
}
void GLGizmoSketch::delete_profile(int idx)
{
if (idx >= 0 && idx < (int)m_profiles.size()) {
m_profiles.erase(m_profiles.begin() + idx);
if (m_active_profile >= (int)m_profiles.size()) m_active_profile = -1;
}
}
Vec2d GLGizmoSketch::snap(Vec2d pt) const
{
if (!m_snap_grid) return pt;
double gs = m_grid_step;
return {round(pt.x() / gs) * gs, round(pt.y() / gs) * gs};
}
void GLGizmoSketch::build_preset_profile()
{
auto& ap = active_profile();
ap.clear();
auto add = [&](double x, double y) { ap.points.emplace_back(x, y); };
switch (m_tool) {
case SketchTool::Rectangle:
add(-m_rect_w/2, -m_rect_h/2); add( m_rect_w/2, -m_rect_h/2);
add( m_rect_w/2, m_rect_h/2); add(-m_rect_w/2, m_rect_h/2);
ap.closed = true; m_active_profile = -1; break;
case SketchTool::Circle:
for (int i = 0; i <= m_circle_seg; ++i) {
double a = 2.0*M_PI*i/m_circle_seg;
add(cos(a)*m_circle_r, sin(a)*m_circle_r);
}
ap.closed = true; m_active_profile = -1; break;
case SketchTool::Polygon:
for (int i = 0; i < m_poly_sides; ++i) {
double a = 2.0*M_PI*i/m_poly_sides - M_PI/2;
add(cos(a)*m_poly_r, sin(a)*m_poly_r);
}
ap.closed = true; m_active_profile = -1; break;
default: break;
}
}
void GLGizmoSketch::handle_canvas_click(ImVec2 pos)
{
Vec2d pt = snap({pos.x / m_canvas_scale, -pos.y / m_canvas_scale});
if (m_tool == SketchTool::Line) {
auto& ap = active_profile();
if (ap.points.size() >= 3 && (pt - ap.points.front()).norm() < m_grid_step) {
ap.points.push_back(ap.points.front());
ap.closed = true;
m_active_profile = -1;
return;
}
ap.points.push_back(pt);
}
}
void GLGizmoSketch::draw_canvas()
{
ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 pos = ImGui::GetCursorScreenPos();
float w = 280, h = 200;
ImVec2 end(pos.x+w, pos.y+h);
float cx = pos.x+w/2, cy = pos.y+h/2;
auto tc = [&](const ImVec2& p) { return ImVec2(cx+p.x*m_canvas_scale, cy-p.y*m_canvas_scale); };
dl->AddRectFilled(pos, end, IM_COL32(28,28,36,255));
dl->AddRect(pos, end, IM_COL32(55,55,68,255));
float gs = m_grid_step;
for (float g = 0; g < w; g += gs * m_canvas_scale) {
ImU32 gc = (int(g/(gs*m_canvas_scale)) % 5 == 0) ? IM_COL32(60,60,75,100) : IM_COL32(45,45,55,60);
dl->AddLine({pos.x+g,pos.y}, {pos.x+g,end.y}, gc);
}
for (float g = 0; g < h; g += gs * m_canvas_scale) {
ImU32 gc = (int(g/(gs*m_canvas_scale)) % 5 == 0) ? IM_COL32(60,60,75,100) : IM_COL32(45,45,55,60);
dl->AddLine({pos.x,pos.y+g}, {end.x,pos.y+g}, gc);
}
dl->AddLine({cx,pos.y},{cx,end.y}, IM_COL32(70,70,85,180), 1.5f);
dl->AddLine({pos.x,cy},{end.x,cy}, IM_COL32(70,70,85,180), 1.5f);
dl->AddText({end.x-12, cy+2}, IM_COL32(120,120,140,200), "X");
dl->AddText({cx+4, pos.y+2}, IM_COL32(120,120,140,200), "Y");
for (size_t pi = 0; pi < m_profiles.size(); ++pi) {
auto& prof = m_profiles[pi];
if (prof.points.size() < 2) continue;
std::vector<ImVec2> sp;
for (auto& p : prof.points) sp.push_back(tc({(float)p.x(), (float)p.y()}));
if (prof.closed && sp.size() >= 3) {
bool is_outer = (pi == 0);
ImU32 fill = is_outer ? IM_COL32(0,180,90,35) : IM_COL32(180,60,60,35);
ImU32 line = is_outer ? IM_COL32(0,220,100,255) : IM_COL32(220,80,80,255);
dl->AddConvexPolyFilled(sp.data(), (int)sp.size(), fill);
for (size_t i=0; i<sp.size(); ++i)
dl->AddLine(sp[i], sp[(i+1)%sp.size()], line, (pi==0)?2.5f:2.0f);
for (size_t i=0; i<sp.size()-1; ++i)
dl->AddCircleFilled(sp[i], 3.0f, IM_COL32(255,255,255,255));
}
}
auto& ap = active_profile();
if (!ap.closed && ap.points.size() >= 1) {
std::vector<ImVec2> sp;
for (auto& p : ap.points) sp.push_back(tc({(float)p.x(), (float)p.y()}));
for (size_t i=1; i<sp.size(); ++i)
dl->AddLine(sp[i-1], sp[i], IM_COL32(0,200,255,200), 2.0f);
for (auto& s : sp) dl->AddCircleFilled(s, 3.5f, IM_COL32(100,200,255,255));
ImVec2 mouse = ImGui::GetMousePos();
if (mouse.x > pos.x && mouse.x < end.x && mouse.y > pos.y && mouse.y < end.y)
dl->AddLine(sp.back(), mouse, IM_COL32(100,160,220,120), 1.5f);
}
ImGui::InvisibleButton("canvas", ImVec2(w,h));
if (ImGui::IsItemHovered()) {
ImVec2 m = ImGui::GetMousePos();
Vec2d sk({(m.x-cx)/m_canvas_scale, -(m.y-cy)/m_canvas_scale});
if (m_snap_grid) sk = snap(sk);
auto txt = wxString::Format("X:%.1f Y:%.1f", sk.x(), sk.y()).ToStdString();
dl->AddText({pos.x+4, end.y-16}, IM_COL32(160,160,180,200), txt.c_str());
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left))
handle_canvas_click({(m.x-cx)/m_canvas_scale, -(m.y-cy)/m_canvas_scale});
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
auto& ap2 = active_profile();
if (ap2.points.size() >= 3) {
ap2.points.push_back(ap2.points.front());
ap2.closed = true;
m_active_profile = -1;
}
}
}
}
TopoDS_Shape GLGizmoSketch::build_combined_shape()
{
if (m_profiles.empty() || !m_profiles[0].closed)
throw std::runtime_error("No outer profile");
TopoDS_Wire outer_wire = m_profiles[0].to_occt_wire(m_plane);
BRepBuilderAPI_MakeFace face_maker(outer_wire);
if (!face_maker.IsDone()) throw std::runtime_error("Failed to make outer face");
for (size_t i = 1; i < m_profiles.size(); ++i) {
if (!m_profiles[i].closed) continue;
TopoDS_Wire inner = m_profiles[i].to_occt_wire(m_plane);
face_maker.Add(inner);
}
face_maker.Build();
if (!face_maker.IsDone()) throw std::runtime_error("Failed to build face with holes");
TopoDS_Face face = face_maker.Face();
TopoDS_Shape shape;
if (m_sp.revolve_deg < 360.0 && m_sp.revolve_deg > 0.0) {
gp_Pnt o(m_plane.origin.x(), m_plane.origin.y(), m_plane.origin.z());
gp_Dir xd(m_plane.x_axis.x(), m_plane.x_axis.y(), m_plane.x_axis.z());
gp_Ax1 axis(o, xd);
BRepPrimAPI_MakeRevol rev(face, axis, m_sp.revolve_deg * M_PI / 180.0);
if (!rev.IsDone()) throw std::runtime_error("Revolve failed");
shape = rev.Shape();
} else {
shape = SketchEngine::make_extrude_face(face, m_plane, m_sp.extrude_len, m_sp.extrude_sym);
}
if (m_sp.dressup_enabled) {
if (m_sp.dressup_type == DressUpType::Fillet)
shape = GeometryEngine::apply_fillet(shape, m_sp.dressup_radius, m_sp.dressup_faces);
else
shape = GeometryEngine::apply_chamfer(shape, m_sp.dressup_chamfer_dist, m_sp.dressup_faces);
}
return shape;
}
void GLGizmoSketch::on_render_input_window(float x, float y, float bottom_limit)
{
y = std::min(y, bottom_limit - ImGui::GetWindowHeight());
const float scale = m_parent.get_scale();
ImGuiWrapper::push_toolbar_style(scale);
GizmoImguiSetNextWIndowPos(x, y, ImGuiCond_Always, 0.0f, 0.0f);
GizmoImguiBegin("Sketch", ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove
| ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse
| ImGuiWindowFlags_NoTitleBar);
if (ImGui::CollapsingHeader(UL("Profile"), ImGuiTreeNodeFlags_DefaultOpen)) {
static const char* names[] = {"Line", "Rectangle", "Circle", "Polygon"};
int cur = (int)m_tool;
if (ImGui::Combo("##shape", &cur, names, (int)SketchTool::COUNT)) {
m_tool = (SketchTool)cur;
if (m_tool != SketchTool::Line) build_preset_profile();
}
ImGui::SameLine();
if (m_imgui->button("+##newprofile")) m_active_profile = -1;
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", UL("Start new profile (for holes)"));
if (m_tool == SketchTool::Rectangle) {
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble("W", &m_rect_w,1,10,"%.0f")) build_preset_profile();
ImGui::SameLine();
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble("H", &m_rect_h,1,10,"%.0f")) build_preset_profile();
} else if (m_tool == SketchTool::Circle) {
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble("R", &m_circle_r,1,5,"%.0f")) build_preset_profile();
ImGui::SameLine();
ImGui::SetNextItemWidth(80); if (ImGui::SliderInt("Seg", &m_circle_seg,8,64)) build_preset_profile();
} else if (m_tool == SketchTool::Polygon) {
ImGui::SetNextItemWidth(80); if (ImGui::SliderInt("Sides", &m_poly_sides,3,12)) build_preset_profile();
ImGui::SameLine();
ImGui::SetNextItemWidth(80); if (ImGui::InputDouble("R", &m_poly_r,1,5,"%.0f")) build_preset_profile();
} else {
ImGui::Text("%s", UL("Click on canvas to draw"));
}
ImGui::Checkbox(UL("Snap to grid"), &m_snap_grid);
ImGui::SameLine();
ImGui::SetNextItemWidth(80); ImGui::InputFloat("Step", &m_grid_step, 1, 5, "%.0f mm");
draw_canvas();
if (!m_profiles.empty()) {
ImGui::Text("%s: %zu", UL("Profiles"), m_profiles.size());
for (int i = 0; i < (int)m_profiles.size(); ++i) {
auto& p = m_profiles[i];
ImGui::PushID(i);
bool outer = (i == 0);
ImVec4 col = outer ? ImVec4(0,1,0,1) : ImVec4(1,0.3f,0.3f,1);
const char* label = outer ? "Outer" : "Hole";
ImGui::TextColored(col, "%s %d: %zu pts %s", label, i+1, p.points.size(), p.closed ? "CLOSED" : "");
ImGui::SameLine();
if (ImGui::SmallButton("X")) delete_profile(i);
ImGui::PopID();
}
}
}
ImGui::Separator();
bool is_revolve = false;
bool has_sel = false;
if (ImGui::CollapsingHeader(UL("Operation"), ImGuiTreeNodeFlags_DefaultOpen)) {
static int pi = 0;
if (ImGui::Combo(UL("Plane"), &pi, "XY (Top)\0XZ (Front)\0YZ (Side)\0"))
m_plane = (pi==0) ? SketchPlane::XY() : (pi==1) ? SketchPlane::XZ() : SketchPlane::YZ();
is_revolve = (m_sp.revolve_deg > 0 && m_sp.revolve_deg < 360);
ImGui::SetNextItemWidth(100);
if (ImGui::InputDouble(UL("Revolve deg"), &m_sp.revolve_deg, 15, 90, "%.0f")) {
if (m_sp.revolve_deg > 360) m_sp.revolve_deg = 360;
if (m_sp.revolve_deg < 0) m_sp.revolve_deg = 0;
}
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", UL("Set to 0 for extrude, >0 for revolve"));
if (!is_revolve) {
ImGui::SetNextItemWidth(100);
ImGui::InputDouble(UL("Length"), &m_sp.extrude_len, 0.5, 5, "%.1f mm");
ImGui::SameLine();
ImGui::Checkbox(UL("Symmetric"), &m_sp.extrude_sym);
}
has_sel = !m_parent.get_selection().is_empty();
if (has_sel) {
if (ImGui::Checkbox(UL("Pocket (cut)"), &m_sp.is_pocket))
if (m_sp.is_pocket) m_sp.dressup_enabled = false;
} else m_sp.is_pocket = false;
}
ImGui::Separator();
if (!m_sp.is_pocket && ImGui::CollapsingHeader(UL("Fillet / Chamfer"))) {
ImGui::Checkbox(UL("Enable"), &m_sp.dressup_enabled);
if (m_sp.dressup_enabled) {
static const char* dn[] = {"Fillet", "Chamfer"};
int du = (int)m_sp.dressup_type;
ImGui::SetNextItemWidth(100);
if (ImGui::Combo("##dtype", &du, dn, 2)) m_sp.dressup_type = (DressUpType)du;
static const char* fn[] = {"All edges", "Top edges", "Bottom edges", "Lateral edges"};
int fg = (int)m_sp.dressup_faces;
ImGui::SetNextItemWidth(140);
ImGui::Combo(UL("Edges"), &fg, fn, 4); m_sp.dressup_faces = (FaceGroup)fg;
ImGui::SetNextItemWidth(100);
if (m_sp.dressup_type == DressUpType::Fillet)
ImGui::InputDouble(UL("Radius"), &m_sp.dressup_radius, 0.1, 1, "%.1f mm");
else
ImGui::InputDouble(UL("Distance"), &m_sp.dressup_chamfer_dist, 0.1, 1, "%.1f mm");
}
}
ImGui::Separator();
bool ok = has_closed_profile();
if (ok) ImGui::TextColored({0,1,0,1}, "%zu %s", m_profiles.size(), UL("closed profile(s)"));
else ImGui::TextColored({0.6f,0.6f,0.6f,1}, "%s", UL("Draw a closed profile to enable"));
auto btn = [&](const char* label, bool enabled) {
if (!enabled) { ImGui::PushItemFlag(ImGuiItemFlags_Disabled,true); ImGui::PushStyleColor(ImGuiCol_Button,{0.25f,0.25f,0.25f,1}); }
bool clicked = ImGui::Button(label, {-1,0});
if (!enabled) { ImGui::PopStyleColor(); ImGui::PopItemFlag(); }
return clicked && enabled;
};
if (m_sp.is_pocket && has_sel) {
if (btn(_u8L("Pocket (Cut)").c_str(), ok)) apply_pocket();
} else if (is_revolve) {
if (btn(_u8L("Revolve").c_str(), ok)) apply_revolve();
} else {
if (btn(_u8L("Extrude").c_str(), ok)) apply_extrude();
}
if (ImGui::Button(_u8L("Clear All").c_str(), {-1,0})) clear_all();
if (ImGui::Button(_u8L("Close").c_str(), {-1,0})) m_parent.reset_all_gizmos();
GizmoImguiEnd();
ImGuiWrapper::pop_toolbar_style();
}
void GLGizmoSketch::apply_extrude()
{
try {
TopoDS_Shape shape = build_combined_shape();
TriangleMesh mesh = SketchEngine::tessellate(shape, m_sp.linear_deflection);
if (mesh.its.indices.empty()) throw std::runtime_error("Empty result");
wxGetApp().plater()->take_snapshot("Sketch Extrude");
ModelObject* mo = wxGetApp().model().add_object();
mo->name = "Extrusion";
mo->add_volume(std::move(mesh))->set_new_unique_id();
mo->ensure_on_bed();
wxGetApp().plater()->update();
clear_all();
} catch (const std::exception& e) {
wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::ErrorNotificationLevel, std::string("Extrude: ")+e.what());
}
}
void GLGizmoSketch::apply_revolve()
{
try {
TopoDS_Shape shape = build_combined_shape();
TriangleMesh mesh = SketchEngine::tessellate(shape, m_sp.linear_deflection);
if (mesh.its.indices.empty()) throw std::runtime_error("Empty result");
wxGetApp().plater()->take_snapshot("Sketch Revolve");
ModelObject* mo = wxGetApp().model().add_object();
mo->name = "Revolve";
mo->add_volume(std::move(mesh))->set_new_unique_id();
mo->ensure_on_bed();
wxGetApp().plater()->update();
clear_all();
} catch (const std::exception& e) {
wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::ErrorNotificationLevel, std::string("Revolve: ")+e.what());
}
}
void GLGizmoSketch::apply_pocket()
{
try {
Selection& sel = m_parent.get_selection();
int obj_idx = sel.get_object_idx();
if (obj_idx < 0) throw std::runtime_error("No object selected");
ModelObject* mo = wxGetApp().model().objects[obj_idx];
TopoDS_Wire outer = m_profiles[0].to_occt_wire(m_plane);
BRepBuilderAPI_MakeFace fm(outer);
if (!fm.IsDone()) throw std::runtime_error("Face failed");
for (size_t i = 1; i < m_profiles.size(); ++i)
if (m_profiles[i].closed) fm.Add(m_profiles[i].to_occt_wire(m_plane));
fm.Build();
if (!fm.IsDone()) throw std::runtime_error("Face with holes failed");
TopoDS_Shape tool = SketchEngine::make_extrude_face(fm.Face(), m_plane, m_sp.extrude_len + 5.0, false);
TriangleMesh tool_mesh = SketchEngine::tessellate(tool, m_sp.linear_deflection);
if (tool_mesh.its.indices.empty()) throw std::runtime_error("Tool mesh empty");
wxGetApp().plater()->take_snapshot("Sketch Pocket");
mo->add_volume(std::move(tool_mesh), ModelVolumeType::NEGATIVE_VOLUME)->set_new_unique_id();
mo->ensure_on_bed();
wxGetApp().plater()->update();
clear_all();
wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::RegularNotificationLevel, UL("Pocket added (negative volume)"));
} catch (const std::exception& e) {
wxGetApp().notification_manager()->push_notification(NotificationType::CustomNotification, NotificationManager::NotificationLevel::ErrorNotificationLevel, std::string("Pocket: ")+e.what());
}
}
} // namespace GUI
} // namespace Slic3r
+74
View File
@@ -0,0 +1,74 @@
#ifndef slic3r_GLGizmoSketch_hpp_
#define slic3r_GLGizmoSketch_hpp_
#include "GLGizmoBase.hpp"
#include "GLGizmosCommon.hpp"
#include "libslic3r/CAD/SketchEngine.hpp"
#include <imgui/imgui.h>
namespace Slic3r {
namespace GUI {
enum class SketchTool { Line, Rectangle, Circle, Polygon, COUNT };
class GLGizmoSketch : public GLGizmoBase
{
public:
GLGizmoSketch(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id);
bool on_mouse(const wxMouseEvent& mouse_event) override;
protected:
bool on_init() override;
std::string on_get_name() const override;
bool on_is_activable() const override;
void on_render() override;
void on_set_state() override;
CommonGizmosDataID on_get_requirements() const override;
void on_render_input_window(float x, float y, float bottom_limit) override;
void on_load(cereal::BinaryInputArchive& ar) override;
void on_save(cereal::BinaryOutputArchive& ar) const override;
private:
SketchTool m_tool{SketchTool::Line};
std::vector<SketchProfile> m_profiles; // multiple profiles (outer + holes)
SketchPlane m_plane{SketchPlane::XY()};
SketchParams m_sp;
// Shape presets
double m_rect_w{20}, m_rect_h{15};
double m_circle_r{10}; int m_circle_seg{32};
int m_poly_sides{6}; double m_poly_r{10};
// Canvas
std::vector<ImVec2> m_canvas_points;
Vec2d m_canvas_center{0,0};
float m_canvas_scale{5.0f};
bool m_snap_grid{true};
float m_grid_step{5.0f};
// Current profile being drawn
int m_active_profile{-1};
SketchProfile& active_profile();
bool has_closed_profile() const;
void build_preset_profile();
void add_closed_profile();
void delete_profile(int idx);
void clear_all();
TopoDS_Shape build_combined_shape(); // all profiles as face with holes
void apply_extrude();
void apply_revolve();
void apply_pocket();
void draw_canvas();
void handle_canvas_click(ImVec2 pos);
Vec2d snap(Vec2d pt) const;
};
} // namespace GUI
} // namespace Slic3r
#endif // slic3r_GLGizmoSketch_hpp_
+4
View File
@@ -410,6 +410,10 @@ void ObjectClipper::set_position_by_ratio(double pos, bool keep_normal, bool ver
void ObjectClipper::set_range_and_pos(const Vec3d& cpl_normal, double cpl_offset, double pos)
{
// Called every frame by GLGizmoCut3D::on_render(), usually with the plane already set.
if (m_clp && *m_clp == ClippingPlane(cpl_normal, cpl_offset) && m_clp_ratio == pos)
return;
m_clp.reset(new ClippingPlane(cpl_normal, cpl_offset));
m_clp_ratio = pos;
get_pool()->get_canvas()->set_as_dirty();
+56 -7
View File
@@ -27,11 +27,17 @@
#include "slic3r/GUI/Gizmos/GLGizmoSVG.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoMeshBoolean.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoAssembly.hpp"
#ifdef SLIC3R_CAD
#include "slic3r/GUI/Gizmos/GLGizmoPrimitive.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoSketch.hpp"
#endif
#include "libslic3r/format.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/PresetBundle.hpp"
#include <boost/functional/hash.hpp>
#include <wx/glcanvas.h>
namespace Slic3r {
@@ -176,6 +182,14 @@ void GLGizmosManager::switch_gizmos_icon_filename()
case (EType::BrimEars):
gizmo->set_icon_filename(m_is_dark ? "toolbar_brimears_dark.svg" : "toolbar_brimears.svg");
break;
#ifdef SLIC3R_CAD
case (EType::Primitive):
gizmo->set_icon_filename(m_is_dark ? "toolbar_modifier_cube_dark.svg" : "toolbar_modifier_cube.svg");
break;
case (EType::Sketch):
gizmo->set_icon_filename(m_is_dark ? "toolbar_sketch_dark.svg" : "toolbar_sketch.svg");
break;
#endif
}
}
@@ -219,6 +233,14 @@ bool GLGizmosManager::init()
m_gizmos.emplace_back(new GLGizmoAssembly(m_parent, m_is_dark ? "toolbar_assembly_dark.svg" : "toolbar_assembly.svg", EType::Assembly));
m_gizmos.emplace_back(new GLGizmoSimplify(m_parent, "reduce_triangles.svg", EType::Simplify));
m_gizmos.emplace_back(new GLGizmoBrimEars(m_parent, m_is_dark ? "toolbar_brimears_dark.svg" : "toolbar_brimears.svg", EType::BrimEars));
#ifdef SLIC3R_CAD
// Registered last: Primitive and Sketch are the final entries before Undefined, so
// omitting them leaves every preceding m_gizmos index (indexed by EType) untouched.
if (wxGetApp().is_enable_cad_feature()) {
m_gizmos.emplace_back(new GLGizmoPrimitive(m_parent, m_is_dark ? "toolbar_modifier_cube_dark.svg" : "toolbar_modifier_cube.svg", static_cast<unsigned int>(Primitive)));
m_gizmos.emplace_back(new GLGizmoSketch(m_parent, m_is_dark ? "toolbar_sketch_dark.svg" : "toolbar_sketch.svg", static_cast<unsigned int>(Sketch)));
}
#endif
//m_gizmos.emplace_back(new GLGizmoSlaSupports(m_parent, "sla_supports.svg", sprite_id++));
//m_gizmos.emplace_back(new GLGizmoFaceDetector(m_parent, "face recognition.svg", sprite_id++));
//m_gizmos.emplace_back(new GLGizmoHollow(m_parent, "hollow.svg", sprite_id++));
@@ -611,6 +633,7 @@ void GLGizmosManager::render_painter_assemble_view() const
m_assemble_view_data->model_objects_clipper()->render_cut();
}
// The icon bar, drawn with GL.
void GLGizmosManager::render_overlay()
{
if (!m_enabled)
@@ -619,7 +642,27 @@ void GLGizmosManager::render_overlay()
if (m_icons_texture_dirty)
generate_icons_texture();
do_render_overlay();
do_render_overlay(true);
}
// The open gizmo's settings panel, ImGui.
void GLGizmosManager::render_overlay_input_window()
{
if (!m_enabled)
return;
do_render_overlay(false);
}
size_t GLGizmosManager::get_overlay_state_hash() const
{
size_t hash = 0;
boost::hash_combine(hash, m_enabled);
boost::hash_combine(hash, (int)m_hover);
boost::hash_combine(hash, (int)m_current);
boost::hash_combine(hash, (int)m_highlight.first);
boost::hash_combine(hash, m_highlight.second);
return hash;
}
std::string GLGizmosManager::get_tooltip() const
@@ -1202,7 +1245,9 @@ void GLGizmosManager::render_arrow(const GLCanvas3D& parent, EType highlighted_t
//BBS: GUI refactor: GLToolbar&&Gizmo adjust
//when rendering, {0, 0} is at the center, {-0.5, 0.5} at the left-top
void GLGizmosManager::do_render_overlay() const
// draw_icons selects the icon bar (GL) or the open gizmo's input window (ImGui), placed by the same
// layout walk.
void GLGizmosManager::do_render_overlay(bool draw_icons) const
{
const std::vector<size_t> selectable_idxs = get_selectable_idxs();
if (selectable_idxs.empty())
@@ -1241,7 +1286,8 @@ void GLGizmosManager::do_render_overlay() const
}
float top_y = 1.0f;
render_background(top_x, top_y, top_x + width, top_y - height, border_w, border_h);
if (draw_icons)
render_background(top_x, top_y, top_x + width, top_y - height, border_w, border_h);
top_x += border_w;
top_y -= border_h;
@@ -1278,7 +1324,8 @@ void GLGizmosManager::do_render_overlay() const
const float v_top = v_offset + sprite_id * dv;
const float v_bottom = v_top + dv - v_offset;
GLTexture::render_sub_texture(icons_texture_id, top_x, top_x + icons_size_x, top_y - icons_size_y, top_y, { { u_left, v_bottom }, { u_right, v_bottom }, { u_right, v_top }, { u_left, v_top } });
if (draw_icons)
GLTexture::render_sub_texture(icons_texture_id, top_x, top_x + icons_size_x, top_y - icons_size_y, top_y, { { u_left, v_bottom }, { u_right, v_bottom }, { u_right, v_top }, { u_left, v_top } });
if (idx == m_current
// Orca: Show Svg dialog at the same place as emboss gizmo
|| (m_current == Svg && idx == Emboss)) {
@@ -1286,7 +1333,8 @@ void GLGizmosManager::do_render_overlay() const
//render_input_window uses a different coordination(imgui)
//1. no need to scale by camera zoom, set {0,0} at left-up corner for imgui
//gizmo->render_input_window(width, 0.5f * cnv_h - zoomed_top_y * zoom, toolbar_top);
m_gizmos[m_current]->render_input_window(0.5 * cnv_w + 0.5f * top_x * cnv_w, get_scaled_total_height(), cnv_h);
if (!draw_icons)
m_gizmos[m_current]->render_input_window(0.5 * cnv_w + 0.5f * top_x * cnv_w, get_scaled_total_height(), cnv_h);
is_render_current = true;
}
@@ -1294,7 +1342,7 @@ void GLGizmosManager::do_render_overlay() const
}
// BBS simplify gizmo is not a selected gizmo and need to render input window
if (!is_render_current && m_current != Undefined) {
if (!draw_icons && !is_render_current && m_current != Undefined) {
m_gizmos[m_current]->render_input_window(0.5 * cnv_w + 0.5f * top_x * cnv_w, get_scaled_total_height(), cnv_h);
}
}
@@ -1326,7 +1374,8 @@ GLGizmoBase* GLGizmosManager::get_current() const
GLGizmoBase* GLGizmosManager::get_gizmo(GLGizmosManager::EType type) const
{
return ((type == Undefined) || m_gizmos.empty()) ? nullptr : m_gizmos[type].get();
// m_gizmos ends before the enum does when the CAD gizmos are not registered.
return type < m_gizmos.size() ? m_gizmos[type].get() : nullptr;
}
GLGizmosManager::EType GLGizmosManager::get_gizmo_from_name(const std::string& gizmo_name) const
+10 -1
View File
@@ -90,6 +90,12 @@ public:
Assembly,
Simplify,
BrimEars,
#ifdef SLIC3R_CAD
// Both need the CAD kernel (GeometryEngine); keep them last so that with
// SLIC3R_CAD off the enum matches upstream's numbering exactly.
Primitive,
Sketch,
#endif
//SlaSupports,
// BBS
//FaceRecognition,
@@ -290,6 +296,9 @@ public:
void render_painter_assemble_view() const;
void render_overlay();
void render_overlay_input_window();
// Hash of the state render_overlay() draws from: enabled, hover, current and highlight.
size_t get_overlay_state_hash() const;
void render_arrow(const GLCanvas3D& parent, EType highlighted_type) const;
@@ -323,7 +332,7 @@ private:
void render_background(float left, float top, float right, float bottom, float border_w, float border_h) const;
void do_render_overlay() const;
void do_render_overlay(bool draw_icons) const;
bool generate_icons_texture();
+47 -2
View File
@@ -505,6 +505,23 @@ bool ImGuiWrapper::update_key_data(wxKeyEvent &evt)
if (evt.GetEventType() == wxEVT_CHAR) {
// Char event
const auto key = evt.GetUnicodeKey();
// THE MEASUREMENT THAT CANNOT LIE. This is the ONLY place in the application where ImGui
// is ever handed a character, so an ImGui text field that stays empty while reporting
// itself active has exactly two possible causes, and this line separates them: no output
// at all means the wxEVT_CHAR never reached the GL canvas (a focus problem, upstream of
// ImGui entirely), while output with unicode=0 means the character arrived empty and is
// being dropped right here.
//
// It lives here rather than on the canvas because a probe bound on the canvas CANNOT
// answer this: GLCanvas3D::on_char is bound later than any constructor-time probe, wx
// runs handlers in reverse bind order, and on_char returns without Skip() whenever this
// function returns true — so such a probe stays silent whether or not the key arrived.
// A day was lost to reading that silence as evidence.
if (std::getenv("ORCA_CAD_UXTRACE")) {
fprintf(stderr, "[UX] imgui_char unicode=%d keycode=%d want_text=%d\n",
(int) key, evt.GetKeyCode(), (int) io.WantTextInput);
fflush(stderr);
}
if (key != 0) {
io.AddInputCharacter(key);
}
@@ -573,11 +590,39 @@ void ImGuiWrapper::new_frame()
// BBL: end copy & paste
}
void ImGuiWrapper::render()
ImDrawData* ImGuiWrapper::end_frame()
{
ImGui::Render();
render_draw_data(ImGui::GetDrawData());
m_new_frame_open = false;
return ImGui::GetDrawData();
}
void ImGuiWrapper::render(ImDrawData* draw_data)
{
render_draw_data(draw_data);
}
ImGuiID ImGuiWrapper::draw_data_signature(const ImDrawData* draw_data)
{
ImGuiID hash = 0;
if (draw_data == nullptr)
return hash;
for (int i = 0; i < draw_data->CmdListsCount; ++i) {
const ImDrawList* list = draw_data->CmdLists[i];
hash = ImHashData(list->VtxBuffer.Data, list->VtxBuffer.Size * sizeof(ImDrawVert), hash);
hash = ImHashData(list->IdxBuffer.Data, list->IdxBuffer.Size * sizeof(ImDrawIdx), hash);
// ImDrawCmd has padding, and a hovered ImageButton3() differs only in TextureId.
for (const ImDrawCmd& cmd : list->CmdBuffer) {
hash = ImHashData(&cmd.ClipRect, sizeof(cmd.ClipRect), hash);
hash = ImHashData(&cmd.TextureId, sizeof(cmd.TextureId), hash);
hash = ImHashData(&cmd.VtxOffset, sizeof(cmd.VtxOffset), hash);
hash = ImHashData(&cmd.IdxOffset, sizeof(cmd.IdxOffset), hash);
hash = ImHashData(&cmd.ElemCount, sizeof(cmd.ElemCount), hash);
hash = ImHashData(&cmd.UserCallback, sizeof(cmd.UserCallback), hash);
}
}
return hash;
}
ImVec2 ImGuiWrapper::calc_text_size(std::string_view text,
+5 -1
View File
@@ -98,7 +98,11 @@ public:
const ImWchar *get_glyph_ranges() const { return m_glyph_ranges; } // language specific
void new_frame();
void render();
// Ends the frame and returns its draw data without drawing it.
ImDrawData* end_frame();
void render(ImDrawData* draw_data);
// Hash of every draw list's vertices, indices and commands.
static ImGuiID draw_data_signature(const ImDrawData* draw_data);
float scaled(float x) const { return x * m_font_size; }
ImVec2 scaled(float x, float y) const { return ImVec2(x * m_font_size, y * m_font_size); }
+3 -2
View File
@@ -198,6 +198,9 @@ void KBShortcutsDialog::fill_shortcuts()
// Switch table page
{ ctrl + L("Tab"), L("Switch table page")},
// Open speed dial
{ L_CONTEXT("Space", "Keyboard Shortcut"), L("Open speed dial") },
{ alt + "1..9,0", L("Run a Speed Dial favourite (while the Speed Dial is open)") },
//DEL
#ifdef __APPLE__
{"fn+⌫", L("Delete Selected")},
@@ -268,8 +271,6 @@ void KBShortcutsDialog::fill_shortcuts()
{ "O", L("Zoom out") },
{ "V", L("Toggle printable for object/part") },
{ L_CONTEXT("Tab", "Keyboard Shortcut"), L("Switch between Prepare/Preview") },
{ L_CONTEXT("Space", "Keyboard Shortcut"), L("Open actions speed dial") },
};
m_full_shortcuts.push_back({ { _L("Plater"), "" }, plater_shortcuts });
+201 -94
View File
@@ -1,6 +1,7 @@
#include "MainFrame.hpp"
#include <wx/panel.h>
#include <wx/textentry.h>
#include <wx/notebook.h>
#include <wx/listbook.h>
#include <wx/simplebook.h>
@@ -37,12 +38,18 @@
#include "I18N.hpp"
#include "GLCanvas3D.hpp"
#include "Plater.hpp"
#ifdef SLIC3R_CAD
#include "slic3r/GUI/CAD/DesignPanel.hpp"
#include "slic3r/GUI/CAD/McpControl.hpp"
#endif
#include "WebViewDialog.hpp"
#include "../Utils/Process.hpp"
// BBS
#include "PartPlate.hpp"
#include "Preferences.hpp"
#include "Widgets/Button.hpp"
#include "Widgets/ProgressDialog.hpp"
#include "Widgets/StaticBox.hpp"
#include "BindDialog.hpp"
#include "../Utils/MacDarkMode.hpp"
#include "../Utils/NetworkAgentFactory.hpp"
@@ -103,6 +110,31 @@ enum class ERescaleTarget
SettingsDialog
};
namespace {
// Space opens the speed dial, but it is the activation key for buttons, checkboxes and other
// controls. CHAR_HOOK runs before the focused child, so only take Space when the focused window has
// no keyboard-activation meaning of its own. Canvases (GLCanvas3D) and panels are not controls and
// fall through to "open"; the Notebook itself does too, so Space still opens the dial on any page.
bool focus_keeps_space(wxWindow* focus)
{
if (!focus)
return false;
if (dynamic_cast<wxTextEntryBase*>(focus))
return true; // typing a space into a text field
if (dynamic_cast<wxWebView*>(focus))
return true; // web content scrolls and hosts its own text fields
if (dynamic_cast<::Button*>(focus))
return true; // custom button: Space clicks it (it is a wxWindow, not a wxControl)
if (dynamic_cast<StaticBox*>(focus))
return true; // custom composites (ComboBox, SpinInput, ...) activate with Space and are wxWindow
if (dynamic_cast<wxControl*>(focus) && !dynamic_cast<Notebook*>(focus))
return true; // stock button/checkbox/choice/list/etc. keep Space
return false;
}
} // namespace
#ifdef __WXGTK__
// A thin transparent panel placed at a window edge to handle resize.
// Works regardless of underlying content (GLCanvas3D, wxWebView, etc.)
@@ -698,6 +730,22 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
}
return;}
#endif
// Orca: open the speed dial from any page with a bare Space. Only when no modifier is held (so
// editing shortcuts like Ctrl+Shift+Space in the canvas still reach it) and the focused window
// doesn't use Space to activate itself (buttons, checkboxes, list/choice controls, text fields),
// so a bare Space there still clicks/toggles instead of being hijacked. Gated by a preference
// (default on) so users can hand Space back to the focused control entirely.
if (wxGetApp().app_config->get_bool("enable_speed_dial") && !evt.CmdDown() && !evt.ShiftDown() &&
!evt.AltDown() && evt.GetKeyCode() == WXK_SPACE) {
if (focus_keeps_space(wxWindow::FindFocus())) {
evt.Skip(); // let the focused control keep Space
return;
}
// Defer out of the native key-event stack: open_speed_dial() may create a WebView and
// run script, the same window work the codebase avoids doing on native callbacks.
this->CallAfter([] { wxGetApp().open_speed_dial(); });
return;
}
if (evt.CmdDown() && evt.GetKeyCode() == 'R') { if (m_slice_enable) { wxGetApp().plater()->update(true, true); wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_SLICE_PLATE)); this->m_tabpanel->SelectPageByName(TAB_ID_PREVIEW); } return; }
if (evt.CmdDown() && evt.ShiftDown() && evt.GetKeyCode() == 'G') {
m_plater->apply_background_progress();
@@ -1019,7 +1067,15 @@ void MainFrame::update_layout()
// Right after Home — or first, when there is no Home tab (PositionAfter() would
// append instead, and by now the other built-in tabs are already in place).
const int home_idx = m_tabpanel->FindPageByName(TAB_ID_HOME);
const size_t prepare_pos = (home_idx == wxNOT_FOUND) ? 0 : static_cast<size_t>(home_idx) + 1;
size_t prepare_pos = (home_idx == wxNOT_FOUND) ? 0 : static_cast<size_t>(home_idx) + 1;
#ifdef SLIC3R_CAD
// Design sits between Home and Prepare, so it goes in first and pushes Prepare along.
// The page only exists when the experimental CAD feature is enabled.
if (m_design_page != nullptr) {
m_design_page->Reparent(m_tabpanel);
m_tabpanel->InsertPage(prepare_pos++, TAB_ID_DESIGN, m_design_page, _L("Design"), "tab_design_active");
}
#endif
m_tabpanel->InsertPage(prepare_pos, TAB_ID_PREPARE, m_plater, _L("Prepare"), "tab_3d_active");
m_tabpanel->InsertPage(prepare_pos + 1, TAB_ID_PREVIEW, m_plater, _L("Preview"), "tab_preview_active");
m_main_sizer->Add(m_tabpanel, 1, wxEXPAND | wxTOP, 0);
@@ -1237,6 +1293,19 @@ void MainFrame::show_option(bool show)
}
}
#ifdef SLIC3R_CAD
DesignPanel* MainFrame::ensure_design_panel()
{
if (m_design_panel == nullptr && m_design_page != nullptr) {
wxBusyCursor busy;
m_design_panel = new DesignPanel(m_design_page);
m_design_page->GetSizer()->Add(m_design_panel, 1, wxEXPAND);
m_design_page->Layout();
}
return m_design_panel;
}
#endif
void MainFrame::init_tabpanel() {
// wxNB_NOPAGETHEME: Disable Windows Vista theme for the Notebook background. The theme performance is terrible on
// Windows 10 with multiple high resolution displays connected.
@@ -1277,9 +1346,26 @@ void MainFrame::init_tabpanel() {
}
//else if (panel == m_param_panel)
// m_param_panel->OnActivate();
#ifdef SLIC3R_CAD
else if (m_design_page != nullptr && panel == m_design_page) {
// Built on first activation, never at startup: the panel creates several hundred
// controls and its own GL canvas, which a user who does not open the tab should
// not pay for.
ensure_design_panel();
// Re-sync the Design bed to the active printer: the panel is built before the
// printer profile is fully applied, so its bed must refresh on activation or the
// grid (true bed) spills past the stale default bed quad.
m_design_panel->on_tab_shown();
}
#endif
else if (panel == m_monitor) {
//monitor
}
#ifdef SLIC3R_CAD
// Any page that is not Design takes the Design status line down with it — see
// DesignPanel::on_tab_hidden for why the popup does not follow the page on its own.
if (m_design_panel != nullptr && panel != m_design_page) m_design_panel->on_tab_hidden();
#endif
#ifndef __APPLE__
if (m_last_selected_tab == TAB_ID_PREPARE) {
m_topbar->EnableUndoRedoItems();
@@ -1310,6 +1396,20 @@ void MainFrame::init_tabpanel() {
wxGetApp().plater_ = m_plater;
#ifdef SLIC3R_CAD
// Stand-in page for the Design tab. The real DesignPanel is built into it the first time
// the tab is selected (see the page-changed handler above), so nothing it constructs sits
// on the startup path. The experimental feature is off by default, and when it is off the
// page is never created, so the tab does not appear at all (the preference takes effect on
// the next start, like the other feature toggles).
if (wxGetApp().is_enable_cad_feature()) {
m_design_page = new wxPanel(this);
m_design_page->SetSizer(new wxBoxSizer(wxVERTICAL));
m_design_page->Hide();
start_mcp_control_if_enabled(); // opens the MCP socket iff ORCA_CAD_MCP is set
}
#endif
create_preset_tabs();
//BBS add pages
@@ -3288,6 +3388,11 @@ void MainFrame::init_menubar_as_editor()
wxGetApp().open_preferences();
},
"", nullptr, []() { return true; }, this, 1);
parent_menu->AppendSeparator();
append_menu_item(
parent_menu, wxID_ANY, _L("Open speed dial...") + sep + "Space", "",
[](wxCommandEvent &) { wxGetApp().open_speed_dial(); },
"", nullptr, []() { return true; }, this);
//parent_menu->Insert(1, preference_item);
#endif
// Help menu
@@ -3312,7 +3417,13 @@ void MainFrame::init_menubar_as_editor()
auto top_menu = m_topbar->GetTopMenu();
top_menu->AppendSeparator();
append_menu_item(
append_menu_item(
top_menu, wxID_ANY, _L("Open speed dial...") + "\t" + "Space", "",
[](wxCommandEvent &) { wxGetApp().open_speed_dial(); },
"", nullptr, []() { return true; }, this);
top_menu->AppendSeparator();
append_menu_item(
top_menu, wxID_ANY, _L("Preset Bundle") + "\t", "",
[this](wxCommandEvent &) {
// Orca: Use GUI_App::open_preferences instead of direct call so windows associations are updated on exit
@@ -3358,88 +3469,51 @@ void MainFrame::init_menubar_as_editor()
// Temperature
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Temperature"), _L("Temperature Calibration"),
[this](wxCommandEvent&) {
if (!m_temp_calib_dlg)
m_temp_calib_dlg = new Temp_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_temp_calib_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::Temperature); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Max Volumetric Speed
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Max flowrate"), _L("Max flowrate"),
[this](wxCommandEvent&) {
if (!m_vol_test_dlg)
m_vol_test_dlg = new MaxVolumetricSpeed_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_vol_test_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::MaxVolumetric); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Pressure Advance
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Pressure advance"), _L("Pressure advance"),
[this](wxCommandEvent&) {
if (!m_pa_calib_dlg)
m_pa_calib_dlg = new PA_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_pa_calib_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::PressureAdvance); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Flow rate (Wizard Dialog)
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Flow ratio"), _L("Flow Rate Calibration"),
[this](wxCommandEvent&) {
if (!m_plater) return;
if (!m_flow_rate_calib_dlg)
m_flow_rate_calib_dlg = new FlowRateCalibrationDialog((wxWindow*)this, wxID_ANY, m_plater);
m_flow_rate_calib_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::FlowRatio); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Retraction
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Retraction"), _L("Retraction"),
[this](wxCommandEvent&) {
if (!m_retraction_calib_dlg)
m_retraction_calib_dlg = new Retraction_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_retraction_calib_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::Retraction); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Cornering
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Cornering"), _L("Cornering calibration"),
[this](wxCommandEvent&) {
auto dlg = new Cornering_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::Cornering); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Input Shaping (with submenu)
auto input_shaping_menu = new wxMenu();
append_menu_item(
input_shaping_menu, wxID_ANY, _L("Input Shaping Frequency"), _L("Input Shaping Frequency"),
[this](wxCommandEvent&) {
auto dlg = new Input_Shaping_Freq_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
},
[this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingFreq); },
"", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
append_menu_item(
input_shaping_menu, wxID_ANY, _L("Input Shaping Damping/zeta factor"), _L("Input Shaping Damping/zeta factor"),
[this](wxCommandEvent&) {
auto dlg = new Input_Shaping_Damp_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
},
[this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingDamp); },
"", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
m_topbar->GetCalibMenu()->AppendSubMenu(input_shaping_menu, _L("Input Shaping"));
// VFA
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("VFA"), _L("VFA"),
[this](wxCommandEvent&) {
if (!m_vfa_test_dlg)
m_vfa_test_dlg = new VFA_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_vfa_test_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::VFA); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// help
@@ -3450,6 +3524,10 @@ void MainFrame::init_menubar_as_editor()
#else
// On Mac, the Apple menu ignores non-standard custom items, so add Preset Bundle to the File menu
fileMenu->AppendSeparator();
append_menu_item(
fileMenu, wxID_ANY, _L("Open speed dial...") + sep + "Space", "",
[](wxCommandEvent&) { wxGetApp().open_speed_dial(); },
"", nullptr, []() { return true; }, this);
append_menu_item(
fileMenu, wxID_ANY, _L("Preset Bundle"), "",
[this](wxCommandEvent&) {
@@ -3496,89 +3574,52 @@ void MainFrame::init_menubar_as_editor()
// Temperature
append_menu_item(calib_menu, wxID_ANY, _L("Temperature"), _L("Temperature"),
[this](wxCommandEvent&) {
if (!m_temp_calib_dlg)
m_temp_calib_dlg = new Temp_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_temp_calib_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::Temperature); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Max Volumetric Speed
append_menu_item(calib_menu, wxID_ANY, _L("Max flowrate"), _L("Max flowrate"),
[this](wxCommandEvent&) {
if (!m_vol_test_dlg)
m_vol_test_dlg = new MaxVolumetricSpeed_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_vol_test_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::MaxVolumetric); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Pressure Advance
append_menu_item(calib_menu, wxID_ANY, _L("Pressure advance"), _L("Pressure advance"),
[this](wxCommandEvent&) {
if (!m_pa_calib_dlg)
m_pa_calib_dlg = new PA_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_pa_calib_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::PressureAdvance); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Flowrate (with submenu)
// ORCA: Flow rate (Wizard Dialog)
append_menu_item(calib_menu, wxID_ANY, _L("Flow ratio"), _L("Flow Rate Calibration"),
[this](wxCommandEvent&) {
if (!m_plater) return;
if (!m_flow_rate_calib_dlg)
m_flow_rate_calib_dlg = new FlowRateCalibrationDialog((wxWindow*)this, wxID_ANY, m_plater);
m_flow_rate_calib_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::FlowRatio); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Retraction
append_menu_item(calib_menu, wxID_ANY, _L("Retraction"), _L("Retraction"),
[this](wxCommandEvent&) {
if (!m_retraction_calib_dlg)
m_retraction_calib_dlg = new Retraction_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_retraction_calib_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::Retraction); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Cornering
append_menu_item(calib_menu, wxID_ANY, _L("Cornering"), _L("Cornering calibration"),
[this](wxCommandEvent&) {
auto dlg = new Cornering_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::Cornering); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Input Shaping (with submenu)
auto input_shaping_menu = new wxMenu();
append_menu_item(
input_shaping_menu, wxID_ANY, _L("Input Shaping Frequency"), _L("Input Shaping Frequency"),
[this](wxCommandEvent&) {
auto dlg = new Input_Shaping_Freq_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
},
[this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingFreq); },
"", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
append_menu_item(
input_shaping_menu, wxID_ANY, _L("Input Shaping Damping/zeta factor"), _L("Input Shaping Damping/zeta factor"),
[this](wxCommandEvent&) {
auto dlg = new Input_Shaping_Damp_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
},
[this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingDamp); },
"", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
calib_menu->AppendSubMenu(input_shaping_menu, _L("Input Shaping"));
// VFA
append_menu_item(calib_menu, wxID_ANY, _L("VFA"), _L("VFA"),
[this](wxCommandEvent&) {
if (!m_vfa_test_dlg)
m_vfa_test_dlg = new VFA_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_vfa_test_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::VFA); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// help
append_menu_item(calib_menu, wxID_ANY, _L("Calibration Guide"), _L("Calibration Guide"),
@@ -4352,6 +4393,72 @@ void MainFrame::technology_changed()
m_menubar->SetMenuLabel(id, pt == ptSLA ? _omitL("Material Settings") : _L("Filament settings"));
}
// Opens the calibration wizard for `calib_kind`. Single source of truth for the wizard lifecycle:
// the Calibration menu handlers and the Speed Dial native commands both call it. Most wizards are
// cached members reused across launches; cornering/input-shaping build a fresh transient dialog.
// Call while the Prepare (3D) panel is shown.
void MainFrame::run_calibration(CalibKind calib_kind)
{
switch (calib_kind) {
case CalibKind::Temperature: {
if (!m_temp_calib_dlg)
m_temp_calib_dlg = new Temp_Calibration_Dlg((wxWindow*) this, wxID_ANY, m_plater);
m_temp_calib_dlg->ShowModal();
break;
}
case CalibKind::MaxVolumetric: {
if (!m_vol_test_dlg)
m_vol_test_dlg = new MaxVolumetricSpeed_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
m_vol_test_dlg->ShowModal();
break;
}
case CalibKind::PressureAdvance: {
if (!m_pa_calib_dlg)
m_pa_calib_dlg = new PA_Calibration_Dlg((wxWindow*) this, wxID_ANY, m_plater);
m_pa_calib_dlg->ShowModal();
break;
}
case CalibKind::FlowRatio: {
if (!m_plater)
break;
if (!m_flow_rate_calib_dlg)
m_flow_rate_calib_dlg = new FlowRateCalibrationDialog((wxWindow*) this, wxID_ANY, m_plater);
m_flow_rate_calib_dlg->ShowModal();
break;
}
case CalibKind::Retraction: {
if (!m_retraction_calib_dlg)
m_retraction_calib_dlg = new Retraction_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
m_retraction_calib_dlg->ShowModal();
break;
}
case CalibKind::Cornering: {
auto dlg = new Cornering_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
break;
}
case CalibKind::InputShapingFreq: {
auto dlg = new Input_Shaping_Freq_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
break;
}
case CalibKind::InputShapingDamp: {
auto dlg = new Input_Shaping_Damp_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
break;
}
case CalibKind::VFA: {
if (!m_vfa_test_dlg)
m_vfa_test_dlg = new VFA_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
m_vfa_test_dlg->ShowModal();
break;
}
}
}
//
// Called after the Preferences dialog is closed and the program settings are saved.
+36
View File
@@ -40,6 +40,9 @@
// Stable identifiers for MainFrame::m_tabpanel's built-in pages. These are
// names rather than positional indices so optional pages cannot shift them.
#define TAB_ID_HOME "home"
#ifdef SLIC3R_CAD
#define TAB_ID_DESIGN "design"
#endif
#define TAB_ID_PREPARE "prepare"
#define TAB_ID_PREVIEW "preview"
#define TAB_ID_MONITOR "monitor"
@@ -65,6 +68,9 @@ namespace GUI
class Tab;
class PrintHostQueueDialog;
class Plater;
#ifdef SLIC3R_CAD
class DesignPanel;
#endif
class MainFrame;
class WebViewPanel;
class ParamsDialog;
@@ -107,6 +113,20 @@ protected:
void on_dpi_changed(const wxRect& suggested_rect) override;
};
// Calibration wizard identity, shared by MainFrame::run_calibration and the Speed Dial command runners.
enum class CalibKind : int
{
Temperature,
MaxVolumetric,
PressureAdvance,
FlowRatio,
Retraction,
Cornering,
InputShapingFreq,
InputShapingDamp,
VFA
};
class MainFrame : public DPIFrame
{
#ifdef __APPLE__
@@ -354,6 +374,11 @@ public:
void technology_changed();
// Opens the calibration wizard for `kind`. Single source of truth for the wizard lifecycle:
// the Calibration menu handlers and the Speed Dial native commands both call this. Most wizards
// are cached members; cornering/input-shaping are transient. Call while the Prepare (3D) panel
// is shown (menu items are gated on is_view3D_shown; the speed dial ensures it first).
void run_calibration(CalibKind calib_kind);
//BBS
void load_url(wxString url);
@@ -384,6 +409,17 @@ public:
BBLTopbar* m_topbar{ nullptr };
PrintHostQueueDialog* printhost_queue_dlg() { return m_printhost_queue_dlg; }
Plater* m_plater { nullptr };
#ifdef SLIC3R_CAD
// The tab page is the placeholder; m_design_panel stays null until the tab is first
// selected, so everything the Design panel builds stays off the startup path.
wxPanel* m_design_page { nullptr };
DesignPanel* m_design_panel { nullptr };
// Builds the Design panel if it does not exist yet and returns it (null only before the
// placeholder page itself exists). Main thread only -- it creates wx controls. Both the
// tab activation and the MCP socket go through this: the socket is driven headlessly,
// with nobody to click the tab, and without this every verb would answer "not ready".
DesignPanel* ensure_design_panel();
#endif
//BBS: GUI refactor
MonitorPanel* m_monitor{ nullptr };
+1 -1
View File
@@ -389,7 +389,7 @@ void MediaPlayCtrl::Play()
if (is_webrtc) {
BOOST_LOG_TRIVIAL(info) << "MediaPlayCtrl::Play webrtc: last_state=" << m_last_state << " failed_retry=" << m_failed_retry
<< " shown=" << IsShownOnScreen();
auto channel = agent ? agent->create_camera_signaling_channel(m_machine) : nullptr;
auto channel = agent ? agent->create_camera_signaling_channel(m_machine, wxGetApp().get_printer_cloud_provider()) : nullptr;
if (!channel) {
Stop(_L("Sign in to OrcaCloud to view the camera."));
return;
+668
View File
@@ -0,0 +1,668 @@
#include "NativeCommands.hpp"
#include "calib_dlg.hpp"
#include "Camera.hpp"
#include "DailyTips.hpp"
#include "GCodeViewer.hpp"
#include "GLCanvas3D.hpp"
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "GUI_Factories.hpp"
#include "GUI_ObjectList.hpp"
#include "I18N.hpp"
#include "IMSlider.hpp"
#include "MainFrame.hpp"
#include "NetworkTestDialog.hpp"
#include "Plater.hpp"
#include "PluginsDialog.hpp"
#include "PlateSettingsDialog.hpp"
#include "DeviceCore/DevManager.h"
#include <libslic3r/Model.hpp>
#include <libslic3r/Utils.hpp>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <exception>
#include <memory>
#include <string>
#include <tuple>
#include <utility>
#include <wx/utils.h>
namespace Slic3r { namespace GUI {
namespace {
// Plate ops are an FFF feature: SLA has a single plate and no plate UI, gcode-only mode has no
// editable project - so gate every plate op on FFF + the normal editor.
bool is_fff_plater(Plater* plater) { return plater && plater->printer_technology() == ptFFF && !plater->only_gcode_mode(); }
AppActionRunResult plate_unavailable() { return {AppActionRunResult::Level::Info, _L("Plates are a filament (FFF) feature.")}; }
// Switch to the Prepare (3D) panel so object/calibration ops have a live canvas + selection, and
// the notebook page label matches. A no-op when the 3D panel is already shown.
void ensure_3d_view(Plater* plater)
{
if (plater && !plater->is_view3D_shown()) {
plater->select_view_3D("3D");
if (MainFrame* mf = wxGetApp().mainframe; mf)
mf->select_tab(TAB_ID_PREPARE);
}
}
// Object op guard + run: object ops read the Prepare canvas selection, so ensure that view first so
// a launch from another tab doesn't report a spuriously empty selection.
AppActionRunResult object_op(Plater* plater, bool (*ok)(Plater*), void (*op)(Plater*))
{
if (!plater)
return {AppActionRunResult::Level::Info, _L("Open the 3D view first.")};
ensure_3d_view(plater);
if (!ok(plater))
return {AppActionRunResult::Level::Info, _L("Select an object first.")};
op(plater);
return {AppActionRunResult::Level::Success};
}
// Jump the preview to a layer selected by a 0-100 percent of the layer range. The caller has already
// switched to Preview (which may request a slice); if a slicer result is present the slider is
// repositioned immediately, otherwise the jump is a no-op until the user re-slices.
void go_to_layer(Plater* plater, const std::string& param)
{
if (!plater)
return;
double pct = 50.0;
try {
pct = std::stod(param);
} catch (const std::exception&) {}
pct = std::clamp(pct, 0.0, 100.0);
GLCanvas3D* canvas = plater->get_current_canvas3D();
if (!canvas)
return;
GCodeViewer& viewer = canvas->get_gcode_viewer();
IMSlider* layers = viewer.get_layers_slider();
IMSlider* moves = viewer.get_moves_slider();
if (!layers || layers->GetMaxValue() <= 0)
return;
const double max = double(layers->GetMaxValue());
const int target = int(std::lround(pct / 100.0 * max));
layers->SetHigherValue(target);
if (layers->is_one_layer())
layers->SetLowerValue(target);
layers->set_as_dirty();
if (moves) {
moves->SetHigherValue(moves->GetMaxValue());
moves->set_as_dirty();
}
}
// Select a named camera view. Plater::select_view dispatches to the current panel.
AppActionRunResult view_command(Plater* plater, const std::string& dir)
{
if (plater)
plater->select_view(dir);
return {AppActionRunResult::Level::Success};
}
// Calibration wizards. Routes through MainFrame::run_calibration, the same entry point as the
// Calibration menu (which caches most of the wizard dialogs).
AppActionRunResult calib_command(CalibKind kind)
{
MainFrame* mf = wxGetApp().mainframe;
if (!mf)
return {AppActionRunResult::Level::Info, _L("Open the 3D view first.")};
ensure_3d_view(wxGetApp().plater());
mf->run_calibration(kind);
return {AppActionRunResult::Level::Success};
}
constexpr const char* kCommandPrefix = "orca_command";
// Thin AppAction wrapper for one catalog entry: identity and presentation come from the catalog,
// run() routes back to it. The id is keyed by the stable catalog key (not the display title), so a
// rename or a UI-language switch never re-keys the action.
struct CommandAction : AppAction
{
std::string command_key;
AppActionRunResult run(const std::string& param) const override { return NativeCommands::run(command_key, param); }
explicit CommandAction(const NativeCommand& c)
: AppAction(AppActionId{AppAction::compose_id(kCommandPrefix, c.key, kOrcaSourceKey)}, c.title, kOrcaSourceKey, kOrcaSourceName)
, command_key(c.key)
{
this->kind = AppActionKind::Command;
this->group = c.group;
this->input = c.input;
this->icon = c.icon;
}
};
std::vector<NativeCommand> build_command_catalog()
{
std::vector<NativeCommand> out;
auto add = [&](std::string key, std::string title, std::string group, std::function<AppActionRunResult(const std::string&)> runner,
std::string input = {}, std::string icon = {}) {
out.push_back({std::move(key), std::move(title), std::move(group), std::move(input), std::move(icon), std::move(runner)});
};
// Presentation-first overload: keeps the tile icon next to the title/group it belongs to.
auto add_with_icon = [&](std::string key, std::string title, std::string group, std::string icon,
std::function<AppActionRunResult(const std::string&)> runner, std::string input = {}) {
add(std::move(key), std::move(title), std::move(group), std::move(runner), std::move(input), std::move(icon));
};
// ---- Slice & Export ----
add_with_icon("slice_and_preview", _u8L("Slice and Preview"), _u8L("Slice & Export"), "media_play", [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (plater) {
plater->reslice();
plater->select_view_3D("Preview", false);
if (MainFrame* mf = wxGetApp().mainframe; mf)
mf->select_tab(TAB_ID_PREVIEW);
}
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon(
"go_to_layer", _u8L("Go to layer (percent)"), _u8L("Commands"), "height_range_layer",
[](const std::string& param) {
Plater* plater = wxGetApp().plater();
if (plater) {
plater->select_view_3D("Preview", false);
if (MainFrame* mf = wxGetApp().mainframe; mf)
mf->select_tab(TAB_ID_PREVIEW);
go_to_layer(plater, param);
}
return AppActionRunResult{AppActionRunResult::Level::Success};
},
"percent");
// "go_to_tab" is two-phase: the palette collects the tab after activating it, then hands the tab
// id back as `param` (same contract as go_to_layer's percent).
add(
"go_to_tab", _u8L("Go to tab..."), _u8L("Commands"),
[](const std::string& param) {
if (MainFrame* mf = wxGetApp().mainframe; mf && !param.empty())
mf->select_tab(from_u8(param));
return AppActionRunResult{AppActionRunResult::Level::Success};
},
"tab");
add_with_icon("load_project", _u8L("Load Project"), _u8L("Commands"), "menu_open", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->load_project();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("save_project", _u8L("Save Project"), _u8L("Commands"), "menu_save", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->save_project(false);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("save_project_as", _u8L("Save Project As"), _u8L("Commands"), "menu_save", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->save_project(true);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("open_preferences", _u8L("Preferences"), _u8L("Commands"), "cog", [](const std::string&) {
wxGetApp().open_preferences();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Mode ----
add_with_icon("mode_simple", _u8L("Mode: Simple"), _u8L("Mode"), "advanced", [](const std::string&) {
wxGetApp().set_mode(comSimple);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("mode_advanced", _u8L("Mode: Advanced"), _u8L("Mode"), "advanced", [](const std::string&) {
wxGetApp().set_mode(comAdvanced);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("mode_expert", _u8L("Mode: Expert"), _u8L("Mode"), "advanced", [](const std::string&) {
wxGetApp().set_mode(comExpert);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// Mirrors Preferences > Developer > Developer mode: flip the flag, persist, refresh the UI.
add_with_icon("toggle_developer_mode", _u8L("Toggle Developer Mode"), _u8L("Mode"), "advanced", [](const std::string&) {
GUI_App& app = wxGetApp();
const bool on = !app.app_config->get_bool("developer_mode");
app.app_config->set_bool("developer_mode", on);
app.app_config->save();
app.update_mode();
return AppActionRunResult{AppActionRunResult::Level::Success, on ? _L("Developer mode enabled.") : _L("Developer mode disabled.")};
});
// ---- Export pipeline ----
add_with_icon("export_gcode", _u8L("Export G-code"), _u8L("Slice & Export"), "custom-gcode_gcode", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_gcode(false);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("export_stl", _u8L("Export STL"), _u8L("Slice & Export"), "save", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_stl();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("export_3mf", _u8L("Export 3MF"), _u8L("Slice & Export"), "menu_save", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_core_3mf();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("export_sliced_file", _u8L("Export Sliced File"), _u8L("Slice & Export"), "save", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_gcode_3mf(false);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("export_all_sliced_file", _u8L("Export All Sliced Files"), _u8L("Slice & Export"), "save", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_gcode_3mf(true);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Calibration ----
// The tab-strip calib_sf glyph is drawn white for the dark tab bar and vanishes on the palette's
// light tile, so each wizard borrows the matching settings-group icon instead (gray + accent green).
add_with_icon("calib_temperature", _u8L("Temperature Calibration"), _u8L("Calibration"), "param_temperature",
[](const std::string&) { return calib_command(CalibKind::Temperature); });
add_with_icon("calib_max_volumetric", _u8L("Max Volumetric Speed Calibration"), _u8L("Calibration"), "param_volumetric_speed",
[](const std::string&) { return calib_command(CalibKind::MaxVolumetric); });
add_with_icon("calib_pressure_advance", _u8L("Pressure Advance Calibration"), _u8L("Calibration"), "param_flow_ratio_and_pressure_advance",
[](const std::string&) { return calib_command(CalibKind::PressureAdvance); });
add_with_icon("calib_flow_ratio", _u8L("Flow Ratio Calibration"), _u8L("Calibration"), "param_flow_ratio_and_pressure_advance",
[](const std::string&) { return calib_command(CalibKind::FlowRatio); });
add_with_icon("calib_retraction", _u8L("Retraction Calibration"), _u8L("Calibration"), "param_retraction",
[](const std::string&) { return calib_command(CalibKind::Retraction); });
add_with_icon("calib_cornering", _u8L("Cornering Calibration"), _u8L("Calibration"), "param_precision",
[](const std::string&) { return calib_command(CalibKind::Cornering); });
add_with_icon("calib_input_shaping_freq", _u8L("Input Shaping Frequency Calibration"), _u8L("Calibration"), "param_resonance_avoidance",
[](const std::string&) { return calib_command(CalibKind::InputShapingFreq); });
add_with_icon("calib_input_shaping_damp", _u8L("Input Shaping Damping Calibration"), _u8L("Calibration"), "param_resonance_avoidance",
[](const std::string&) { return calib_command(CalibKind::InputShapingDamp); });
add_with_icon("calib_vfa", _u8L("VFA Calibration"), _u8L("Calibration"), "param_speed", [](const std::string&) { return calib_command(CalibKind::VFA); });
// ---- View ----
// Titles are built with _u8L here (not via a variable) so xgettext can extract them.
for (auto [key, dir, title] :
std::initializer_list<std::tuple<const char*, const char*, std::string>>{{"view_top", "top", _u8L("View: Top")},
{"view_bottom", "bottom", _u8L("View: Bottom")},
{"view_front", "front", _u8L("View: Front")},
{"view_rear", "rear", _u8L("View: Rear")},
{"view_left", "left", _u8L("View: Left")},
{"view_right", "right", _u8L("View: Right")},
{"view_iso", "iso", _u8L("View: Isometric")}}) {
std::string k = key, d = dir;
add(k, title, _u8L("View"),
[d](const std::string&) { return view_command(wxGetApp().plater(), d); });
}
add("view_default", _u8L("View: Default"), _u8L("View"), [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (plater) {
plater->select_view("plate");
if (GLCanvas3D* canvas = plater->get_current_canvas3D())
canvas->zoom_to_bed();
}
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("view_fit_bed", _u8L("Fit Bed to View"), _u8L("View"), [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
if (GLCanvas3D* canvas = plater->get_current_canvas3D())
canvas->zoom_to_bed();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("view_toggle_perspective", _u8L("Toggle Perspective"), _u8L("View"), [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->get_camera().select_next_type();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("reset_window_layout", _u8L("Reset Window Layout"), _u8L("View"), "toolbar_reset", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->reset_window_layout();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Object ----
add_with_icon("obj_delete", _u8L("Delete Selected"), _u8L("Object"), "delete", [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->remove_selected(); });
});
add_with_icon("obj_delete_all", _u8L("Delete All Objects"), _u8L("Object"), "delete", [](const std::string&) {
return object_op(
wxGetApp().plater(), [](Plater* p) { return p->can_delete_all(); }, [](Plater* p) { p->delete_all_objects_from_model(); });
});
add_with_icon("obj_mirror_x", _u8L("Mirror X"), _u8L("Object"), "menu_mirror_x", [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::X); });
});
add_with_icon("obj_mirror_y", _u8L("Mirror Y"), _u8L("Object"), "menu_mirror_y", [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Y); });
});
add_with_icon("obj_mirror_z", _u8L("Mirror Z"), _u8L("Object"), "menu_mirror_z", [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Z); });
});
add_with_icon("obj_split_objects", _u8L("Split to Objects"), _u8L("Object"), "menu_split_objects", [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_split_to_objects(); }, [](Plater* p) { p->split_object(true); });
});
add_with_icon("obj_split_parts", _u8L("Split to Parts"), _u8L("Object"), "menu_split_parts", [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_split_to_volumes(); }, [](Plater* p) { p->split_volume(); });
});
add("obj_center", _u8L("Center Selected on Plate"), _u8L("Object"), [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->center_selection(); });
});
add_with_icon("obj_drop", _u8L("Drop to Bed"), _u8L("Object"), "toolbar_flatten", [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->drop_selection(); });
});
add("obj_fit_volume", _u8L("Scale to Fit Print Volume"), _u8L("Object"), [](const std::string&) {
return object_op(
wxGetApp().plater(), [](Plater* p) { return p->can_scale_to_print_volume(); },
[](Plater* p) { p->scale_selection_to_fit_print_volume(); });
});
add_with_icon("obj_instances_up", _u8L("Increase Instances"), _u8L("Object"), "instance_add", [](const std::string&) {
return object_op(
wxGetApp().plater(), [](Plater* p) { return p->can_increase_instances(); }, [](Plater* p) { p->increase_instances(); });
});
add_with_icon("obj_instances_down", _u8L("Decrease Instances"), _u8L("Object"), "instance_remove", [](const std::string&) {
return object_op(
wxGetApp().plater(), [](Plater* p) { return p->can_decrease_instances(); }, [](Plater* p) { p->decrease_instances(); });
});
add_with_icon("obj_arrange", _u8L("Auto-Arrange"), _u8L("Object"), "toolbar_arrange", [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->arrange(); });
});
add_with_icon("obj_orient", _u8L("Auto-Orient"), _u8L("Object"), "toolbar_orient", [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->orient(); });
});
// ---- Add Primitive ---- (the Add > Add Primitive submenu; creates a new object)
auto add_primitive = [&](std::string key, std::string title, std::string icon, const char* type_name) {
add_with_icon(std::move(key), std::move(title), _u8L("Add Primitive"), std::move(icon), [type_name](const std::string&) {
Plater* plater = wxGetApp().plater();
if (plater) {
ensure_3d_view(plater);
if (ObjectList* list = wxGetApp().obj_list())
list->load_generic_subobject(type_name, ModelVolumeType::INVALID);
}
return AppActionRunResult{AppActionRunResult::Level::Success};
});
};
add_primitive("add_primitive_cube", _u8L("Cube"), "menu_obj_cube", "Cube");
add_primitive("add_primitive_cylinder", _u8L("Cylinder"), "menu_obj_cylinder", "Cylinder");
add_primitive("add_primitive_sphere", _u8L("Sphere"), "menu_obj_sphere", "Sphere");
add_primitive("add_primitive_cone", _u8L("Cone"), "menu_obj_cone", "Cone");
add_primitive("add_primitive_disc", _u8L("Disc"), "menu_obj_disc", "Disc");
add_primitive("add_primitive_torus", _u8L("Torus"), "menu_obj_torus", "Torus");
add_with_icon("add_primitive_text", _u8L("Text"), _u8L("Add Primitive"), "menu_obj_text", [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (plater) {
ensure_3d_view(plater);
if (GLCanvas3D* canvas = plater->canvas3D())
canvas->clear_popup_menu_position();
MenuFactory::add_text_volume(ModelVolumeType::INVALID);
}
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("add_primitive_svg", _u8L("SVG"), _u8L("Add Primitive"), "menu_obj_svg", [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (plater) {
ensure_3d_view(plater);
if (GLCanvas3D* canvas = plater->canvas3D())
canvas->clear_popup_menu_position();
MenuFactory::add_svg_volume(ModelVolumeType::INVALID);
}
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Add Handy models ---- (the Add > Add Handy models submenu)
const std::vector<MenuFactory::HandyModel>& handy = MenuFactory::handy_models();
for (std::size_t i = 0; i < handy.size(); ++i) {
add("add_handy_" + std::string(handy[i].key), Slic3r::GUI::I18N::translate_utf8(handy[i].label), _u8L("Add Handy models"),
[i](const std::string&) {
if (Plater* plater = wxGetApp().plater())
ensure_3d_view(plater);
MenuFactory::load_handy_model(i);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
}
// ---- Plate ----
add_with_icon("plate_add", _u8L("Add Plate"), _u8L("Plate"), "toolbar_add_plate", [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (!is_fff_plater(plater))
return plate_unavailable();
if (!plater->can_add_plate())
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Cannot add another plate (maximum reached).")};
plater->add_plate();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("plate_duplicate", _u8L("Duplicate Plate"), _u8L("Plate"), "menu_copy", [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (!is_fff_plater(plater))
return plate_unavailable();
if (!plater->can_add_plate())
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Cannot duplicate a plate (maximum reached).")};
plater->duplicate_plate();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("plate_delete", _u8L("Delete Plate"), _u8L("Plate"), "delete", [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (!is_fff_plater(plater))
return plate_unavailable();
if (!plater->can_delete_plate())
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Cannot delete the only plate.")};
plater->delete_plate();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("plate_rename", _u8L("Rename Plate"), _u8L("Plate"), "plate_name_edit", [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (!is_fff_plater(plater))
return plate_unavailable();
PartPlate* curr = plater->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{AppActionRunResult::Level::Success};
});
add_with_icon("plate_toggle_lock", _u8L("Toggle Plate Lock"), _u8L("Plate"), "lock_normal", [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (!is_fff_plater(plater))
return plate_unavailable();
PartPlateList& plates = plater->get_partplate_list();
const int index = plates.get_curr_plate_index();
plater->take_snapshot("lock partplate");
plates.lock_plate(index, !plates.is_locked(index));
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("plate_goto", _u8L("Go to Plate"), _u8L("Plate"), "go_next_plate", [](const std::string& param) {
Plater* plater = wxGetApp().plater();
if (!is_fff_plater(plater))
return plate_unavailable();
PartPlateList& plates = plater->get_partplate_list();
const int count = plates.get_plate_count();
if (count <= 0)
return AppActionRunResult{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);
plater->select_plate(index, false);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Printer / device connection ----
add_with_icon("sync_ams", _u8L("Synchronize Filament List from AMS"), _u8L("Printer"), "ams_fila_sync", [](const std::string&) {
Plater* plater = wxGetApp().plater();
DeviceManager* dev = wxGetApp().getDeviceManager();
if (dev && dev->get_selected_machine() && plater) {
plater->sidebar().sync_ams_list();
return AppActionRunResult{AppActionRunResult::Level::Success};
}
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Connect a printer to synchronize the AMS filament list.")};
});
// ---- Presets / cloud ----
add_with_icon("preset_bundle", _u8L("Open Preset Bundle"), _u8L("Presets"), "menu_edit_preset", [](const std::string&) {
wxGetApp().open_presetbundledialog();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("sync_presets", _u8L("Sync Presets"), _u8L("Presets"), "printer_sync_ok", [](const std::string&) {
if (!wxGetApp().is_user_login())
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Sign in to sync presets.")};
wxGetApp().restart_sync_user_preset();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Import ----
add_with_icon("import_file", _u8L("Import 3MF/STL/STEP/SVG/OBJ/AMF"), _u8L("Import"), "menu_open", [](const std::string&) {
if (Plater* plater = wxGetApp().plater()) {
#ifdef __APPLE__
plater->add_model();
#else
plater->add_file();
#endif
}
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("import_zip_archive", _u8L("Import ZIP Archive"), _u8L("Import"), "menu_open", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->import_zip_archive();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("import_configs", _u8L("Import Configs"), _u8L("Import"), "menu_open", [](const std::string&) {
if (MainFrame* mf = wxGetApp().mainframe)
mf->load_config_file();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Export extras ----
add_with_icon("export_stl_multi", _u8L("Export All Objects as STLs"), _u8L("Export"), "save", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_stl(false, false, true);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("export_drc_single", _u8L("Export All Objects as DRC (one file)"), _u8L("Export"), "save", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_stl(false, false, false, FT_DRC);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("export_drc_multi", _u8L("Export All Objects as DRCs"), _u8L("Export"), "save", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_stl(false, false, true, FT_DRC);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("export_toolpaths_obj", _u8L("Export Toolpaths as OBJ"), _u8L("Export"), "custom-gcode_gcode", [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_toolpaths_to_obj();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("export_config", _u8L("Export Preset Bundle"), _u8L("Export"), "save", [](const std::string&) {
if (MainFrame* mf = wxGetApp().mainframe)
mf->export_config();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Help ---- (mirrors the top-bar Help menu, plus the wiki/YouTube links)
add("help_keyboard_shortcuts", _u8L("Keyboard Shortcuts"), _u8L("Help"), [](const std::string&) {
wxGetApp().keyboard_shortcuts();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("help_setup_wizard", _u8L("Setup Wizard"), _u8L("Help"), [](const std::string&) {
wxGetApp().ShowUserGuide();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("help_open_config_folder", _u8L("Show Configuration Folder"), _u8L("Help"), "open_project", [](const std::string&) {
Slic3r::GUI::desktop_open_datadir_folder();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("help_troubleshoot", _u8L("Troubleshoot Center"), _u8L("Help"), [](const std::string&) {
wxGetApp().troubleshoot();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("help_network_test", _u8L("Open Network Test"), _u8L("Help"), [](const std::string&) {
NetworkTestDialog dlg(wxGetApp().mainframe);
dlg.ShowModal();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("help_tip_of_the_day", _u8L("Show Tip of the Day"), _u8L("Help"), "info", [](const std::string&) {
if (Plater* plater = wxGetApp().plater()) {
plater->get_dailytips()->open();
if (GLCanvas3D* canvas = plater->get_current_canvas3D())
canvas->set_as_dirty();
}
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("help_check_updates", _u8L("Check for Updates"), _u8L("Help"), "refresh", [](const std::string&) {
wxGetApp().check_new_version_sf(true, 1);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("help_about", _u8L("About OrcaSlicer"), _u8L("Help"), "OrcaSlicer_gradient_circle", [](const std::string&) {
Slic3r::GUI::about();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add_with_icon("open_wiki", _u8L("Open Wiki"), _u8L("Help"), "link_wiki_img", [](const std::string&) {
wxLaunchDefaultBrowser("https://www.orcaslicer.com/wiki/", wxBROWSER_NEW_WINDOW);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("open_youtube", _u8L("Open YouTube Channel"), _u8L("Help"), [](const std::string&) {
wxLaunchDefaultBrowser("https://www.youtube.com/@OfficialOrcaSlicer/videos", wxBROWSER_NEW_WINDOW);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Plugins ----
add("open_plugins", _u8L("Open Plugins"), _u8L("Plugins"), [](const std::string&) {
wxGetApp().open_plugins_dialog();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("refresh_plugins", _u8L("Refresh Plugins"), _u8L("Plugins"), [](const std::string&) {
wxGetApp().refresh_plugins();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("install_plugin", _u8L("Install Plugin"), _u8L("Plugins"), [](const std::string&) {
open_plugin_hub();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("install_local_plugin", _u8L("Install Local Plugin"), _u8L("Plugins"), [](const std::string&) {
wxGetApp().install_local_plugin();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
return out;
}
std::vector<NativeCommand>& catalog_storage()
{
static std::vector<NativeCommand> commands = build_command_catalog();
return commands;
}
} // namespace
const std::vector<NativeCommand>& NativeCommands::catalog()
{
return catalog_storage();
}
void NativeCommands::rebuild_catalog()
{
// build_command_catalog() re-runs _u8L under the current locale, so replacing the storage
// refreshes every translated title/group after a language switch.
catalog_storage() = build_command_catalog();
}
std::unique_ptr<AppAction> NativeCommands::make_action(const NativeCommand& command)
{
return std::make_unique<CommandAction>(command);
}
AppActionRunResult NativeCommands::run(const std::string& key, const std::string& param)
{
GUI_App& app = wxGetApp();
if (app.is_closing())
return {};
for (const NativeCommand& c : catalog())
if (c.key == key)
return c.runner(param);
return {AppActionRunResult::Level::Info, _L("Unknown command.")};
}
}} // namespace Slic3r::GUI
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include "ActionRegistry.hpp" // for AppAction / AppActionRunResult
namespace Slic3r { namespace GUI {
// A built-in speed-dial command: identity + how to run it. make_action() wraps a value as a thin
// AppAction for the registry, so this catalog is the single source of truth for the behaviour
// (runner => an owner method), the presentation (title/group/input), and the tile pictogram
// (icon = an SVG base name under resources/images, "" for no icon).
struct NativeCommand
{
std::string key;
std::string title;
std::string group;
std::string input; // "percent"/"tab" or "" for immediate run
std::string icon; // SVG base name, or "" to render a blank tile
std::function<AppActionRunResult(const std::string& param)> runner;
};
namespace NativeCommands {
// The full built-in command catalog. Built on first use and reused; call rebuild_catalog() after a
// live UI language switch so the translated titles/groups match the new locale. UI thread only.
const std::vector<NativeCommand>& catalog();
// Rebuilds the catalog in the current locale. UI thread only.
void rebuild_catalog();
// Dispatches `key` to its runner (unknown keys return a quiet Info). UI thread only.
AppActionRunResult run(const std::string& key, const std::string& param = {});
// Materialises one catalog entry as a runnable AppAction. Keeps the catalog's identity,
// presentation and behaviour as the single source of truth; ActionRegistry only stores and
// dispatches the result. UI thread only.
std::unique_ptr<AppAction> make_action(const NativeCommand& command);
} // namespace NativeCommands
}} // namespace Slic3r::GUI
+21 -2
View File
@@ -10,6 +10,7 @@
#include "Widgets/Label.hpp"
#include <wx/button.h>
#include <wx/dcclient.h>
#include <wx/sizer.h>
wxDEFINE_EVENT(wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED, wxCommandEvent);
@@ -158,12 +159,22 @@ void ButtonsListCtrl::SetSelection(int sel)
bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /* = false*/, const std::string &bmp_name /* = ""*/, const wxBitmap &bmp /* = wxNullBitmap */)
{
Button * btn = new Button(this, text.empty() ? text : " " + text, bmp_name, wxNO_BORDER);
Button * btn = new Button(this, text, bmp_name, wxNO_BORDER);
btn->SetCornerRadius(0);
if (bmp_name.empty() && bmp.IsOk())
btn->SetIcon(bmp);
// The label no longer carries a leading space, so widen the icon<->text gap to keep the
// original spacing between a tab's icon and its caption.
{
wxClientDC dc(btn);
dc.SetFont(btn->GetFont());
int space_w = 0;
dc.GetTextExtent(" ", &space_w, nullptr);
btn->SetIconSpacing(5 + space_w);
}
int em = em_unit(this);
//BBS set size for button
btn->SetMinSize({(text.empty() ? 40 : 136) * em / 10, 36 * em / 10});
@@ -190,6 +201,7 @@ bool ButtonsListCtrl::InsertPage(size_t n, const wxString &text, bool bSelect /*
Slic3r::GUI::wxGetApp().UpdateDarkUI(btn);
m_pageButtons.insert(m_pageButtons.begin() + n, btn);
m_pageLabels.insert(m_pageLabels.begin() + n, text); // ORCA
m_pageIcons.insert(m_pageIcons.begin() + n, bmp_name);
m_buttons_sizer->Insert(n, new wxSizerItem(btn));
m_buttons_sizer->SetCols(m_buttons_sizer->GetCols() + 1);
m_sizer->Layout();
@@ -209,6 +221,7 @@ void ButtonsListCtrl::RemovePage(size_t n)
Button* btn = m_pageButtons[n];
m_pageButtons.erase(m_pageButtons.begin() + n);
m_pageLabels.erase(m_pageLabels.begin() + n); // ORCA
m_pageIcons.erase(m_pageIcons.begin() + n);
m_buttons_sizer->Remove(n);
#if __WXOSX__
RemoveChild(btn);
@@ -245,7 +258,7 @@ void ButtonsListCtrl::SetCompact(size_t n, bool compact)
int em = em_unit(this);
Button* btn = m_pageButtons[n];
btn->SetMinSize({(compact ? 40 : 136) * em / 10, 36 * em / 10});
btn->SetLabel(compact ? "" : (" " + m_pageLabels[n]));
btn->SetLabel(compact ? "" : m_pageLabels[n]);
}
wxString ButtonsListCtrl::GetPageText(size_t n) const
@@ -254,6 +267,12 @@ wxString ButtonsListCtrl::GetPageText(size_t n) const
return btn->GetLabel();
}
// ORCA
wxString ButtonsListCtrl::GetPageLabel(size_t n) const
{
return n < m_pageLabels.size() ? m_pageLabels[n] : wxString();
}
// ORCA
void ButtonsListCtrl::SetOverflowButton(wxWindow* button)
{
+23
View File
@@ -33,6 +33,14 @@ public:
void SetPageText(size_t n, const wxString& strText);
void SetCompact(size_t n, bool compact); // ORCA
wxString GetPageText(size_t n) const;
// ORCA: the full page label, unaffected by SetCompact() blanking the button text.
wxString GetPageLabel(size_t n) const;
// Resource name the page was inserted with (empty for plugin pages, which pass a wxBitmap).
const std::string& GetPageIcon(size_t n) const
{
static const std::string empty;
return n < m_pageIcons.size() ? m_pageIcons[n] : empty;
}
wxFlexGridSizer* GetBtnsSizer(){return m_buttons_sizer;}; // ORCA
// ORCA: a companion widget shown right after the tab buttons (before any side_tools), e.g.
// an overflow indicator. Pass nullptr to remove it; ownership stays with the caller.
@@ -47,6 +55,7 @@ private:
int m_btn_margin;
int m_line_margin;
std::vector<wxString> m_pageLabels; // ORCA
std::vector<std::string> m_pageIcons; // ORCA: resource icon name per page, plugin pages empty
wxWindow* m_overflow_button{nullptr}; // ORCA
};
@@ -241,6 +250,20 @@ public:
return GetBtnsListCtrl()->GetPageText(n);
}
// ORCA: the real page label. GetPageText() returns the button label, which SetCompact() blanks.
wxString GetPageLabel(size_t n) const
{
wxCHECK_MSG(n < GetPageCount(), wxString(), wxS("Invalid page"));
return GetBtnsListCtrl()->GetPageLabel(n);
}
// Resource icon name the page was inserted with; empty for pages added with a wxBitmap.
std::string GetPageIcon(size_t n) const
{
wxCHECK_MSG(n < GetPageCount(), std::string(), wxS("Invalid page"));
return GetBtnsListCtrl()->GetPageIcon(n);
}
virtual bool SetPageImage(size_t WXUNUSED(n), int WXUNUSED(imageId)) override
{
return false;
+21 -1
View File
@@ -1,6 +1,7 @@
#include "OptionsGroup.hpp"
#include "ConfigExceptions.hpp"
#include "Plater.hpp"
#include "SettingsIndex.hpp"
#include "GUI_App.hpp"
#include "MainFrame.hpp"
#include "OG_CustomCtrl.hpp"
@@ -244,6 +245,25 @@ void OptionsGroup::append_line(const Line& line)
{
m_lines.emplace_back(line);
// Feed the searcher the row's wiki path (Line::label_path, for the Speed Dial's "open wiki"
// affordance) and the label the row actually draws, so a setting action is named like the page.
if (m_use_custom_ctrl) {
Search::SettingsIndex& index = wxGetApp().sidebar().settings_index();
const Preset::Type type = static_cast<Preset::Type>(config_type());
const bool multi = line.get_options().size() > 1;
for (const auto& opt : line.get_options()) {
if (!line.label_path.empty())
index.set_path(opt.opt_id, type, line.label_path);
// Mirror the sub-label OG_CustomCtrl draws for a multi-option row, so the palette
// names each field like the page does.
const std::string& leaf_src = opt.opt.label;
const wxString leaf = (leaf_src == L_CONTEXT("Top", "Layers") || leaf_src == L_CONTEXT("Bottom", "Layers")) ?
_L_CONTEXT(leaf_src, "Layers") :
_(leaf_src);
index.set_line_label(opt.opt_id, type, Search::compose_display_label(line.label, leaf, multi));
}
}
if (line.full_width && (line.widget != nullptr || !line.get_extra_widgets().empty()))
return;
@@ -650,7 +670,7 @@ Option ConfigOptionsGroup::get_option(const std::string& opt_key, int opt_index
m_opt_map.emplace(opt_id, pair);
if (m_use_custom_ctrl) // fill group and category values just for options from Settings Tab
wxGetApp().sidebar().get_searcher().add_key(opt_id, static_cast<Preset::Type>(this->config_type()), title, this->config_category());
wxGetApp().sidebar().settings_index().add_key(opt_id, static_cast<Preset::Type>(this->config_type()), title, this->config_category(), this->icon);
return Option(*m_config->def()->get(opt_key), opt_id);
}
+5 -1
View File
@@ -250,6 +250,10 @@ protected:
virtual void back_to_initial_value(const std::string& opt_key) {}
virtual void back_to_sys_value(const std::string& opt_key) {}
// Preset::Type of a settings group; -1 for groups not tied to a preset. Used by append_line to
// register each option's wiki path with the searcher. Overridden by ConfigOptionsGroup.
virtual int config_type() const { return -1; }
public:
static wxString get_url(const std::string& path_end);
static bool launch_browser(const std::string& path_end);
@@ -273,7 +277,7 @@ public:
OptionsGroup(parent, wxEmptyString, wxEmptyString, true, nullptr) {}
const wxString& config_category() const throw() { return m_config_category; }
int config_type() const throw() { return m_config_type; }
int config_type() const throw() override { return m_config_type; }
const t_opt_map& opt_map() const throw() { return m_opt_map; }
void set_config_category_and_type(const wxString &category, int type) { m_config_category = category; m_config_type = type; }
+3 -1
View File
@@ -324,7 +324,9 @@ ParamsPanel::ParamsPanel( wxWindow* parent, wxWindowID id, const wxPoint& pos, c
wxID_ANY,
wxDefaultPosition,
wxDefaultSize,
wxVSCROLL) // hide hori-bar will cause hidden field mis-position
wxVSCROLL // hide hori-bar will cause hidden field mis-position
| wxTAB_TRAVERSAL // Allows for traversal via tab key
)
{
// ShowScrollBar(GetHandle(), SB_BOTH, FALSE);
Bind(wxEVT_SCROLL_CHANGED, [this](auto &e) {
+48 -32
View File
@@ -1053,7 +1053,13 @@ void PartPlate::render_grid(bool bottom) {
void PartPlate::render_height_limit(PartPlate::HeightLimitMode mode)
{
if (m_print && m_print->config().print_sequence == PrintSequence::ByObject && mode != HEIGHT_LIMIT_NONE)
// Orca: a prime tower compacted by "No sparse layers" drags the nozzle back down to the plate on
// every toolchange, so the rod and the lid limit how tall a neighbouring object may be exactly as
// they do in sequential printing. The reference lines are just as useful there.
const bool relevant_for_print_mode = m_print && (m_print->config().print_sequence == PrintSequence::ByObject ||
(m_print->config().print_sequence == PrintSequence::ByLayer &&
wipe_tower_sparse_layers_skipped(m_print->config()) && m_print->has_wipe_tower()));
if (relevant_for_print_mode && mode != HEIGHT_LIMIT_NONE)
{
// draw lower limit
// ORCA: OpenGL Core Profile
@@ -1109,19 +1115,9 @@ void PartPlate::render_plate_name_texture()
glsafe(::glBindTexture(GL_TEXTURE_2D, 0));
}
void PartPlate::show_tooltip(const std::string tooltip)
void PartPlate::set_hover_tooltip(const std::string& tooltip)
{
const auto scale = m_plater->get_current_canvas3D()->get_scale();
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {6 * scale, 3 * scale});
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3 * scale);
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGuiWrapper::COL_WINDOW_BACKGROUND);
ImGui::PushStyleColor(ImGuiCol_Border, {0, 0, 0, 0});
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.00f, 1.00f, 1.00f, 1.00f));
ImGui::BeginTooltip();
ImGui::TextUnformatted(tooltip.c_str());
ImGui::EndTooltip();
ImGui::PopStyleColor(3);
ImGui::PopStyleVar(2);
m_partplate_list->m_hover_tooltip = tooltip;
}
void PartPlate::render_icons(bool bottom, bool only_name, int hover_id)
@@ -1146,21 +1142,21 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id)
if (!only_name) {
if (hover_id == 1) {
render_icon_texture(m_del_icon.model, m_partplate_list->m_del_hovered_texture);
show_tooltip(_u8L("Remove current plate (if not last one)"));
set_hover_tooltip(_u8L("Remove current plate (if not last one)"));
}
else
render_icon_texture(m_del_icon.model, m_partplate_list->m_del_texture);
if (hover_id == 2) {
render_icon_texture(m_orient_icon.model, m_partplate_list->m_orient_hovered_texture);
show_tooltip(_u8L("Auto orient objects on current plate"));
set_hover_tooltip(_u8L("Auto orient objects on current plate"));
}
else
render_icon_texture(m_orient_icon.model, m_partplate_list->m_orient_texture);
if (hover_id == 3) {
render_icon_texture(m_arrange_icon.model, m_partplate_list->m_arrange_hovered_texture);
show_tooltip(_u8L("Arrange objects on current plate"));
set_hover_tooltip(_u8L("Arrange objects on current plate"));
}
else
render_icon_texture(m_arrange_icon.model, m_partplate_list->m_arrange_texture);
@@ -1169,12 +1165,12 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id)
if (this->is_locked()) {
render_icon_texture(m_lock_icon.model,
m_partplate_list->m_locked_hovered_texture);
show_tooltip(_u8L("Unlock current plate"));
set_hover_tooltip(_u8L("Unlock current plate"));
}
else {
render_icon_texture(m_lock_icon.model,
m_partplate_list->m_lockopen_hovered_texture);
show_tooltip(_u8L("Lock current plate"));
set_hover_tooltip(_u8L("Lock current plate"));
}
} else {
if (this->is_locked())
@@ -1188,21 +1184,21 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id)
if (dual_bbl) {
if (hover_id == PLATE_FILAMENT_MAP_ID){
render_icon_texture(m_plate_filament_map_icon.model, m_partplate_list->m_plate_set_filament_map_hovered_texture);
show_tooltip(_u8L("Filament grouping"));
set_hover_tooltip(_u8L("Filament grouping"));
} else
render_icon_texture(m_plate_filament_map_icon.model, m_partplate_list->m_plate_set_filament_map_texture);
}
if (hover_id == 6) {
render_icon_texture(m_plate_name_edit_icon.model, m_partplate_list->m_plate_name_edit_hovered_texture);
show_tooltip(_u8L("Edit current plate name"));
set_hover_tooltip(_u8L("Edit current plate name"));
}
else
render_icon_texture(m_plate_name_edit_icon.model, m_partplate_list->m_plate_name_edit_texture);
if (hover_id == 7) {
render_icon_texture(m_move_front_icon.model, m_partplate_list->m_move_front_hovered_texture);
show_tooltip(_u8L("Move plate to the front"));
set_hover_tooltip(_u8L("Move plate to the front"));
} else
render_icon_texture(m_move_front_icon.model, m_partplate_list->m_move_front_texture);
@@ -1215,7 +1211,7 @@ void PartPlate::render_icons(bool bottom, bool only_name, int hover_id)
else
render_icon_texture(m_plate_settings_icon.model, m_partplate_list->m_plate_settings_changed_hovered_texture);
show_tooltip(_u8L("Customize current plate"));
set_hover_tooltip(_u8L("Customize current plate"));
} else {
if (!has_plate_settings)
render_icon_texture(m_plate_settings_icon.model, m_partplate_list->m_plate_settings_texture);
@@ -3501,7 +3497,7 @@ bool PartPlate::intersects(const BoundingBoxf3& bb) const
return print_volume.intersects(bb);
}
void PartPlate::render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_body, bool force_background_color, HeightLimitMode mode, int hover_id, bool render_cali, bool show_grid)
void PartPlate::render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_body, bool force_background_color, HeightLimitMode mode, int hover_id, bool render_cali, bool show_grid, bool hide_chrome)
{
glsafe(::glEnable(GL_DEPTH_TEST));
@@ -3552,16 +3548,18 @@ void PartPlate::render(const Transform3d& view_matrix, const Transform3d& projec
if (wxGetApp().show_plate_gridlines() && show_grid)
render_grid(bottom);
if (!bottom && m_selected && !force_background_color) {
if (!hide_chrome && !bottom && m_selected && !force_background_color) {
if (m_partplate_list)
render_logo(bottom, m_partplate_list->render_cali_logo && render_cali);
else
render_logo(bottom);
}
render_icons(bottom, only_body, hover_id);
if (!force_background_color) {
render_only_numbers(bottom);
if (!hide_chrome) {
render_icons(bottom, only_body, hover_id);
if (!force_background_color) {
render_only_numbers(bottom);
}
}
glsafe(::glDisable(GL_DEPTH_TEST));
@@ -5956,7 +5954,7 @@ void PartPlateList::postprocess_arrange_polygon(arrangement::ArrangePolygon& arr
/*rendering related functions*/
//render
void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid)
void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current, bool only_body, int hover_id, bool render_cali, bool show_grid, bool hide_chrome)
{
const std::lock_guard<std::mutex> local_lock(m_plates_mutex);
std::vector<PartPlate*>::iterator it = m_plate_list.begin();
@@ -5981,19 +5979,37 @@ void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& pr
if (current_index == m_current_plate) {
PartPlate::HeightLimitMode height_mode = (only_current)?PartPlate::HEIGHT_LIMIT_NONE:m_height_limit_mode;
if (plate_hover_index == current_index)
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, height_mode, plate_hover_action, render_cali, show_grid);
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, height_mode, plate_hover_action, render_cali, show_grid, hide_chrome);
else
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, height_mode, -1, render_cali, show_grid);
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, height_mode, -1, render_cali, show_grid, hide_chrome);
}
else {
if (plate_hover_index == current_index)
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, PartPlate::HEIGHT_LIMIT_NONE, plate_hover_action, render_cali, show_grid);
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, PartPlate::HEIGHT_LIMIT_NONE, plate_hover_action, render_cali, show_grid, hide_chrome);
else
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, PartPlate::HEIGHT_LIMIT_NONE, -1, render_cali, show_grid);
(*it)->render(view_matrix, projection_matrix, bottom, only_body, false, PartPlate::HEIGHT_LIMIT_NONE, -1, render_cali, show_grid, hide_chrome);
}
}
}
void PartPlateList::render_hover_tooltip() const
{
if (m_hover_tooltip.empty())
return;
const auto scale = m_plater->get_current_canvas3D()->get_scale();
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {6 * scale, 3 * scale});
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3 * scale);
ImGui::PushStyleColor(ImGuiCol_PopupBg, ImGuiWrapper::COL_WINDOW_BACKGROUND);
ImGui::PushStyleColor(ImGuiCol_Border, {0, 0, 0, 0});
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.00f, 1.00f, 1.00f, 1.00f));
ImGui::BeginTooltip();
ImGui::TextUnformatted(m_hover_tooltip.c_str());
ImGui::EndTooltip();
ImGui::PopStyleColor(3);
ImGui::PopStyleVar(2);
}
/*int PartPlateList::select_plate_by_hover_id(int hover_id)
{
int index = hover_id / PartPlate::GRABBER_COUNT;
+7 -3
View File
@@ -197,7 +197,7 @@ private:
// void render_left_arrow(const ColorRGBA render_color, bool use_lighting) const;
// void render_right_arrow(const ColorRGBA render_color, bool use_lighting) const;
void render_icon_texture(GLModel &buffer, GLTexture &texture);
void show_tooltip(const std::string tooltip);
void set_hover_tooltip(const std::string& tooltip);
void render_icons(bool bottom, bool only_name = false, int hover_id = -1);
void render_only_numbers(bool bottom);
void render_plate_name_texture();
@@ -428,7 +428,7 @@ public:
bool contains(const BoundingBoxf3& bb) const;
bool intersects(const BoundingBoxf3& bb) const;
void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_body = false, bool force_background_color = false, HeightLimitMode mode = HEIGHT_LIMIT_NONE, int hover_id = -1, bool render_cali = false, bool show_grid = true);
void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_body = false, bool force_background_color = false, HeightLimitMode mode = HEIGHT_LIMIT_NONE, int hover_id = -1, bool render_cali = false, bool show_grid = true, bool hide_chrome = false);
void set_selected();
void set_unselected();
@@ -640,6 +640,8 @@ class PartPlateList : public ObjectBase
bool render_bedtype_logo = true;
bool render_plate_settings = true;
bool render_cali_logo = true;
// Tooltip of the plate icon the last scene pass drew hovered; the canvas overlay shows it.
std::string m_hover_tooltip;
bool m_is_dark = false;
@@ -857,9 +859,11 @@ public:
/*rendering related functions*/
void on_change_color_mode(bool is_dark) { m_is_dark = is_dark; }
void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current = false, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true);
void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_current = false, bool only_body = false, int hover_id = -1, bool render_cali = false, bool show_grid = true, bool hide_chrome = false);
void set_render_option(bool bedtype_texture, bool plate_settings);
void set_render_cali(bool value = true) { render_cali_logo = value; }
void render_hover_tooltip() const;
void clear_hover_tooltip() { m_hover_tooltip.clear(); }
void register_raycasters_for_picking(GLCanvas3D& canvas)
{
for (auto plate : m_plate_list)
+62 -3
View File
@@ -265,6 +265,43 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr
return sizer;
};
auto ultimaker_generate_creds = [=](wxWindow* parent) {
auto sizer = create_sizer_with_btn(parent, &m_printhost_generate_creds_btn, "ultimaker_generate_creds", _L("Generate API Key"));
m_printhost_generate_creds_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent& e) {
std::unique_ptr<PrintHost> host(PrintHost::get_print_host(m_config));
if (!host) {
const wxString text = _L("Could not get a valid Printer Host reference");
show_error(this, text);
return;
}
wxString msg = "generate_auth_creds";
bool result;
{
// Show a wait cursor during the connection test, as it is blocking UI.
wxBusyCursor wait;
// Send request to printer for api key and such
result = host->test(msg); // using test with special input because I don't want to create the generate_auth_creds func for every printer
// Prompt user to approve access on the machine.
show_info(this, "API Key created. Go to the physical printer and hit \"authorize\" on the screen, then run \"Test\" again.\n"+msg, "API Key created.");
}
if (result)
show_info(this, host->get_test_ok_msg(), _L("Success!"));
else
show_error(this, host->get_test_failed_msg(msg));
update();
});
return sizer;
};
auto print_host_logout = [&](wxWindow* parent) {
auto sizer = create_sizer_with_btn(parent, &m_printhost_logout_btn, "", _L("Log Out"));
@@ -303,6 +340,7 @@ void PhysicalPrinterDialog::build_printhost_settings(ConfigOptionsGroup* m_optgr
Line host_line = m_optgroup->create_single_option_line(option);
host_line.append_widget(printhost_browse);
host_line.append_widget(print_host_test);
host_line.append_widget(ultimaker_generate_creds);
host_line.append_widget(print_host_logout);
m_optgroup->append_line(host_line);
@@ -606,8 +644,7 @@ void PhysicalPrinterDialog::update(bool printer_change)
m_optgroup->hide_field("bbl_use_print_host_webui");
m_optgroup->enable_field("printhost_cafile");
m_optgroup->enable_field("printhost_ssl_ignore_revoke");
if (m_printhost_cafile_browse_btn)
m_printhost_cafile_browse_btn->Enable();
if (m_printhost_cafile_browse_btn) { m_printhost_cafile_browse_btn->Enable(); }
// hide pre-configured address, in case user switched to a different host type
if (Field* printhost_field = m_optgroup->get_field("print_host"); printhost_field) {
@@ -704,7 +741,24 @@ void PhysicalPrinterDialog::update(bool printer_change)
m_optgroup->hide_field("printhost_authorization_type");
} else {
m_optgroup->hide_field("flashforge_serial_number");
}
}
if (opt->value == htUltiMaker) {
m_optgroup->hide_field("printhost_apikey");
m_optgroup->hide_field("printhost_authorization_type");
m_optgroup->hide_field("bbl_use_print_host_webui");
m_optgroup->hide_field("printhost_cafile");
m_optgroup->show_field("printhost_user");
m_optgroup->show_field("printhost_password");
m_optgroup->enable_field("print_host");
m_optgroup->show_field("print_host_webui");
if (m_printhost_cafile_browse_btn) {
m_printhost_cafile_browse_btn->Disable();
}
if (m_printhost_generate_creds_btn) {
m_printhost_generate_creds_btn->Enable();
}
}
}
else {
m_optgroup->set_value("host_type", int(PrintHostType::htOctoPrint), false);
@@ -720,6 +774,10 @@ void PhysicalPrinterDialog::update(bool printer_change)
m_optgroup->show_field(opt_key, auth_type == AuthorizationType::atUserPassword);
}
// The "Generate API Key" button is only meaningful for UltiMaker printers.
if (m_printhost_generate_creds_btn)
m_printhost_generate_creds_btn->Show(tech == ptFFF && m_config->opt_enum<PrintHostType>("host_type") == htUltiMaker);
m_optgroup->show_field("printhost_port", supports_multiple_printers);
m_printhost_port_browse_btn->Show(supports_multiple_printers);
@@ -787,6 +845,7 @@ void PhysicalPrinterDialog::on_dpi_changed(const wxRect& suggested_rect)
m_printhost_browse_btn->Rescale();
m_printhost_test_btn->Rescale();
m_printhost_generate_creds_btn->Rescale();
m_printhost_logout_btn->Rescale();
if (m_printhost_cafile_browse_btn)
m_printhost_cafile_browse_btn->Rescale();
+1
View File
@@ -31,6 +31,7 @@ class PhysicalPrinterDialog : public DPIDialog
Button* m_printhost_browse_btn {nullptr};
Button* m_printhost_test_btn {nullptr};
Button* m_printhost_generate_creds_btn {nullptr};
Button* m_printhost_logout_btn {nullptr};
Button* m_printhost_cafile_browse_btn {nullptr};
Button* m_printhost_port_browse_btn {nullptr};
+183 -106
View File
@@ -91,6 +91,9 @@
#include "wxExtensions.hpp"
#include "../Utils/PrintHost.hpp"
#include "MainFrame.hpp"
#ifdef SLIC3R_CAD
#include "slic3r/GUI/CAD/DesignPanel.hpp"
#endif
#include "format.hpp"
#include "3DScene.hpp"
#include "GLCanvas3D.hpp"
@@ -145,7 +148,6 @@
#include "Widgets/RadioGroup.hpp"
#include "Widgets/CheckBox.hpp"
#include "Widgets/Button.hpp"
#include "Widgets/StaticGroup.hpp"
#include "GUI_ObjectTable.hpp"
#include "libslic3r/Thread.hpp"
@@ -472,14 +474,9 @@ enum class ActionButtonType : int {
abSendGCode
};
// Background for the extruder-group title chip and its edit buttons, matching the StaticGroup
// interior. macOS keeps a lighter #F7F7F7 tint in light mode; dark mode uses the mapped colour.
// Background for the extruder-group title chip and its edit buttons
static wxColour extruder_group_chip_bg()
{
#ifdef __WXOSX__
if (!wxGetApp().dark_mode())
return wxColour("#F7F7F7");
#endif
return StateColor::darkModeColorFor(*wxWHITE);
}
@@ -494,38 +491,39 @@ public:
SetBackgroundColour(extruder_group_chip_bg());
auto sizer = new wxBoxSizer(wxHORIZONTAL);
auto label_color = StateColor::darkModeColorFor(wxColour("#363636"));
m_label = new wxStaticText(this, wxID_ANY, label, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
m_label->SetFont(Label::Body_13);
m_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B")));
m_label->SetFont(Label::Body_12);
m_label->SetForegroundColour(label_color);
m_brace_left = new wxStaticText(this, wxID_ANY, "(", wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
m_brace_left->SetFont(Label::Body_13);
m_brace_left->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
m_brace_left->SetFont(Label::Body_12);
m_brace_left->SetForegroundColour(label_color);
m_brace_left->Hide();
m_count = new wxStaticText(this, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
m_count->SetFont(Label::Body_13.Bold());
m_count->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
m_count->SetFont(Label::Body_12.Bold());
m_count->SetForegroundColour(label_color);
m_count->Hide();
m_brace_right = new wxStaticText(this, wxID_ANY, ")", wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
m_brace_right->SetFont(Label::Body_13);
m_brace_right->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
m_brace_right->SetFont(Label::Body_12);
m_brace_right->SetForegroundColour(label_color);
m_brace_right->Hide();
m_hover_btn = new ScalableButton(this, wxID_ANY, "dot");
m_hover_btn->SetMinSize(wxSize(FromDIP(25), -1));
m_hover_btn = new ScalableButton(this, wxID_ANY, "edit_12px", wxEmptyString, FromDIP(wxSize(12,12)), wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 12);
m_hover_btn->SetBackgroundColour(extruder_group_chip_bg());
m_hover_btn->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &evt) {
if (m_enabled && m_hover_on_click)
m_hover_on_click();
});
sizer->Add(m_label, 0, wxALIGN_CENTER_VERTICAL);
sizer->Add(m_brace_left, 0, wxALIGN_CENTER_VERTICAL);
sizer->Add(m_count, 0, wxALIGN_CENTER_VERTICAL);
sizer->Add(m_label , 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, FromDIP(4));
sizer->Add(m_brace_left , 0, wxALIGN_CENTER_VERTICAL);
sizer->Add(m_count , 0, wxALIGN_CENTER_VERTICAL);
sizer->Add(m_brace_right, 0, wxALIGN_CENTER_VERTICAL);
sizer->Add(m_hover_btn, 0, wxLEFT | wxALIGN_CENTER_VERTICAL, FromDIP(5));
sizer->Add(m_hover_btn , 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, FromDIP(4));
// No SetSizerAndFit: that would record the count-hidden width as an explicit min size,
// which outranks best size in sizer allocation, so once the count is shown any ancestor
@@ -537,7 +535,7 @@ public:
void EnableEdit(bool enable)
{
m_enabled = enable;
m_hover_btn->SetBitmap_(enable ? "edit" : "dot");
//m_hover_btn->SetBitmap_(enable ? "edit_12px" : "dot"); // it causes crash if icon sizes not matches
}
void SetOnHoverClick(std::function<void()> on_click) { m_hover_on_click = std::move(on_click); }
@@ -548,11 +546,13 @@ public:
m_count->Hide();
m_brace_left->Hide();
m_brace_right->Hide();
m_hover_btn->Hide();
} else {
m_count->SetLabel(wxString::Format("%d", count));
m_count->Show();
m_brace_left->Show();
m_brace_right->Show();
m_hover_btn->Show();
}
UpdateSizing();
}
@@ -563,15 +563,19 @@ public:
UpdateSizing();
}
void Rescale() { m_hover_btn->msw_rescale(); }
void Rescale() {
m_hover_btn->msw_rescale();
UpdateSizing();
}
// Re-apply the chip colours on a live light/dark switch (they are set once at construction).
void sys_color_changed()
{
SetBackgroundColour(extruder_group_chip_bg());
m_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B")));
auto label_color = StateColor::darkModeColorFor(wxColour("#363636"));
m_label->SetForegroundColour(label_color);
for (wxStaticText *t : {m_brace_left, m_count, m_brace_right})
t->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
t->SetForegroundColour(label_color);
m_hover_btn->SetBackgroundColour(extruder_group_chip_bg());
Refresh();
}
@@ -598,11 +602,12 @@ private:
bool m_enabled{false};
};
struct ExtruderGroup : StaticGroup
struct ExtruderGroup : StaticBox
{
ExtruderGroup(wxWindow * parent, int index, wxString const &title);
wxStaticBoxSizer *sizer = nullptr;
wxBoxSizer * sizer = nullptr;
HoverLabel * hover_label = nullptr;
wxStaticText* ams_label{nullptr};
ScalableButton * btn_edit = nullptr;
ComboBox * combo_diameter = nullptr;
ComboBox * combo_flow = nullptr;
@@ -642,8 +647,10 @@ struct ExtruderGroup : StaticGroup
{
if (hover_label)
hover_label->Rescale();
if (btn_edit)
if (btn_edit){
btn_edit->msw_rescale();
btn_edit->SetMinSize(ams_label->GetSize());
}
btn_up->msw_rescale();
btn_down->msw_rescale();
combo_diameter->Rescale();
@@ -858,9 +865,9 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual)
// double
extruder_dual_sizer = new wxBoxSizer(wxHORIZONTAL);
extruder_dual_sizer->Add(left_extruder->sizer, 1, wxEXPAND, 0);
extruder_dual_sizer->Add(left_extruder, 1, wxEXPAND, 0);
extruder_dual_sizer->AddSpacer(FromDIP(4));
extruder_dual_sizer->Add(right_extruder->sizer, 1, wxEXPAND, 0);
extruder_dual_sizer->Add(right_extruder, 1, wxEXPAND, 0);
// Filament Track Switch status icon, floated over the extruder AMS area (positioned in
// update_extruder_separator_icon). Created hidden; a click re-shows the ready/not-ready tip.
@@ -875,7 +882,9 @@ void Sidebar::priv::layout_printer(bool isBBL, bool isDual)
}
// single
extruder_single_sizer = single_extruder->sizer;
extruder_single_sizer = new wxBoxSizer(wxHORIZONTAL);
extruder_single_sizer->Add(single_extruder, 1, wxEXPAND, 0);
wxBoxSizer * extruder_sizer = new wxBoxSizer(wxVERTICAL);
extruder_sizer->Add(extruder_dual_sizer , 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin()));
extruder_sizer->Add(extruder_single_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(SidebarProps::ContentMargin()));
@@ -1253,7 +1262,7 @@ public:
Bind(wxEVT_PAINT, [this](wxPaintEvent& evt) {
wxPaintDC dc(this);
dc.SetPen(StateColor::darkModeColorFor(wxColour("#DBDBDB"))); // ORCA match popup border color
dc.SetPen(StateColor::darkModeColorFor(wxColour("#009688"))); // ORCA match popup border color
dc.SetBrush(*wxTRANSPARENT_BRUSH);
dc.DrawRoundedRectangle(0, 0, GetSize().x, GetSize().y, 0);
});
@@ -1307,29 +1316,25 @@ public:
};
ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title)
: StaticGroup(parent, wxID_ANY, wxString())
: StaticBox(parent)
{
SetFont(Label::Body_10);
SetForegroundColour(wxColour("#CECECE"));
SetBorderColor(wxColour("#EEEEEE"));
SetCornerRadius(FromDIP(PRINTER_PANEL_RADIUS)); // ORCA match radius with other boxes
ShowBadge(true);
SetTopMargin(FromDIP(7)); // ORCA
// The title lives in an interactive row inside the card (with the nozzle-count badge and its edit
// button) instead of being painted on the border by StaticGroup.
// The title lives in an interactive row inside the card (with the nozzle-count badge and its edit button)
hover_label = new HoverLabel(this, title);
hover_label->SetPosition(wxPoint(FromDIP(PRINTER_PANEL_RADIUS), 0)); // position it without putting in a sizer so it will look like title
// Nozzle
wxStaticText *label_diameter = new wxStaticText(this, wxID_ANY, _L("Diameter"));
label_diameter->SetFont(Label::Body_14);
label_diameter->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
if (index >= 0) label_diameter->SetMinSize({FromDIP(80), -1});
auto combo_diameter = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY);
this->combo_diameter = combo_diameter;
wxStaticText *label_flow = new wxStaticText(this, wxID_ANY, _L("Flow"));
label_flow->SetFont(Label::Body_14);
label_flow->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
if (index >= 0) label_flow->SetMinSize({FromDIP(80), -1});
combo_diameter->SetToolTip(_L("Diameter"));
// Flow
auto combo_flow = new ComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, wxCB_READONLY);
combo_flow->GetDropDown().SetUseContentWidth(true);
combo_flow->Bind(wxEVT_COMBOBOX, [index, combo_flow](wxCommandEvent &evt) {
@@ -1346,51 +1351,75 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title
}
});
this->combo_flow = combo_flow;
combo_flow->SetToolTip(_L("Flow"));
// AMS
wxStaticText *label_ams = new wxStaticText(this, wxID_ANY, _L("AMS"));
label_ams->SetFont(Label::Body_14);
label_ams->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
//label_ams->SetMinSize({FromDIP(70), -1});
auto ams_panel = new wxPanel(this, wxID_ANY);
ams_panel->SetBackgroundColour(*wxWHITE);
ams_label = new wxStaticText(ams_panel, wxID_ANY, _L("AMS"));
ams_label->SetFont(Label::Body_14);
ams_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636")));
// AMS not installed message
ams_not_installed_msg = new wxStaticText(ams_panel, wxID_ANY, _L("Not installed"));
ams_not_installed_msg->SetFont(Label::Body_14);
ams_not_installed_msg->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B")));
if (index >= 0) {
btn_edit = new ScalableButton(this, wxID_ANY, "dot");
btn_edit = new ScalableButton(ams_panel, wxID_ANY, "edit");
btn_edit->SetMinSize(ams_label->GetSize());
btn_edit->SetBackgroundColour(extruder_group_chip_bg());
btn_edit->Hide();
btn_edit->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this, index](auto &evt) {
btn_edit->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this, index, combo_diameter](auto &evt) {
PopupWindow *window = new AMSCountPopupWindow(this, index);
auto size = GetSize();
auto pos = ClientToScreen({0, size.y + 12});
auto size = GetSize();
auto pos = ClientToScreen({0, size.y - FromDIP(8) - combo_diameter->GetSize().y});
size.SetWidth(size.GetWidth() + FromDIP(10));
window->Position(pos, {0, 0});
window->Popup();
});
auto hovered = std::make_shared<wxWindow *>();
for (wxWindow *w : std::initializer_list<wxWindow *>{this, label_diameter, combo_diameter, label_flow, combo_flow, btn_edit, label_ams}) {
w->Bind(wxEVT_ENTER_WINDOW, [w, hovered, this](wxMouseEvent &evt) { *hovered = w; btn_edit->SetBitmap_("edit"); });
w->Bind(wxEVT_LEAVE_WINDOW, [w, hovered, this](wxMouseEvent &evt) { if (*hovered == w) { btn_edit->SetBitmap_("dot"); *hovered = nullptr; } });
for (wxWindow *w : std::initializer_list<wxWindow *>{this, btn_edit, ams_not_installed_msg, ams_label, ams_panel}) {
// ORCA using CallAfter fixes crash on linux while clicking edit button
w->Bind(wxEVT_ENTER_WINDOW, [w, hovered, this](wxMouseEvent &evt) {
*hovered = w;
this->CallAfter([this]() {
btn_edit->Show();
ams_label->Hide();
hsizer_ams->Layout();
});
});
w->Bind(wxEVT_LEAVE_WINDOW, [w, hovered, this](wxMouseEvent &evt) {
if (*hovered == w) {
*hovered = nullptr;
this->CallAfter([this]() {
btn_edit->Hide();
ams_label->Show();
hsizer_ams->Layout();
});
}
});
}
}
// AMS not installed message
ams_not_installed_msg = new wxStaticText(this, wxID_ANY, _L("Not installed"));
ams_not_installed_msg->SetFont(Label::Body_14);
ams_not_installed_msg->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
// AMS group
for (size_t i = 0; i < 4; ++i) {
ams[i] = new AMSPreview(this, wxID_ANY, AMSinfo(), AMSModel::GENERIC_AMS);
ams[i] = new AMSPreview(ams_panel, wxID_ANY, AMSinfo(), AMSModel::GENERIC_AMS);
ams[i]->Close();
}
hsizer_ams = new wxBoxSizer(wxHORIZONTAL);
hsizer_ams->SetMinSize(0, ams[0]->GetMinHeight());
hsizer_ams->Add(label_ams, 0, wxALIGN_CENTER);
hsizer_ams->Add(ams_label, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(5));
if (btn_edit)
hsizer_ams->Add(btn_edit, 0, wxLEFT | wxALIGN_CENTER, FromDIP(2));
hsizer_ams->Add(ams_not_installed_msg, 0, wxALIGN_CENTER);
hsizer_ams->Add(btn_edit, 0, wxALIGN_CENTER | wxRIGHT, FromDIP(5));
hsizer_ams->Add(ams_not_installed_msg, 1, wxALIGN_CENTER);
btn_up = new ScalableButton(this, wxID_ANY, "page_up", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 14);
ams_panel->SetSizer(hsizer_ams);
btn_up = new ScalableButton(ams_panel, wxID_ANY, "page_up", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 14);
btn_up->SetBackgroundColour(*wxWHITE);
btn_up->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &evt) {
if (page_cur > 0)
@@ -1398,7 +1427,7 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title
update_ams();
});
btn_up->Hide();
btn_down = new ScalableButton(this, wxID_ANY, "page_down", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 14);
btn_down = new ScalableButton(ams_panel, wxID_ANY, "page_down", "", {FromDIP(14), FromDIP(14)}, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, false, 14);
btn_down->SetBackgroundColour(*wxWHITE);
btn_down->Bind(wxEVT_COMMAND_BUTTON_CLICKED, [this](auto &evt) {
if (page_cur + 1 < page_num)
@@ -1407,31 +1436,24 @@ ExtruderGroup::ExtruderGroup(wxWindow * parent, int index, wxString const &title
});
btn_down->Hide();
wxBoxSizer *hsizer_diameter = new wxBoxSizer(wxHORIZONTAL);
hsizer_diameter->Add(label_diameter, 0, wxALIGN_CENTER);
hsizer_diameter->Add(combo_diameter, 1, wxEXPAND);
wxBoxSizer * hsizer_nozzle = new wxBoxSizer(wxHORIZONTAL);
hsizer_nozzle->Add(label_flow, 0, wxALIGN_CENTER);
hsizer_nozzle->Add(combo_flow, 1, wxEXPAND);
wxBoxSizer *vsizer = new wxBoxSizer(wxVERTICAL);
wxBoxSizer *hsizer = new wxBoxSizer(wxHORIZONTAL);
hsizer->Add(combo_diameter, 1, wxRIGHT, FromDIP(5));
hsizer->Add(combo_flow , 1);
vsizer->AddSpacer(FromDIP(16)); // spacing for title and control
if (index < 0) {
label_ams->Hide();
ams_not_installed_msg->Hide();
wxStaticBoxSizer *vsizer = new wxStaticBoxSizer(this, wxVERTICAL);
wxBoxSizer *hsizer = new wxBoxSizer(wxHORIZONTAL);
hsizer->Add(hsizer_diameter, 1, wxEXPAND | wxTOP| wxBOTTOM, FromDIP(8));
hsizer->Add(hsizer_nozzle, 1, wxEXPAND | wxALL, FromDIP(8));
hsizer->AddSpacer(FromDIP(2)); // Avoid badge
vsizer->Add(hover_label, 0, wxLEFT | wxALL, FromDIP(2));
vsizer->Add(hsizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(2));
this->sizer = vsizer;
ams_panel->Hide();
} else {
wxStaticBoxSizer *vsizer = new wxStaticBoxSizer(this, wxVERTICAL);
vsizer->Add(hover_label, 0, wxLEFT | wxALL, FromDIP(2));
vsizer->Add(hsizer_ams, 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, FromDIP(2));
vsizer->Add(hsizer_diameter, 0, wxEXPAND | wxLEFT | wxTOP | wxRIGHT, FromDIP(2));
vsizer->Add(hsizer_nozzle, 0, wxEXPAND | wxALL, FromDIP(2));
this->sizer = vsizer;
vsizer->Add(ams_panel, 0, wxEXPAND | wxLEFT | wxRIGHT , FromDIP(5));
vsizer->AddSpacer(FromDIP(2));
}
vsizer->Add(hsizer, 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(5));
SetSizer(vsizer);
Layout();
AMSCountPopupWindow::UpdateAMSCount(index < 0 ? 0 : index, this);
}
@@ -1502,7 +1524,7 @@ void ExtruderGroup::update_ams()
}
}
sizer->Layout();
Layout();
}
void ExtruderGroup::sync_ams(MachineObject const *obj, std::vector<DevAms *> const &ams4, std::vector<DevAms *> const &ams1)
@@ -6479,6 +6501,11 @@ Search::OptionsSearcher& Sidebar::get_searcher()
return p->searcher;
}
Search::SettingsIndex& Sidebar::settings_index()
{
return p->searcher.index();
}
std::string& Sidebar::get_search_line()
{
return p->searcher.search_string();
@@ -7628,10 +7655,6 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame)
view3D_canvas->Bind(EVT_GLCANVAS_SELECT_ALL, [this](SimpleEvent&) { this->q->select_all(); });
view3D_canvas->Bind(EVT_GLCANVAS_QUESTION_MARK, [](SimpleEvent&) { wxGetApp().keyboard_shortcuts(); });
view3D_canvas->Bind(EVT_GLCANVAS_OPEN_SPEED_DIAL, [this](SimpleEvent&) {
if (this->q->is_view3D_shown())
wxGetApp().open_speed_dial();
});
view3D_canvas->Bind(EVT_GLCANVAS_INCREASE_INSTANCES, [this](Event<int>& evt)
{ if (evt.data == 1) this->q->increase_instances(); else if (this->can_decrease_instances()) this->q->decrease_instances(); });
view3D_canvas->Bind(EVT_GLCANVAS_INSTANCE_MOVED, [this](SimpleEvent&) { update(); });
@@ -8387,6 +8410,9 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
int answer_convert_from_meters = wxOK_DEFAULT;
int answer_convert_from_imperial_units = wxOK_DEFAULT;
int tolal_model_count = 0;
// Whether one of the files being loaded here carried a CAD recipe. A statement about these
// files, not about the plater — q->model() may still hold the previous project's recipe.
bool loaded_cad_recipe = false;
int progress_percent = 0;
int total_files = input_files.size();
@@ -9512,6 +9538,16 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
auto loaded_idxs = load_model_objects(model.objects, is_project_file);
obj_idxs.insert(obj_idxs.end(), loaded_idxs.begin(), loaded_idxs.end());
// load_model_objects only transfers ModelObjects; carry the Model-level CAD recipe
// onto the plater model so the Design tab can rehydrate the editable feature tree on
// reopen. Assigned unconditionally on the project-replacing path so that opening a
// project without a recipe clears whatever the previous one left behind; importing a
// plain model into the open project leaves the current recipe alone.
if (is_project_file) {
q->model().cad_recipe = model.cad_recipe;
loaded_cad_recipe = !model.cad_recipe.empty();
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(", finished load_model_objects");
wxString msg = wxString::Format(_L("Loading file: %s"), from_path(real_filename));
dlg_cont = dlg.Update(progress_percent, msg);
@@ -9728,7 +9764,12 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
// q->model().stl_design_country = "";
//}
if (tolal_model_count <= 0 && !q->m_exported_file) {
// A CAD project legitimately carries no mesh: the model lives in the feature tree until it is
// committed to the plate. Warning "no geometry data" for one is false, and it is the LAST thing
// a user sees after opening a design they spent an hour on — it reads as "your work is gone"
// when the recipe has in fact just been loaded and the Design tab will rehydrate it. Count a
// recipe that came from THESE files as geometry.
if (tolal_model_count <= 0 && !loaded_cad_recipe && !q->m_exported_file) {
dlg.Hide();
if (!is_user_cancel) {
MessageDialog msg(wxGetApp().mainframe, _L("The file does not contain any geometry data."), _L("Warning"), wxYES | wxICON_WARNING);
@@ -10275,6 +10316,16 @@ void Plater::priv::reset(bool apply_presets_change)
// Stop and reset the Print content.
this->background_process.reset();
model.clear_objects();
// clear_objects() only drops the ModelObjects; the CAD recipe is Model-level state and would
// otherwise be written into every project saved for the rest of the session.
model.cad_recipe.clear();
#ifdef SLIC3R_CAD
// Same reason, one level up: the Design tab keeps the editable document, not the Model, so
// clearing the recipe alone leaves the tab showing the previous project's feature tree —
// and its next edit syncs that tree straight back into the new project.
if (wxGetApp().mainframe != nullptr && wxGetApp().mainframe->m_design_panel != nullptr)
wxGetApp().mainframe->m_design_panel->clear_document();
#endif
assemble_view->get_canvas3d()->reset_explosion_ratio();
update();
@@ -12860,17 +12911,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
@@ -13777,6 +13819,14 @@ void Plater::priv::unbind_canvas_event_handlers()
if (assemble_view != nullptr)
assemble_view->get_canvas3d()->unbind_event_handlers();
#ifdef SLIC3R_CAD
// The Design tab's viewport is a fourth GLCanvas3D on the same shared GL context, owned by
// MainFrame rather than by us — same reach as reset() uses for clear_document(). Null until
// the tab has been opened once, so most sessions skip it.
if (wxGetApp().mainframe != nullptr && wxGetApp().mainframe->m_design_panel != nullptr)
wxGetApp().mainframe->m_design_panel->unbind_canvas_event_handlers();
#endif
}
void Plater::priv::reset_canvas_volumes()
@@ -13786,6 +13836,11 @@ void Plater::priv::reset_canvas_volumes()
if (preview != nullptr)
preview->get_canvas3d()->reset_volumes();
#ifdef SLIC3R_CAD
if (wxGetApp().mainframe != nullptr && wxGetApp().mainframe->m_design_panel != nullptr)
wxGetApp().mainframe->m_design_panel->reset_canvas_volumes();
#endif
}
bool Plater::priv::check_ams_status_impl(bool is_slice_all)
@@ -15737,8 +15792,12 @@ bool Plater::up_to_date(bool saved, bool backup)
Slic3r::clear_other_changes(backup);
return p->up_to_date(saved, backup);
}
return p->model.objects.empty() || (p->up_to_date(saved, backup) &&
!Slic3r::has_other_changes(backup));
// A Design-tab project is object-less until it is committed to the plate, but its feature
// tree is real work: treating it as an empty project skipped both the autosave and the
// "unsaved changes" prompt, so quitting threw it away without asking. Non-CAD projects
// never carry a recipe, so the empty-project shortcut is unchanged for them.
return (p->model.objects.empty() && p->model.cad_recipe.empty()) ||
(p->up_to_date(saved, backup) && !Slic3r::has_other_changes(backup));
}
bool Plater::add_model(bool imperial_units, std::string fname)
@@ -21930,6 +21989,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;
+4
View File
@@ -283,6 +283,7 @@ public:
std::vector<std::string>& types,
std::vector<size_t>* config_indices = nullptr);
Search::OptionsSearcher& get_searcher();
Search::SettingsIndex& settings_index();
std::string& get_search_line();
void update_printer_thumbnail();
@@ -781,6 +782,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);
+185 -122
View File
@@ -126,15 +126,6 @@ PluginCapabilityType primary_capability_type_of(PluginManager& manager, const st
return capabilities.empty() ? PluginCapabilityType::Unknown : capabilities.front()->type();
}
std::vector<PluginDescriptor> current_cloud_metadata_snapshot()
{
std::vector<PluginDescriptor> cloud_entries;
for (const PluginDescriptor& entry : PluginManager::instance().get_plugin_descriptors(/*include_invalid=*/true))
if (entry.is_cloud_plugin())
cloud_entries.push_back(entry);
return cloud_entries;
}
PluginDescriptor as_cloud_only_descriptor(PluginDescriptor descriptor)
{
descriptor.plugin_root.clear();
@@ -150,41 +141,6 @@ PluginDescriptor as_cloud_only_descriptor(PluginDescriptor descriptor)
return descriptor;
}
void refresh_plugin_metadata_blocking(bool fetch_cloud)
{
PluginManager& manager = PluginManager::instance();
std::vector<std::string> not_found, unauthorized;
const std::vector<PluginDescriptor> current_cloud_metadata = fetch_cloud ? std::vector<PluginDescriptor>{} :
current_cloud_metadata_snapshot();
manager.rescan_plugins();
if (!fetch_cloud) {
manager.update_cloud_metadata(current_cloud_metadata);
return;
}
manager.fetch_plugins_from_cloud(&not_found, &unauthorized);
wxGetApp().CallAfter([not_found = std::move(not_found), unauthorized = std::move(unauthorized)]() {
if (wxGetApp().is_closing())
return;
Plater* plater = wxGetApp().plater();
if (plater == nullptr)
return;
for (const auto& uuid : not_found)
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
NotificationManager::NotificationLevel::RegularNotificationLevel,
format(_L("Plugin %s is no longer available."), uuid));
for (const auto& uuid : unauthorized)
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
NotificationManager::NotificationLevel::RegularNotificationLevel,
format(_L("Plugin %s access is unauthorized."), uuid));
});
}
std::string to_string(PluginUpdateStatus status);
nlohmann::json build_context_actions_payload(const PluginAvailableActions& available_actions);
@@ -442,6 +398,176 @@ bool take_plugin_operation_result(const std::shared_ptr<PluginOperationState>& s
}
} // namespace
// ── Dialog-independent plugin actions (also used by the speed dial) ───────────────────────────
namespace {
// Snapshot of the currently-known cloud plugin descriptors, used to refresh metadata without a
// network round-trip (kUseCurrentCloudMeta).
std::vector<PluginDescriptor> current_cloud_metadata_snapshot()
{
std::vector<PluginDescriptor> cloud_entries;
for (const PluginDescriptor& entry : PluginManager::instance().get_plugin_descriptors(/*include_invalid=*/true))
if (entry.is_cloud_plugin())
cloud_entries.push_back(entry);
return cloud_entries;
}
} // namespace
void refresh_plugin_metadata_blocking(bool fetch_cloud)
{
PluginManager& manager = PluginManager::instance();
std::vector<std::string> not_found, unauthorized;
const std::vector<PluginDescriptor> current_cloud_metadata = fetch_cloud ? std::vector<PluginDescriptor>{} :
current_cloud_metadata_snapshot();
manager.rescan_plugins();
if (!fetch_cloud) {
manager.update_cloud_metadata(current_cloud_metadata);
return;
}
manager.fetch_plugins_from_cloud(&not_found, &unauthorized);
wxGetApp().CallAfter([not_found = std::move(not_found), unauthorized = std::move(unauthorized)]() {
if (wxGetApp().is_closing())
return;
Plater* plater = wxGetApp().plater();
if (plater == nullptr)
return;
for (const auto& uuid : not_found)
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
NotificationManager::NotificationLevel::RegularNotificationLevel,
format(_L("Plugin %s is no longer available."), uuid));
for (const auto& uuid : unauthorized)
plater->get_notification_manager()->push_notification(NotificationType::CustomNotification,
NotificationManager::NotificationLevel::RegularNotificationLevel,
format(_L("Plugin %s access is unauthorized."), uuid));
});
}
void open_plugin_hub()
{
std::string cloud_base_url = "https://cloud.orcaslicer.com";
if (wxGetApp().getAgent()) {
auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(wxGetApp().getAgent()->get_cloud_agent());
if (orca_agent && !orca_agent->get_cloud_base_url().empty())
cloud_base_url = orca_agent->get_cloud_base_url();
}
while (!cloud_base_url.empty() && cloud_base_url.back() == '/')
cloud_base_url.pop_back();
if (cloud_base_url.empty())
cloud_base_url = "https://cloud.orcaslicer.com";
wxLaunchDefaultBrowser(wxString::FromUTF8(cloud_base_url + "/app/plugins/plugin-hub"));
}
bool install_local_plugin_package(const boost::filesystem::path& package_file, wxWindow* parent, wxString& message)
{
message.clear();
if (package_file.empty())
return false;
// ---- pre-flight (main thread): validate + inspect + overwrite prompt ----
const wxString package_name = from_u8(package_file.filename().string());
std::string extension = package_file.extension().string();
std::transform(extension.begin(), extension.end(), extension.begin(),
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
if (extension != ".py" && extension != ".whl") {
message = _L("Select a .py or .whl plugin package.");
return false;
}
PluginDescriptor plugin_descriptor;
bool existing_installation = false;
std::string error;
try {
if (!PluginManager::instance().inspect_local_plugin_package(package_file, plugin_descriptor, existing_installation, error)) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package inspection failed for " << package_file << " error=" << error;
message = _L("Failed to install plugin package. See the log for details.");
return false;
}
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package inspection failed for " << package_file << " error=" << ex.what();
message = _L("Failed to install plugin package. See the log for details.");
return false;
} catch (...) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package inspection failed for " << package_file;
message = _L("Failed to install plugin package. See the log for details.");
return false;
}
if (existing_installation) {
const wxString plugin_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name);
wxMessageDialog dialog(parent,
wxString::Format(_L("Plugin \"%s\" is already installed.\n\nInstalling this package will overwrite the existing plugin."),
plugin_name),
kOverwritePluginTitle, wxOK | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING);
dialog.SetOKCancelLabels(_L("Overwrite"), _L("Cancel"));
if (dialog.ShowModal() != wxID_OK) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Plugin package installation cancelled before overwrite. package=" << package_file
<< " plugin=" << plugin_descriptor.name;
return false; // cancelled: message stays empty so callers stay silent
}
}
// ---- install + refresh on a worker behind a modal progress dialog (keeps the UI live) ----
bool installed = false;
{
struct Result
{
std::mutex mutex;
bool ok = false;
std::string error;
};
auto state = std::make_shared<Result>();
detail::run_wait_with_progress(
[state, package_file]() {
std::string error;
bool ok = false;
try {
ok = PluginManager::instance().install_plugin(package_file, error);
} catch (const std::exception& ex) {
error = ex.what();
} catch (...) {
error = "Unknown error";
}
if (ok) {
// Reflect the new package in discovery/cloud metadata without blocking the caller.
try { refresh_plugin_metadata_blocking(kUseCurrentCloudMeta); } catch (...) {}
}
std::lock_guard<std::mutex> lock(state->mutex);
state->ok = ok;
state->error = std::move(error);
},
parent, _L("Installing plugin"), _L("Installing plugin") + ": " + package_name, 100,
wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME, /*alive=*/nullptr, /*restore=*/{});
std::lock_guard<std::mutex> lock(state->mutex);
installed = state->ok;
error = std::move(state->error);
}
if (!installed) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": Plugin package installation failed for " << package_file << " error=" << error;
message = _L("Failed to install plugin package. See the log for details.");
return false;
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Plugin package installed successfully from " << package_file;
const wxString installed_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name);
message = wxString::Format(_L("Installed \"%s\"."), installed_name);
return true;
}
PluginsDialog::PluginsDialog(wxWindow* parent, wxWindowID id, const wxString&, const wxPoint& pos, const wxSize& size, long style)
: WebViewHostDialog(parent, id, _L("Plugins"), pos, size, style)
{ create_webview("web/dialog/PluginsDialog/index.html", _L("Plugins"), wxSize(900, 820), wxSize(760, 715)); }
@@ -816,78 +942,31 @@ bool PluginsDialog::install_plugin_package(const std::string& package_path)
{
if (package_path.empty())
return false;
BOOST_LOG_TRIVIAL(info) << "Installing local plugin package from path: " << package_path;
std::string error;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": Installing local plugin package from path: " << package_path;
const boost::filesystem::path package_file(package_path);
const wxString package_name = from_u8(package_file.filename().string());
wxString message;
const bool installed = install_local_plugin_package(package_file, this, message);
// The helper's overwrite prompt and progress dialog can push this webview behind; re-raise it
// once, after both have closed (the speed-dial path parents to the mainframe instead).
restore_z_order();
std::string extension = package_file.extension().string();
std::transform(extension.begin(), extension.end(), extension.begin(),
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
if (extension != ".py" && extension != ".whl") {
show_status(_L("Select a .py or .whl plugin package."), "info");
return false;
}
PluginDescriptor plugin_descriptor;
bool existing_installation = false;
auto report_inspection_failure = [&]() {
BOOST_LOG_TRIVIAL(error) << "Plugin package inspection failed for " << package_path << " error=" << error;
show_status(_L("Failed to install plugin package. See the log for details."), "warn");
// The shared helper reports a user-cancelled overwrite with an empty message: stay silent.
if (message.IsEmpty()) {
send_plugins();
return false;
};
try {
if (!PluginManager::instance().inspect_local_plugin_package(package_file, plugin_descriptor, existing_installation, error))
return report_inspection_failure();
} catch (const std::exception& ex) {
error = ex.what();
return report_inspection_failure();
} catch (...) {
error = "Unknown error";
return report_inspection_failure();
}
if (existing_installation) {
const wxString plugin_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name);
wxMessageDialog dialog(
this,
wxString::Format(_L("Plugin \"%s\" is already installed.\n\nInstalling this package will overwrite the existing plugin."),
plugin_name),
kOverwritePluginTitle, wxOK | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING);
dialog.SetOKCancelLabels(_L("Overwrite"), _L("Cancel"));
const int overwrite_rc = dialog.ShowModal();
restore_z_order();
if (overwrite_rc != wxID_OK) {
BOOST_LOG_TRIVIAL(info) << "Plugin package installation cancelled before overwrite. package=" << package_path
<< " plugin=" << plugin_descriptor.name;
return false;
}
}
bool installed = false;
try {
installed = run_with_dialog_wait([package_file, &error]() { return PluginManager::instance().install_plugin(package_file, error); },
_L("Installing plugin"), _L("Installing plugin") + ": " + package_name, 100,
wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_ELAPSED_TIME);
} catch (const std::exception& ex) {
error = ex.what();
} catch (...) {
error = "Unknown error";
}
if (!installed) {
BOOST_LOG_TRIVIAL(error) << "Plugin package installation failed for " << package_path << " error=" << error;
show_status(_L("Failed to install plugin package. See the log for details."), "warn");
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to install plugin package.";
show_status(message, "warn");
send_plugins();
return false;
}
BOOST_LOG_TRIVIAL(info) << "Plugin package installed successfully from " << package_path;
const wxString installed_name = from_u8(plugin_descriptor.name.empty() ? package_file.filename().string() : plugin_descriptor.name);
show_status(wxString::Format(_L("Installed \"%s\"."), installed_name), "success");
refresh_plugin_metadata_async(_L("Refreshing"), _L("Refreshing plugins data"), kUseCurrentCloudMeta);
show_status(message, "success");
prompt_for_missing_plugins();
send_plugins();
return true;
}
@@ -1083,23 +1162,7 @@ void PluginsDialog::open_plugin_on_cloud(const std::string& sharing_token)
wxLaunchDefaultBrowser(wxString::FromUTF8(orca_agent->get_cloud_base_url() + "/p/" + sharing_token));
}
void PluginsDialog::open_plugin_hub()
{
std::string cloud_base_url = "https://cloud.orcaslicer.com";
if (wxGetApp().getAgent()) {
auto orca_agent = std::dynamic_pointer_cast<OrcaCloudServiceAgent>(wxGetApp().getAgent()->get_cloud_agent());
if (orca_agent && !orca_agent->get_cloud_base_url().empty())
cloud_base_url = orca_agent->get_cloud_base_url();
}
while (!cloud_base_url.empty() && cloud_base_url.back() == '/')
cloud_base_url.pop_back();
if (cloud_base_url.empty())
cloud_base_url = "https://cloud.orcaslicer.com";
wxLaunchDefaultBrowser(wxString::FromUTF8(cloud_base_url + "/app/plugins/plugin-hub"));
}
void PluginsDialog::open_plugin_hub() { Slic3r::GUI::open_plugin_hub(); }
void PluginsDialog::delete_local_plugin(const PluginDescriptor& plugin)
{
+183 -124
View File
@@ -25,6 +25,8 @@
#include <wx/string.h>
#include <wx/timer.h>
#include <boost/filesystem.hpp>
class wxTimer;
namespace Slic3r {
@@ -35,6 +37,184 @@ enum class PluginCapabilityType;
namespace GUI {
// Dialog-independent plugin-management actions, shared by the Plugins dialog and the speed dial:
// they never require the webview dialog to be open.
// Rescans local plugins and (optionally) re-fetches cloud metadata. Blocking: run off the UI
// thread. Used by PluginsDialog (behind its progress dialog) and GUI_App::refresh_plugins().
void refresh_plugin_metadata_blocking(bool fetch_cloud);
// Opens the Cloud plugin hub in the default browser. No dialog needed.
void open_plugin_hub();
// Synchronously installs a local plugin package (.py/.whl). Runs on the UI thread but keeps it
// responsive by performing the install on a worker behind a modal progress dialog. `parent` owns
// the overwrite prompt and the progress dialog. On success `message` carries the localized
// confirmation; on a user-cancelled overwrite it is empty; on failure it carries the reason.
bool install_local_plugin_package(const boost::filesystem::path& package_file, wxWindow* parent, wxString& message);
namespace detail {
// Shared worker + modal-progress machinery: pulse a progress dialog while `run` executes on a
// detached worker, then run `on_finish` back on the UI thread. `alive`, when non-null, gates both
// the pulse and `on_finish` so a worker outliving its dialog can't touch freed windows; pass null
// for a dialog-independent caller. `restore` runs after the progress dialog is destroyed and before
// `on_finish`, so a webview host can re-raise itself. `finish_after_dialog_destroyed` still calls
// `on_finish` (without touching the dialog) when the host died, so a waiting loop can exit.
template<typename Run, typename OnFinish>
void run_off_thread_with_progress(Run&& run,
OnFinish&& on_finish,
wxWindow* parent,
const wxString& title,
const wxString& message,
int maximum,
int style,
std::shared_ptr<std::atomic<bool>> alive,
bool finish_after_dialog_destroyed,
std::function<void()> restore)
{
wxProgressDialog* progress = new wxProgressDialog(title, message, maximum, parent, style);
wxTimer* timer = new wxTimer();
timer->Bind(wxEVT_TIMER, [alive, progress, message](wxTimerEvent&) {
if ((!alive || alive->load(std::memory_order_acquire)) && progress)
progress->Pulse(message);
});
timer->Start(100);
std::thread([alive,
progress,
timer,
run = std::forward<Run>(run),
on_finish = std::forward<OnFinish>(on_finish),
finish_after_dialog_destroyed,
restore = std::move(restore)]() mutable {
try {
run();
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(error) << "Plugin dialog worker failed: " << ex.what();
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "Plugin dialog worker failed with an unknown exception";
}
if (wxTheApp == nullptr)
return;
wxTheApp->CallAfter([alive,
progress,
timer,
on_finish = std::move(on_finish),
finish_after_dialog_destroyed,
restore = std::move(restore)]() mutable {
timer->Stop();
delete timer;
if (!alive || alive->load(std::memory_order_acquire)) {
progress->Destroy();
if (restore)
restore();
on_finish();
} else if (finish_after_dialog_destroyed) {
on_finish();
}
});
}).detach();
}
// Wait for a worker behind a progress dialog, returning its result (or rethrowing). The waiting
// loop stays responsive because it pumps the event loop the worker posts its completion into.
template<typename Run>
std::invoke_result_t<std::decay_t<Run>&> run_wait_with_progress(Run&& run,
wxWindow* parent,
const wxString& title,
const wxString& message,
int maximum,
int style,
std::shared_ptr<std::atomic<bool>> alive,
std::function<void()> restore)
{
using Result = std::invoke_result_t<std::decay_t<Run>&>;
bool finished = false;
wxEventLoop loop;
auto on_finish = [&finished, &loop]() {
finished = true;
if (loop.IsRunning())
loop.Exit();
};
if constexpr (std::is_void_v<Result>) {
struct WaitState
{
std::mutex mutex;
std::exception_ptr exception;
};
auto state = std::make_shared<WaitState>();
run_off_thread_with_progress(
[run = std::forward<Run>(run), state]() mutable {
try {
run();
} catch (...) {
std::lock_guard<std::mutex> lock(state->mutex);
state->exception = std::current_exception();
}
},
on_finish, parent, title, message, maximum, style, std::move(alive), /*finish_after_dialog_destroyed=*/true, std::move(restore));
if (!finished)
loop.Run();
std::exception_ptr exception;
{
std::lock_guard<std::mutex> lock(state->mutex);
exception = state->exception;
}
if (exception)
std::rethrow_exception(exception);
} else {
using StoredResult = std::decay_t<Result>;
struct WaitState
{
std::mutex mutex;
std::optional<StoredResult> result;
std::exception_ptr exception;
};
auto state = std::make_shared<WaitState>();
run_off_thread_with_progress(
[run = std::forward<Run>(run), state]() mutable {
try {
StoredResult result = run();
std::lock_guard<std::mutex> lock(state->mutex);
state->result.emplace(std::move(result));
} catch (...) {
std::lock_guard<std::mutex> lock(state->mutex);
state->exception = std::current_exception();
}
},
on_finish, parent, title, message, maximum, style, std::move(alive), /*finish_after_dialog_destroyed=*/true, std::move(restore));
if (!finished)
loop.Run();
std::optional<StoredResult> result;
std::exception_ptr exception;
{
std::lock_guard<std::mutex> lock(state->mutex);
if (state->result)
result.emplace(std::move(*state->result));
exception = state->exception;
}
if (exception)
std::rethrow_exception(exception);
return std::move(*result);
}
}
} // namespace detail
class PluginsDialog : public Slic3r::GUI::WebViewHostDialog
{
public:
@@ -111,53 +291,8 @@ private:
int style = wxPD_APP_MODAL | wxPD_AUTO_HIDE, // | wxPD_CAN_ABORT for cancel button
bool finish_after_dialog_destroyed = false)
{
const auto alive = m_alive;
ProgressDialog* progress = new ProgressDialog(title, message, maximum, this, style);
wxTimer* timer = new wxTimer();
timer->Bind(wxEVT_TIMER, [alive, progress, message](wxTimerEvent&) {
if (alive->load(std::memory_order_acquire) && progress)
progress->Pulse(message);
});
timer->Start(100);
std::thread([this,
alive,
progress,
timer,
run = std::forward<Run>(run),
on_finish = std::forward<OnFinish>(on_finish),
finish_after_dialog_destroyed]() mutable {
try {
run();
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(error) << "Plugin dialog worker failed: " << ex.what();
} catch (...) {
BOOST_LOG_TRIVIAL(error) << "Plugin dialog worker failed with an unknown exception";
}
if (wxTheApp == nullptr)
return;
wxTheApp->CallAfter([this,
alive,
progress,
timer,
on_finish = std::move(on_finish),
finish_after_dialog_destroyed]() mutable {
timer->Stop();
delete timer;
if (alive->load(std::memory_order_acquire)) {
progress->Destroy();
restore_z_order();
on_finish();
} else if (finish_after_dialog_destroyed) {
on_finish();
}
});
}).detach();
detail::run_off_thread_with_progress(std::forward<Run>(run), std::forward<OnFinish>(on_finish), this, title, message, maximum, style,
m_alive, finish_after_dialog_destroyed, [this] { restore_z_order(); });
}
template<typename Run>
@@ -167,83 +302,7 @@ private:
int maximum = 100,
int style = wxPD_APP_MODAL | wxPD_AUTO_HIDE)
{
using Result = std::invoke_result_t<std::decay_t<Run>&>;
bool finished = false;
wxEventLoop loop;
auto on_finish = [&finished, &loop]() {
finished = true;
if (loop.IsRunning())
loop.Exit();
};
if constexpr (std::is_void_v<Result>) {
struct WaitState
{
std::mutex mutex;
std::exception_ptr exception;
};
auto state = std::make_shared<WaitState>();
run_with_dialog(
[run = std::forward<Run>(run), state]() mutable {
try {
run();
} catch (...) {
std::lock_guard<std::mutex> lock(state->mutex);
state->exception = std::current_exception();
}
},
on_finish, title, message, maximum, style, true);
if (!finished)
loop.Run();
std::exception_ptr exception;
{
std::lock_guard<std::mutex> lock(state->mutex);
exception = state->exception;
}
if (exception)
std::rethrow_exception(exception);
} else {
using StoredResult = std::decay_t<Result>;
struct WaitState
{
std::mutex mutex;
std::optional<StoredResult> result;
std::exception_ptr exception;
};
auto state = std::make_shared<WaitState>();
run_with_dialog(
[run = std::forward<Run>(run), state]() mutable {
try {
StoredResult result = run();
std::lock_guard<std::mutex> lock(state->mutex);
state->result.emplace(std::move(result));
} catch (...) {
std::lock_guard<std::mutex> lock(state->mutex);
state->exception = std::current_exception();
}
},
on_finish, title, message, maximum, style, true);
if (!finished)
loop.Run();
std::optional<StoredResult> result;
std::exception_ptr exception;
{
std::lock_guard<std::mutex> lock(state->mutex);
if (state->result)
result.emplace(std::move(*state->result));
exception = state->exception;
}
if (exception)
std::rethrow_exception(exception);
return std::move(*result);
}
return detail::run_wait_with_progress(std::forward<Run>(run), this, title, message, maximum, style, m_alive, [this] { restore_z_order(); });
}
std::function<void()> m_open_terminal_dlg_fn;
+76 -2
View File
@@ -9,6 +9,7 @@
#include "I18N.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Format/DRC.hpp"
#include "libslic3r/CAD/SketchEngine.hpp"
#include <wx/language.h>
#include "OG_CustomCtrl.hpp"
#include "wx/graphics.h"
@@ -367,7 +368,8 @@ wxBoxSizer *PreferencesDialog::create_item_language_combobox(wxString title, wxS
wxLANGUAGE_PORTUGUESE_BRAZILIAN,
wxLANGUAGE_LITHUANIAN,
wxLANGUAGE_VIETNAMESE,
wxLANGUAGE_THAI
wxLANGUAGE_THAI,
wxLANGUAGE_ROMANIAN
};
auto translations = wxTranslations::Get()->GetAvailableTranslations(SLIC3R_APP_KEY);
@@ -1003,6 +1005,7 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too
checkbox->SetToolTip(tip);
if (param == "sync_user_preset") { m_sync_user_preset_checkbox = checkbox; }
if (param == SETTING_OPENGL_SKIP_IDENTICAL_FRAMES) { m_skip_identical_frames_checkbox = checkbox; }
m_sizer->Add(checkbox, 0, wxALIGN_CENTER);
@@ -1029,6 +1032,9 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " sync_user_preset: " << (sync ? "true" : "false");
}
else if (param == SETTING_OPENGL_SCENE_CACHE) {
if (m_skip_identical_frames_checkbox) m_skip_identical_frames_checkbox->Enable(checkbox->GetValue());
}
else if (param == "stealth_mode") {
bool enabled = app_config->get_stealth_mode();
if (enabled) wxGetApp().on_stealth_mode_enter();
@@ -1724,6 +1730,36 @@ void PreferencesDialog::create_items()
auto item_multi_machine = create_item_checkbox(_L("Multi device management"), _L("With this option enabled, you can send a task to multiple devices at the same time and manage multiple devices."), "enable_multi_machine", _L("(Requires restart)"));
g_sizer->Add(item_multi_machine);
auto item_speed_dial = create_item_checkbox(_L("Open the Speed Dial with the Space key"),
_L("When enabled, pressing Space (with no other key held) opens the Speed Dial action search from any page."),
"enable_speed_dial");
g_sizer->Add(item_speed_dial);
auto item_speed_dial_recents = create_item_spinctrl(
_L("Recent actions"),
"",
_L("actions"),
_L("How many recently launched actions to show at the top of the Speed Dial. Set to 0 to hide recent actions."),
SETTING_SPEED_DIAL_RECENT_COUNT,
SPEED_DIAL_RECENT_COUNT_MIN,
SPEED_DIAL_RECENT_COUNT_MAX);
g_sizer->Add(item_speed_dial_recents);
#ifdef SLIC3R_CAD
auto item_cad_feature = create_item_checkbox(_L("CAD feature (experimental)"),
_L("With this option enabled, the Design tab is shown, where models can be built and edited "
"parametrically. This feature is experimental and still under development."),
"enable_cad_feature", _L("(Requires restart)"));
g_sizer->Add(item_cad_feature);
auto item_auto_close_sketch_loops = create_item_checkbox(_L("Auto-close sketch loops"),
_L("Treat sketch endpoints within 0.001 mm as one joint and weld the loop shut. "
"Off: only exactly coincident endpoints join, so a loop with a tiny gap is "
"shown as open instead of being closed for you."),
"auto_close_sketch_loops");
g_sizer->Add(item_auto_close_sketch_loops);
#endif
#if 0
g_sizer->Add(create_item_title(_L("Filament Grouping")), 1, wxEXPAND);
//temporarily disable it
@@ -1800,6 +1836,21 @@ void PreferencesDialog::create_items()
auto reverse_mouse_zoom = create_item_checkbox(_L("Reverse mouse zoom"), _L("If enabled, reverses the direction of zoom with mouse wheel."), "reverse_mouse_wheel_zoom");
g_sizer->Add(reverse_mouse_zoom);
#ifdef SLIC3R_CAD
// Design-tab only, so it stays out of the way while the CAD feature is switched off.
if (wxGetApp().is_enable_cad_feature()) {
auto item_connector_face_glyph = create_item_checkbox(_L("Draw mate connectors as a face"),
_L("In the Design tab, draw a mate connector as a small face instead of the conventional "
"disc with a roll quadrant. A face's orientation is read without being learned. "
"Turn this off for the conventional CAD representation."), "design_connector_face_glyph");
g_sizer->Add(item_connector_face_glyph);
}
// Push the weld preference into the kernel now so toggling it takes effect without
// a restart (the sketch tool also re-pushes on activation, see DesignSketchTool::begin).
Slic3r::set_sketch_auto_close(wxGetApp().is_auto_close_sketch_loops());
#endif
std::vector<wxString> ButtonDragActions = {_L("None"), _L("Pan"), _L("Rotate")};
auto item_left_mouse_drag = create_item_combobox(_L("Left Mouse Drag"), _L("Set the action that dragging the left mouse button should perform."), "left_mouse_drag_action", ButtonDragActions);
g_sizer->Add(item_left_mouse_drag);
@@ -1912,9 +1963,32 @@ void PreferencesDialog::create_items()
);
g_sizer->Add(item_fps_cap);
auto item_scene_cache = create_item_checkbox(
_L("Reuse the 3D scene while idle"),
_L("Skips redrawing the 3D scene when only the mouse cursor moves over the viewport,\n"
"and reuses the previous frame's scene instead. Reduces GPU load.\n"
"Disable it if the viewport shows stale or missing contents.\n\n"
"Takes effect immediately."),
SETTING_OPENGL_SCENE_CACHE
);
g_sizer->Add(item_scene_cache);
auto item_skip_identical_frames = create_item_checkbox(
_L("Skip unchanged frames"),
_L("Skips drawing a frame altogether when it would be identical to the one already on screen.\n"
"Only applies to frames that reuse the 3D scene, so it needs Reuse the 3D scene while idle.\n"
"Disable it if a hover highlight, tooltip or animation stops updating.\n\n"
"Takes effect immediately."),
SETTING_OPENGL_SKIP_IDENTICAL_FRAMES
);
g_sizer->Add(item_skip_identical_frames);
if (m_skip_identical_frames_checkbox) m_skip_identical_frames_checkbox->Enable(app_config->get_bool(SETTING_OPENGL_SCENE_CACHE));
auto item_fps_overlay = create_item_checkbox(
_L("Show FPS overlay"),
_L("Displays current viewport FPS in the top-right corner."),
_L("Displays rendering counts in the top-right corner of the viewport.") + "\n" +
_L("FPS: frames presented to the screen per second.") + "\n" +
_L("3D: frames per second that redrew the 3D scene."),
SETTING_OPENGL_SHOW_FPS_OVERLAY
);
g_sizer->Add(item_fps_overlay);
+1
View File
@@ -71,6 +71,7 @@ public:
::CheckBox * m_dark_mode_ckeckbox = {nullptr};
::CheckBox * m_sync_user_preset_checkbox = {nullptr};
::CheckBox * m_bambu_cloud_checkbox = {nullptr};
::CheckBox * m_skip_identical_frames_checkbox = {nullptr};
::TextInput *m_backup_interval_textinput = {nullptr};
::SpinInput *m_dim_previous_layers_brightness_input = {nullptr};
::ComboBox * m_network_version_combo = {nullptr};
+172 -79
View File
@@ -8,6 +8,8 @@
#include "ConfigValueFormatter.hpp"
#include "FilamentBitmapUtils.hpp"
#include "Widgets/Label.hpp"
#include "Widgets/CheckBox.hpp"
#include "Widgets/HyperLink.hpp"
#include "Widgets/TextInput.hpp"
#include "Widgets/DialogButtons.hpp"
#include "Widgets/StaticLine.hpp"
@@ -26,6 +28,7 @@
#include <wx/dcmemory.h>
#include <wx/dcgraph.h>
#include <wx/image.h>
#include <wx/wrapsizer.h>
#include <set>
#include <algorithm>
#include <cmath>
@@ -38,6 +41,48 @@
namespace Slic3r { namespace GUI {
namespace {
// Orca's bitmap checkbox has the established teal checked state on every platform. Keep the
// label separate so it stays clickable like a native wxCheckBox, while the control itself
// remains accessible by keyboard.
wxStaticText* add_checkbox_label(wxWindow* parent,
wxBoxSizer* sizer,
::CheckBox* check,
const wxString& label,
const wxString& tooltip,
int label_width = 0)
{
check->SetToolTip(tooltip);
sizer->Add(check, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, parent->FromDIP(2));
auto* text = new wxStaticText(parent, wxID_ANY, label);
text->SetFont(Label::Body_14);
text->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636")));
if (label_width > 0) {
text->SetMinSize(wxSize(label_width, -1));
text->SetMaxSize(wxSize(label_width, -1));
text->Wrap(label_width);
}
text->SetToolTip(tooltip);
text->SetCursor(wxCURSOR_HAND);
const auto toggle = [check]() {
if (!check->IsEnabled())
return;
check->SetValue(!check->GetValue());
wxCommandEvent event(wxEVT_TOGGLEBUTTON, check->GetId());
event.SetEventObject(check);
check->GetEventHandler()->ProcessEvent(event);
};
text->Bind(wxEVT_LEFT_DOWN, [toggle](wxMouseEvent& event) {
if (!event.LeftDClick())
toggle();
});
text->Bind(wxEVT_LEFT_DCLICK, [toggle](wxMouseEvent&) {
toggle();
});
sizer->Add(text, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, parent->FromDIP(5));
return text;
}
// Menu ids for show_menu(): dedicated range so the popup cannot collide with application-level
// bindings (e.g. MainFrame's recent-files wxID_FILE1.. range).
enum {
@@ -416,12 +461,54 @@ std::set<size_t> project_used_filament_slots(const PresetBundle& bundle, const D
return used;
}
// Lays a translated sentence out along `row`, replacing each "%1%"-style placeholder with the
// matching window from `chips`. Keeping the sentence in one msgid lets a translation put the
// placeholders wherever its own grammar needs them; spacing comes from the translation itself.
void add_sentence_with_chips(wxWindow* parent, wxSizer* row, const wxString& sentence, const std::vector<wxWindow*>& chips)
{
std::vector<bool> placed(chips.size(), false);
auto add_text = [&](wxString text) {
text.Replace("%%", "%"); // the sentence is a format string
if (text.IsEmpty())
return;
auto* label = new wxStaticText(parent, wxID_ANY, text);
label->SetFont(Label::Body_12);
label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B")));
row->Add(label, 0, wxALIGN_CENTER_VERTICAL);
};
auto add_chip = [&](size_t i) {
if (i < chips.size() && chips[i] != nullptr && !placed[i]) {
placed[i] = true;
row->Add(chips[i], 0, wxALIGN_CENTER_VERTICAL);
}
};
size_t literal = 0, pos = 0;
while ((pos = sentence.find('%', pos)) != wxString::npos) {
size_t end = pos + 1;
while (end < sentence.length() && sentence[end] >= '0' && sentence[end] <= '9')
++end;
if (end == pos + 1 || end >= sentence.length() || sentence[end] != '%') {
++pos; // a bare '%'
continue;
}
long index = 0;
sentence.Mid(pos + 1, end - pos - 1).ToLong(&index);
add_text(sentence.Mid(literal, pos - literal));
add_chip(size_t(index - 1));
literal = pos = end + 1;
}
add_text(sentence.Mid(literal));
for (size_t i = 0; i < chips.size(); ++i) // whatever the translation left out
add_chip(i);
}
} // namespace
// Warning shown on OK when an enabled mixed-filament slot relies on a filament that would ship
// without its material. One row per unmet dependency: the mixed slot's colour chip, the
// component filament's colour chip, and the reason. "Cancel" is the safe choice and keeps the
// dialog open; "Publish anyway" continues.
// without its material. One row per unmet dependency, each a single translated sentence whose
// two placeholders are the mixed slot's and the component filament's colour chips. "Cancel" is
// the safe choice and keeps the dialog open; "Publish anyway" continues.
class MixedFilamentWarningDialog : public MsgDialog
{
public:
@@ -437,47 +524,37 @@ public:
content->AddSpacer(FromDIP(10));
const int swatch = FromDIP(20);
for (const MixedDependencyIssue& issue : issues) {
auto* row = new wxBoxSizer(wxHORIZONTAL);
// The mixed slot as just its own chip (gradient-aware, numbered like its tab);
// falls back to a plain label when the chip cannot be built.
const wxString mix_label = wxString::Format(_L("Filament %d (mixed)"), int(issue.mixed_slot) + 1);
const wxBitmap mix_bmp = mixed_filament_chip_bitmap(full, issue.mixed_slot, swatch);
if (mix_bmp.IsOk()) {
auto* bmp = new wxStaticBitmap(this, wxID_ANY, mix_bmp);
bmp->SetToolTip(mix_label);
row->Add(bmp, 0, wxALIGN_CENTER_VERTICAL);
} else {
auto* label = new wxStaticText(this, wxID_ANY, mix_label);
label->SetFont(Label::Body_12);
row->Add(label, 0, wxALIGN_CENTER_VERTICAL);
// The slot's colour swatch, numbered like its tab, with the slot name on hover; falls
// back to a label so the sentence always names both filaments.
auto make_chip = [&](const wxBitmap& bmp, const wxString& name) -> wxWindow* {
if (bmp.IsOk()) {
auto* chip = new wxStaticBitmap(this, wxID_ANY, bmp);
chip->SetToolTip(name);
return chip;
}
auto* label = new wxStaticText(this, wxID_ANY, name);
label->SetFont(Label::Body_12);
return label;
};
auto* needs = new wxStaticText(this, wxID_ANY, _L("needs"));
needs->SetFont(Label::Body_12);
needs->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B")));
row->Add(needs, 0, wxALIGN_CENTER_VERTICAL | wxLEFT | wxRIGHT, FromDIP(6));
// The component filament's colour chip, numbered like the tab strips; the slot
// name stays on hover to keep the row itself short.
for (const MixedDependencyIssue& issue : issues) {
std::string hex = filament_color_hex(full, issue.component_slot);
if (hex.empty())
hex = "#D9D9D9";
if (wxBitmap* chip = get_extruder_color_icon(hex, std::to_string(issue.component_slot + 1), swatch, swatch)) {
auto* comp_bmp = new wxStaticBitmap(this, wxID_ANY, *chip);
comp_bmp->SetToolTip(wxString::Format(_L("Filament %d"), int(issue.component_slot) + 1));
row->Add(comp_bmp, 0, wxALIGN_CENTER_VERTICAL);
}
const wxBitmap* comp_bmp = get_extruder_color_icon(hex, std::to_string(issue.component_slot + 1), swatch, swatch);
auto* reason = new wxStaticText(this, wxID_ANY,
issue.reason == MixedDependencyIssue::Reason::Disabled ? _L("not enabled") :
_L("material not published"));
reason->SetFont(Label::Body_12);
reason->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#989898")));
row->Add(reason, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8));
wxWindow* mix_chip = make_chip(mixed_filament_chip_bitmap(full, issue.mixed_slot, swatch),
wxString::Format(_L("Filament %d (mixed)"), int(issue.mixed_slot) + 1));
wxWindow* comp_chip = make_chip(comp_bmp != nullptr ? *comp_bmp : wxNullBitmap,
wxString::Format(_L("Filament %d"), int(issue.component_slot) + 1));
content->Add(row, 0, wxLEFT, FromDIP(10));
auto* row = new wxWrapSizer(wxHORIZONTAL);
add_sentence_with_chips(this, row,
issue.reason == MixedDependencyIssue::Reason::Disabled ?
_L("%1% needs %2%, which is not enabled.") :
_L("%1% needs %2%, whose material will not be published."),
{mix_chip, comp_chip});
content->Add(row, 0, wxEXPAND | wxLEFT, FromDIP(10));
content->AddSpacer(FromDIP(6));
}
@@ -616,6 +693,9 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent,
m_outer_tabs->SetBackgroundColour(GetBackgroundColour());
m_outer_host = new wxPanel(this, wxID_ANY);
#ifdef __WINDOWS__
m_outer_host->SetDoubleBuffered(true);
#endif
m_outer_host->SetBackgroundColour(GetBackgroundColour());
m_outer_host_sizer = new wxBoxSizer(wxVERTICAL);
m_outer_host->SetSizer(m_outer_host_sizer);
@@ -655,24 +735,22 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent,
dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); });
// Guide links, bottom-left, sharing the footer row with the OK/Cancel buttons (pushed right).
auto make_link = [this](const wxString& label, const char* url) {
wxStaticText* link = new wxStaticText(this, wxID_ANY, label);
link->SetFont(Label::Body_13);
link->SetForegroundColour(wxColour(0x1F, 0x8E, 0xEA));
link->SetCursor(wxCURSOR_HAND);
link->Bind(wxEVT_LEFT_DOWN, [url](wxMouseEvent&) { wxLaunchDefaultBrowser(url, wxBROWSER_NEW_WINDOW); });
return link;
};
wxBoxSizer* links_sizer = new wxBoxSizer(wxVERTICAL);
links_sizer->Add(make_link(_L("Publish 3MF Wiki"), "https://www.orcaslicer.com/wiki/publishing_3mf/publish_3mf.html"), 0, wxALIGN_LEFT);
links_sizer->Add(make_link(_L("Publish 3MF Video Guide"), "https://www.youtube.com/watch?v=-xt1N29UIOg"), 0,
wxTOP | wxALIGN_LEFT, FromDIP(4));
auto* wiki_link = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/publishing_3mf/publish_3mf.html");
auto* video_link = new HyperLink(this, _L("Video Guide"), "https://www.youtube.com/watch?v=-xt1N29UIOg");
links_sizer->Add(wiki_link , 0, wxALIGN_LEFT);
links_sizer->Add(video_link, 0, wxTOP | wxALIGN_LEFT, FromDIP(4));
wxBoxSizer* footer = new wxBoxSizer(wxHORIZONTAL);
footer->Add(links_sizer, 0, wxALIGN_CENTER_VERTICAL);
footer->AddStretchSpacer();
footer->Add(dlg_btns, 0, wxALIGN_CENTER_VERTICAL);
w_sizer->Add(footer, 0, wxRIGHT | wxLEFT | wxBOTTOM | wxEXPAND, FromDIP(10));
auto* footer_line = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxSize(-1, 1));
footer_line->SetBackgroundColour(wxColour("#CECECE"));
footer_line->SetMinSize(wxSize(-1, 1));
footer_line->SetMaxSize(wxSize(-1, 1));
w_sizer->Add(footer_line, 0, wxRIGHT | wxLEFT | wxTOP | wxEXPAND, FromDIP(10));
w_sizer->Add(footer, 0, wxRIGHT | wxLEFT | wxTOP | wxBOTTOM | wxEXPAND, FromDIP(10));
SetSizerAndFit(w_sizer);
fit_to_content(); // initial size only; the dialog is resizable
@@ -685,14 +763,14 @@ PublishSettingsDialog::PublishSettingsDialog(wxWindow* parent,
// Size the window to its content: width follows the widest tab strip so no filament tab is
// hidden (TabCtrl::relayout hides overflowing buttons), height scales proportionally. Both
// are floored at the 600x500 base and capped at hard DIP limits - deliberately not the whole
// are floored at the 530x530 base and capped at hard DIP limits - deliberately not the whole
// display - with one last-resort clamp so the dialog can never open larger than the screen.
// Also owns the resize floor: the window cannot be resized below what the tabs need, so
// shrinking never re-hides a filament tab.
void PublishSettingsDialog::fit_to_content()
{
static const wxSize BASE{600, 500};
static const wxSize CAP{1300, 850};
static const wxSize BASE{530, 530}; // base size in DIP, the minimum the dialog can shrink to
static const wxSize CAP{1300, 850}; // hard cap in DIP, the maximum the dialog can grow to
int strip = m_outer_tabs->GetFullSize();
for (const SectionGroup& section : m_sections) {
@@ -760,6 +838,8 @@ void PublishSettingsDialog::build_option_model()
return false;
value = get_string_value(opt_id, full);
unit = _(def->sidetext);
if (unit == "%" && value.EndsWith("%"))
unit.clear();
return true;
};
@@ -972,13 +1052,19 @@ void PublishSettingsDialog::build_option_model()
// stays valid even if the vector is reallocated later.
for (size_t c = 0; c < m_categories.size(); ++c)
if (m_categories[c].enable_check != nullptr)
m_categories[c].enable_check->Bind(wxEVT_CHECKBOX, [this, c](wxCommandEvent&) { on_enable_toggle(c); });
m_categories[c].enable_check->Bind(wxEVT_TOGGLEBUTTON, [this, c](wxCommandEvent& event) {
on_enable_toggle(c);
event.Skip();
});
// Wire the "Full Publish" checkboxes (physical slots): toggling one disables/enables the
// material's rows.
for (size_t c = 0; c < m_categories.size(); ++c)
if (m_categories[c].full_check != nullptr)
m_categories[c].full_check->Bind(wxEVT_CHECKBOX, [this, c](wxCommandEvent&) { on_full_toggle(c); });
m_categories[c].full_check->Bind(wxEVT_TOGGLEBUTTON, [this, c](wxCommandEvent& event) {
on_full_toggle(c);
event.Skip();
});
// No filter is active at startup: every row matches until the user types.
for (Row& row : m_rows)
@@ -1041,6 +1127,9 @@ size_t PublishSettingsDialog::section_group_for(Section kind)
section.mixed_tabs->Hide();
}
section.page_host = new wxPanel(section.page, wxID_ANY);
#ifdef __WINDOWS__
section.page_host->SetDoubleBuffered(true);
#endif
section.page_host->SetBackgroundColour(GetBackgroundColour());
section.page_host_sizer = new wxBoxSizer(wxVERTICAL);
section.page_host->SetSizer(section.page_host_sizer);
@@ -1103,10 +1192,9 @@ size_t PublishSettingsDialog::category_index_for(
if (is_mixed) {
// No chip/title: the lone "Enable" checkbox tops the page.
auto* enable_sizer = new wxBoxSizer(wxHORIZONTAL);
category.enable_check = new wxCheckBox(category.page, wxID_ANY, _L("Enable"));
category.enable_check->SetFont(Label::Body_13);
category.enable_check->SetToolTip(_L("Publish this mixed filament and enable + Full Publish its component filaments"));
enable_sizer->Add(category.enable_check, 0, wxALIGN_CENTER_VERTICAL);
category.enable_check = new ::CheckBox(category.page, wxID_ANY);
category.enable_label = add_checkbox_label(category.page, enable_sizer, category.enable_check, _L("Enable"),
_L("Publish this mixed filament and enable + Full Publish its component filaments"));
page_sizer->Add(enable_sizer, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, FromDIP(6));
} else {
// Line 1: [chip] [title] [Enable]. The Enable checkbox gates the whole slot: while
@@ -1119,19 +1207,19 @@ size_t PublishSettingsDialog::category_index_for(
category.title_label = new wxStaticText(category.page, wxID_ANY, title);
category.title_label->SetFont(Label::Head_14);
header_sizer->Add(category.title_label, 0, wxALIGN_CENTER_VERTICAL);
category.enable_check = new wxCheckBox(category.page, wxID_ANY, _L("Enable"));
category.enable_check->SetFont(Label::Body_13);
category.enable_check->SetToolTip(_L("Publish this filament slot in the 3MF file"));
header_sizer->Add(category.enable_check, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
auto* enable_sizer = new wxBoxSizer(wxHORIZONTAL);
category.enable_check = new ::CheckBox(category.page, wxID_ANY);
category.enable_label = add_checkbox_label(category.page, enable_sizer, category.enable_check, _L("Enable"),
_L("Publish this filament slot in the 3MF file"));
header_sizer->Add(enable_sizer, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(10));
page_sizer->Add(header_sizer, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, FromDIP(6));
// Line 2: the "Full Publish" toggle, on its own line below the title (hidden until
// the slot is enabled), aligned with the colour chip above it.
auto* full_sizer = new wxBoxSizer(wxHORIZONTAL);
category.full_check = new wxCheckBox(category.page, wxID_ANY, _L("Full Publish"));
category.full_check->SetFont(Label::Body_13);
category.full_check->SetToolTip(_L("Embed the entire filament of this slot in the 3MF file"));
full_sizer->Add(category.full_check, 0, wxALIGN_CENTER_VERTICAL);
category.full_check = new ::CheckBox(category.page, wxID_ANY);
category.full_label = add_checkbox_label(category.page, full_sizer, category.full_check, _L("Full Publish"),
_L("Embed the entire filament of this slot in the 3MF file"));
category.full_line_item = page_sizer->Add(full_sizer, 0, wxEXPAND | wxTOP | wxLEFT | wxRIGHT, FromDIP(6));
}
}
@@ -1147,7 +1235,7 @@ size_t PublishSettingsDialog::category_index_for(
category.info->SetFont(Label::Body_13);
category.list_sizer->Add(category.info, 1, wxALIGN_CENTER_HORIZONTAL | wxALL, FromDIP(10));
category.info->Hide();
page_sizer->Add(category.scroll, 1, wxEXPAND | wxALL, FromDIP(4));
page_sizer->Add(category.scroll, 1, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(4));
// A material slot starts disabled: its rows (and its Full Publish line) stay hidden until
// "Enable" is checked.
if (section == Section::Material)
@@ -1208,7 +1296,7 @@ size_t PublishSettingsDialog::subcategory_index_for(size_t category_index, const
sub.header->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636")));
auto* wrap = new wxBoxSizer(wxVERTICAL);
wrap->Add(sub.header, 0, wxEXPAND | wxTOP | wxBOTTOM, FromDIP(6));
sub.item = category.list_sizer->Add(wrap, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(22));
sub.item = category.list_sizer->Add(wrap, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(5));
}
category.subs.push_back(std::move(sub));
return category.subs.size() - 1;
@@ -1238,15 +1326,18 @@ void PublishSettingsDialog::add_row_ui(const std::string& key,
const size_t row_index = m_rows.size();
m_rows.push_back(std::move(row));
Row& current = m_rows[row_index];
current.check = new wxCheckBox(category.scroll, wxID_ANY, label);
current.check->SetFont(Label::Body_13);
current.check->Bind(wxEVT_CHECKBOX, [this](wxCommandEvent&) { refresh_tab_indicators(); });
current.check = new ::CheckBox(category.scroll, wxID_ANY);
current.check->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& event) {
refresh_tab_indicators();
event.Skip();
});
auto* row_sizer = new wxBoxSizer(wxHORIZONTAL);
row_sizer->Add(current.check, 0, wxALIGN_CENTER_VERTICAL);
current.check_label = add_checkbox_label(category.scroll, row_sizer, current.check, label + ":", wxEmptyString,
24 * wxGetApp().em_unit());
// The value is read-only text (incl. the Type row: the published type is the slot's
// normalized type, not author-editable).
current.value_label = new wxStaticText(category.scroll, wxID_ANY, value, wxDefaultPosition, wxDefaultSize, wxST_ELLIPSIZE_END);
current.value_label->SetFont(Label::Body_13);
current.value_label->SetFont(Label::Body_14);
current.value_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
current.value_label->SetToolTip(unit.IsEmpty() ? value : value + " " + unit);
if (kind == RowKind::Color && !value.IsEmpty()) {
@@ -1257,14 +1348,14 @@ void PublishSettingsDialog::add_row_ui(const std::string& key,
row_sizer->Add(current.color_chip, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8));
}
}
row_sizer->Add(current.value_label, 1, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8));
row_sizer->Add(current.value_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(8));
if (!unit.IsEmpty()) {
current.unit_label = new wxStaticText(category.scroll, wxID_ANY, unit);
current.unit_label->SetFont(Label::Body_13);
current.unit_label->SetFont(Label::Body_14);
current.unit_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#6B6B6B")));
row_sizer->Add(current.unit_label, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(4));
}
current.item = category.list_sizer->Add(row_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(38));
current.item = category.list_sizer->Add(row_sizer, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(5));
category.rows.push_back(row_index);
category.subs[subcategory_index].rows.push_back(row_index);
}
@@ -1273,8 +1364,10 @@ void PublishSettingsDialog::on_full_toggle(size_t category_index)
{
Category& cat = m_categories[category_index];
const bool full = cat.full_check->GetValue();
for (size_t r : cat.rows)
for (size_t r : cat.rows) {
m_rows[r].check->Enable(!full);
m_rows[r].check_label->Enable(!full);
}
refresh_tab_indicators();
}
@@ -1468,7 +1561,7 @@ void PublishSettingsDialog::add_mixed_visual(size_t category_index, const MixedV
void PublishSettingsDialog::set_row_bold(Row& row, bool bold)
{
// Rebase on the dialog's body font so clearing bold restores the exact original font.
row.check->SetFont(bold ? Label::Body_13.Bold() : Label::Body_13);
row.check_label->SetFont(bold ? Label::Body_13.Bold() : Label::Body_13);
}
void PublishSettingsDialog::save_scroll_position(Category& category)
@@ -1716,7 +1809,7 @@ void PublishSettingsDialog::select_all(bool value)
for (Category& cat : m_categories)
if (cat.section == Section::Material && cat.enable_check != nullptr)
cat.enable_check->SetValue(value);
// wxCheckBox::SetValue does not emit wxEVT_CHECKBOX, so re-run the enable handlers to
// CheckBox::SetValue does not emit wxEVT_TOGGLEBUTTON, so re-run the enable handlers to
// propagate mixed-slot components and refresh visibility as if the user had clicked.
for (size_t c = 0; c < m_categories.size(); ++c)
if (m_categories[c].section == Section::Material)
+7 -3
View File
@@ -18,6 +18,7 @@
// Widgets/StaticLine.hpp).
class TextInput;
class StaticLine;
class CheckBox;
namespace Slic3r { namespace GUI {
@@ -97,7 +98,8 @@ private:
size_t inner_index{0};
bool dirty{false}; // matches a dirty base key: pre-checked + bold
bool matches_filter{false}; // survives the active filter (computed by apply_filter)
wxCheckBox* check{nullptr};
::CheckBox* check{nullptr};
wxStaticText* check_label{nullptr};
wxStaticText* value_label{nullptr};
wxStaticText* unit_label{nullptr};
wxStaticBitmap* color_chip{nullptr}; // Color rows only; swatch next to the value
@@ -131,11 +133,13 @@ private:
// header row is hidden. For physical slots the Full Publish toggle sits on a second
// line (full_line_item) visible only when enabled; for mixed slots Enable alone implies
// publishing the mix definition, so no Full Publish widget exists at all.
wxCheckBox* enable_check{nullptr};
::CheckBox* enable_check{nullptr};
wxStaticText* enable_label{nullptr};
wxSizerItem* full_line_item{nullptr}; // sizer item of the Full Publish line (physical slots only)
// "Full Publish": while checked, the whole slot preset is serialized and its rows
// (incl. Color/Type) are disabled.
wxCheckBox* full_check{nullptr};
::CheckBox* full_check{nullptr};
wxStaticText* full_label{nullptr};
// True for a mixed-color filament slot: no Material/Retraction rows; Enable publishes
// the slot's gradient/ratio definition as a whole.
bool is_mixed{false};
+62
View File
@@ -0,0 +1,62 @@
#include "libslic3r/libslic3r.h"
#include "SceneCache.hpp"
#include "3DScene.hpp"
#include "GLModel.hpp"
#include "GLShader.hpp"
#include "GLTexture.hpp"
#include "GUI_App.hpp"
#include <glad/gl.h>
namespace Slic3r {
namespace GUI {
void SceneCache::capture(Key key)
{
m_valid = false;
if (wxGetApp().get_shader("flat_texture") == nullptr)
return;
GLTexture::copy_from_framebuffer(m_texture_id, m_texture_size, key.size[0], key.size[1], GL_NEAREST);
m_key = std::move(key);
m_valid = true;
}
void SceneCache::render(GLModel& quad)
{
GLShaderProgram* shader = wxGetApp().get_shader("flat_texture");
glsafe(::glDisable(GL_DEPTH_TEST));
glsafe(::glDisable(GL_BLEND));
shader->start_using();
shader->set_uniform("view_model_matrix", Transform3d::Identity());
shader->set_uniform("projection_matrix", Transform3d::Identity());
shader->set_uniform("uniform_texture", 0);
glsafe(::glActiveTexture(GL_TEXTURE0));
glsafe(::glBindTexture(GL_TEXTURE_2D, m_texture_id));
quad.render();
glsafe(::glBindTexture(GL_TEXTURE_2D, 0));
shader->stop_using();
glsafe(::glEnable(GL_DEPTH_TEST));
glsafe(::glEnable(GL_BLEND));
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
}
void SceneCache::reset()
{
m_valid = false;
if (m_texture_id != 0) {
glsafe(::glDeleteTextures(1, &m_texture_id));
m_texture_id = 0;
}
m_texture_size = { { 0, 0 } };
}
} // namespace GUI
} // namespace Slic3r
+56
View File
@@ -0,0 +1,56 @@
#pragma once
#include "libslic3r/Point.hpp"
#include <array>
#include <vector>
namespace Slic3r {
namespace GUI {
class GLModel;
// The last 3D scene pass, kept as a texture for frames that only rebuild the overlay.
class SceneCache
{
public:
// Scene pass inputs that change without any frame request.
struct Key
{
std::array<unsigned int, 2> size{ { 0, 0 } };
Transform3d view_matrix{ Transform3d::Identity() };
Transform3d projection_matrix{ Transform3d::Identity() };
// Hovered volumes that draw their sinking contour, hovered plate icons, hovered gizmo grabber.
std::vector<int> sinking_hover_volume_idxs;
std::vector<int> hover_plate_icon_idxs;
int gizmo_hover_id{ -1 };
bool render_preview{ true };
bool operator == (const Key& other) const {
return size == other.size && render_preview == other.render_preview &&
gizmo_hover_id == other.gizmo_hover_id &&
sinking_hover_volume_idxs == other.sinking_hover_volume_idxs &&
hover_plate_icon_idxs == other.hover_plate_icon_idxs &&
view_matrix.isApprox(other.view_matrix) &&
projection_matrix.isApprox(other.projection_matrix);
}
};
// Copies the bound read framebuffer, sized by key.size, and remembers key.
void capture(Key key);
bool matches(const Key& key) const { return m_valid && m_key == key; }
// Draws the last capture over the whole viewport, on the given full screen quad.
void render(GLModel& quad);
void invalidate() { m_valid = false; }
// Frees the texture.
void reset();
private:
unsigned int m_texture_id{ 0 };
std::array<unsigned int, 2> m_texture_size{ { 0, 0 } };
Key m_key;
bool m_valid{ false };
};
} // namespace GUI
} // namespace Slic3r
+6 -182
View File
@@ -62,94 +62,12 @@ static char marker_by_type(Preset::Type type, PrinterTechnology pt)
}
}
std::string Option::opt_key() const { return into_u8(key).substr(2); }
void FoundOption::get_marked_label_and_tooltip(const char **label_, const char **tooltip_) const
{
*label_ = marked_label.c_str();
*tooltip_ = tooltip.c_str();
}
template<class T>
// void change_opt_key(std::string& opt_key, DynamicPrintConfig* config)
void change_opt_key(std::string &opt_key, DynamicPrintConfig *config, int &cnt)
{
T *opt_cur = static_cast<T *>(config->option(opt_key));
cnt = opt_cur->values.size();
return;
if (opt_cur->values.size() > 0) opt_key += "#" + std::to_string(0);
}
static std::string get_key(const std::string &opt_key, Preset::Type type) { return std::to_string(int(type)) + ";" + opt_key; }
void OptionsSearcher::append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode)
{
auto emplace = [this, type](const std::string key, const wxString &label) {
const GroupAndCategory &gc = groups_and_categories[key];
if (gc.group.IsEmpty() || gc.category.IsEmpty()) return;
wxString suffix;
wxString suffix_local;
if (gc.category == "Machine limits") {
//suffix = key.back() == '1' ? L("Stealth") : L("Normal");
suffix = key.back() == '1' ? wxEmptyString : wxEmptyString;
suffix_local = " " + _(suffix);
suffix = " " + suffix;
}
if (!label.IsEmpty())
options.emplace_back(Option{boost::nowide::widen(key), type, (label + suffix).ToStdWstring(), (_(label) + suffix_local).ToStdWstring(), gc.group.ToStdWstring(),
_(gc.group).ToStdWstring(), gc.category.ToStdWstring(), GUI::Tab::translate_category(gc.category, type).ToStdWstring()});
};
for (std::string opt_key : config->keys()) {
const ConfigOptionDef &opt = config->def()->options.at(opt_key);
if (opt.mode > mode) continue;
int cnt = 0;
if ((type == Preset::TYPE_SLA_MATERIAL || type == Preset::TYPE_PRINTER || type == Preset::TYPE_PRINT) && opt_key != "printable_area")
switch (config->option(opt_key)->type()) {
case coInts: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break;
case coBools: change_opt_key<ConfigOptionBools>(opt_key, config, cnt); break;
case coFloats: change_opt_key<ConfigOptionFloats>(opt_key, config, cnt); break;
case coStrings: change_opt_key<ConfigOptionStrings>(opt_key, config, cnt); break;
case coPercents: change_opt_key<ConfigOptionPercents>(opt_key, config, cnt); break;
case coPoints: change_opt_key<ConfigOptionPoints>(opt_key, config, cnt); break;
// BBS
case coEnums: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break;
default: break;
}
if (type == Preset::TYPE_FILAMENT && filament_options_with_variant.find(opt_key) != filament_options_with_variant.end())
opt_key += "#0";
wxString label = opt.full_label.empty() ? opt.label : opt.full_label;
std::string key = get_key(opt_key, type);
if (cnt == 0)
emplace(key, label);
else
for (int i = 0; i < cnt; ++i)
// ! It's very important to use "#". opt_key#n is a real option key used in GroupAndCategory
emplace(key + "#" + std::to_string(i), label);
}
}
inline void OptionsSearcher::sort_options()
{
std::sort(options.begin(), options.end(), [](const Option &o1, const Option &o2) { return o1.label < o2.label; });
Option * last = nullptr;
for (auto& opt : options) {
if (last && last->label == opt.label && last->group == opt.group && last->type == opt.type && last->category != opt.category) {
last->multi_category = true;
opt.multi_category = true;
}
last = &opt;
}
}
// Mark a string using ColorMarkerStart and ColorMarkerEnd symbols
static std::wstring mark_string(const std::wstring &str, const std::vector<uint16_t> &matches, Preset::Type type, PrinterTechnology pt)
{
@@ -234,7 +152,8 @@ bool OptionsSearcher::search(const std::string &search, bool force /* = false*/,
return wxString(marker_by_type(opt.type, printer_technology)) + opt.category_local + sep + opt.group_local + sep + opt.label_local;
};
std::vector<uint16_t> matches, matches2;
std::vector<uint16_t> matches, matches2;
const std::vector<Option> &options = m_index.options();
for (size_t i = 0; i < options.size(); i++) {
const Option &opt = options[i];
if (full_list) {
@@ -306,111 +225,21 @@ OptionsSearcher::~OptionsSearcher() {}
void OptionsSearcher::init(std::vector<InputInfo> input_values)
{
options.clear();
for (auto i : input_values) append_options(i.config, i.type, i.mode);
sort_options();
m_index.init(std::move(input_values));
search(search_line, true, search_type);
}
void OptionsSearcher::apply(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode)
{
if (options.empty()) return;
options.erase(std::remove_if(options.begin(), options.end(), [type](Option opt) { return opt.type == type; }), options.end());
append_options(config, type, mode);
sort_options();
search(search_line, true, search_type);
if (m_index.apply(config, type, mode))
search(search_line, true, search_type);
}
const Option &OptionsSearcher::get_option(size_t pos_in_filter) const
{
assert(pos_in_filter != size_t(-1) && found[pos_in_filter].option_idx != size_t(-1));
return options[found[pos_in_filter].option_idx];
}
const Option &OptionsSearcher::get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const
{
std::string opt_key2 = opt_key;
if (auto n = opt_key.find('#'); n != std::string::npos) {
variant_index = std::atoi(opt_key.c_str() + n + 1);
opt_key2 = opt_key.substr(0, n);
}
auto it = std::lower_bound(options.begin(), options.end(), Option({boost::nowide::widen(get_key(opt_key2, type))}));
// BBS: return the 0th option when not found in searcher caused by mode difference
// assert(it != options.end());
if (it == options.end()) { variant_index = -2 ; return options[0]; }
if (it->opt_key() == opt_key2) {
variant_index = -1;
} else {
const std::string opt_key3 = opt_key2 + "#";
it = std::lower_bound(it, options.end(), Option({boost::nowide::widen(get_key(opt_key3, type))}));
if (it == options.end() || it->opt_key().compare(0, opt_key3.length(), opt_key3) != 0) {
variant_index = -2; // Not found
return options[0];
}
auto it2 = it;
++it2;
if (it2 != options.end() && it2->opt_key().compare(0, opt_key3.length(), opt_key3) == 0
&& printer_options_with_variant_1.find(opt_key2) == printer_options_with_variant_1.end())
variant_index = -2;
}
return options[it - options.begin()];
}
static Option create_option(const std::string &opt_key, const wxString &label, Preset::Type type, const GroupAndCategory &gc)
{
wxString suffix;
wxString suffix_local;
if (gc.category == "Machine limits") {
//suffix = opt_key.back() == '1' ? L("Stealth") : L("Normal");
suffix = opt_key.back() == '1' ? wxEmptyString : wxEmptyString;
suffix_local = " " + _(suffix);
suffix = " " + suffix;
}
wxString category = gc.category;
if (type == Preset::TYPE_PRINTER && category.Contains("Extruder ")) {
std::string opt_idx = opt_key.substr(opt_key.find("#") + 1);
category = wxString::Format("%s %d", "Extruder", atoi(opt_idx.c_str()) + 1);
}
return Option{boost::nowide::widen(get_key(opt_key, type)),
type,
(label + suffix).ToStdWstring(),
(_(label) + suffix_local).ToStdWstring(),
gc.group.ToStdWstring(),
_(gc.group).ToStdWstring(),
gc.category.ToStdWstring(),
GUI::Tab::translate_category(category, type).ToStdWstring()};
}
Option OptionsSearcher::get_option(const std::string &opt_key, const wxString &label, Preset::Type type) const
{
std::string key = get_key(opt_key, type);
auto it = std::lower_bound(options.begin(), options.end(), Option({boost::nowide::widen(key)}));
// BBS: return the 0th option when not found in searcher caused by mode difference
if (it == options.end()) return options[0];
if (it->key == boost::nowide::widen(key)) return options[it - options.begin()];
if (groups_and_categories.find(key) == groups_and_categories.end()) {
size_t pos = key.find('#');
if (pos == std::string::npos) return options[it - options.begin()];
std::string zero_opt_key = key.substr(0, pos + 1) + "0";
if (groups_and_categories.find(zero_opt_key) == groups_and_categories.end()) return options[it - options.begin()];
return create_option(opt_key, label, type, groups_and_categories.at(zero_opt_key));
}
const GroupAndCategory &gc = groups_and_categories.at(key);
if (gc.group.IsEmpty() || gc.category.IsEmpty()) return options[it - options.begin()];
return create_option(opt_key, label, type, gc);
return m_index.option_at(found[pos_in_filter].option_idx);
}
void OptionsSearcher::show_dialog(Preset::Type type, wxWindow *parent, TextInput *input, wxWindow* ssearch_btn)
@@ -437,11 +266,6 @@ void OptionsSearcher::dlg_msw_rescale()
{
if (search_dialog) search_dialog->msw_rescale();
}
void OptionsSearcher::add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category)
{
groups_and_categories[get_key(opt_key, type)] = GroupAndCategory{group, category};
}
//------------------------------------------
// SearchItem
//------------------------------------------
+10 -54
View File
@@ -19,6 +19,7 @@
#include "wxExtensions.hpp"
#include "GUI_Utils.hpp"
#include "libslic3r/Preset.hpp"
#include "SettingsIndex.hpp"
#include "Widgets/ScrolledWindow.hpp"
#include "Widgets/TextInput.hpp"
#include "Widgets/PopupWindow.hpp"
@@ -34,39 +35,6 @@ namespace Search {
class SearchDialog;
struct InputInfo
{
DynamicPrintConfig *config{nullptr};
Preset::Type type{Preset::TYPE_INVALID};
ConfigOptionMode mode{comSimple};
};
struct GroupAndCategory
{
wxString group;
wxString category;
};
struct Option
{
// bool operator<(const Option& other) const { return other.label > this->label; }
bool operator<(const Option &other) const { return other.key > this->key; }
// Fuzzy matching works at a character level. Thus matching with wide characters is a safer bet than with short characters,
// though for some languages (Chinese?) it may not work correctly.
std::wstring key;
Preset::Type type{Preset::TYPE_INVALID};
std::wstring label;
std::wstring label_local;
std::wstring group;
std::wstring group_local;
std::wstring category;
std::wstring category_local;
bool multi_category { false };
std::string opt_key() const;
};
struct FoundOption
{
// UTF8 encoding, to be consumed by ImGUI by reference.
@@ -90,25 +58,19 @@ struct OptionViewParameters
class OptionsSearcher
{
std::string search_line;
Preset::Type search_type = Preset::TYPE_INVALID;
SettingsIndex m_index;
std::map<std::string, GroupAndCategory> groups_and_categories;
PrinterTechnology printer_technology;
std::vector<Option> options{};
std::string search_line;
Preset::Type search_type = Preset::TYPE_INVALID;
PrinterTechnology printer_technology;
std::vector<FoundOption> found{};
void append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode);
void sort_options();
void sort_found()
{
std::sort(found.begin(), found.end(),
[](const FoundOption &f1, const FoundOption &f2) { return f1.outScore > f2.outScore || (f1.outScore == f2.outScore && f1.label < f2.label); });
};
size_t options_size() const { return options.size(); }
size_t found_size() const { return found.size(); }
public:
@@ -119,32 +81,26 @@ public:
OptionsSearcher();
~OptionsSearcher();
SettingsIndex & index() { return m_index; }
const SettingsIndex &index() const { return m_index; }
// Rebuild the catalog and re-run the current query so the cached results track it.
void init(std::vector<InputInfo> input_values);
void apply(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode);
bool search();
bool search(const std::string &search, bool force = false, Preset::Type type = Preset::TYPE_INVALID);
void add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category);
size_t size() const { return found_size(); }
const FoundOption &operator[](const size_t pos) const noexcept { return found[pos]; }
const Option & get_option(size_t pos_in_filter) const;
const Option & get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const;
Option get_option(const std::string &opt_key, const wxString &label, Preset::Type type) const;
const std::vector<FoundOption> &found_options() { return found; }
const GroupAndCategory & get_group_and_category(const std::string &opt_key) { return groups_and_categories[opt_key]; }
std::string & search_string() { return search_line; }
void set_printer_technology(PrinterTechnology pt) { printer_technology = pt; }
void sort_options_by_key()
{
std::sort(options.begin(), options.end(), [](const Option &o1, const Option &o2) { return o1.key < o2.key; });
}
void sort_options_by_label() { sort_options(); }
void show_dialog(Preset::Type type, wxWindow *parent, TextInput *input, wxWindow *ssearch_btn);
void dlg_sys_color_changed();
void dlg_msw_rescale();
+258
View File
@@ -0,0 +1,258 @@
#include "SettingsIndex.hpp"
#include <algorithm>
#include <cstddef>
#include <cstdlib>
#include <string>
#include <vector>
#include <boost/nowide/convert.hpp>
#include "GUI.hpp"
#include "I18N.hpp"
#include "Tab.hpp"
#include "libslic3r/PrintConfig.hpp"
namespace Slic3r {
using GUI::into_u8;
namespace Search {
static std::string get_key(const std::string &opt_key, Preset::Type type) { return std::to_string(int(type)) + ";" + opt_key; }
std::string Option::opt_key() const { return key.size() < 2 ? std::string() : into_u8(key).substr(2); }
template<class T>
// void change_opt_key(std::string& opt_key, DynamicPrintConfig* config)
void change_opt_key(std::string &opt_key, DynamicPrintConfig *config, int &cnt)
{
T *opt_cur = static_cast<T *>(config->option(opt_key));
cnt = opt_cur->values.size();
return;
if (opt_cur->values.size() > 0) opt_key += "#" + std::to_string(0);
}
// Single assembler for an indexed Option, shared by append_options() and create_option(), so a new
// Option field is only wired up in one place.
static Option make_option(const std::string &key, Preset::Type type, const wxString &label, const GroupAndCategory &gc,
ConfigOptionMode mode, const std::string &tooltip, bool rewrite_extruder_category)
{
wxString suffix;
wxString suffix_local;
if (gc.category == "Machine limits") {
//suffix = key.back() == '1' ? L("Stealth") : L("Normal");
suffix = key.back() == '1' ? wxEmptyString : wxEmptyString;
suffix_local = " " + _(suffix);
suffix = " " + suffix;
}
wxString category = gc.category;
if (rewrite_extruder_category && type == Preset::TYPE_PRINTER && category.Contains("Extruder ")) {
std::string opt_idx = key.substr(key.find("#") + 1);
category = wxString::Format("%s %d", "Extruder", atoi(opt_idx.c_str()) + 1);
}
Option option{boost::nowide::widen(key), type, (label + suffix).ToStdWstring(), (_(label) + suffix_local).ToStdWstring(),
gc.group.ToStdWstring(), _(gc.group).ToStdWstring(), into_u8(gc.icon), gc.category.ToStdWstring(),
GUI::Tab::translate_category(category, type).ToStdWstring(), false, mode, tooltip, gc.path,
// The settings page draws Line::label; carrying it lets the Speed Dial name a setting
// the way the page does. `label`/`label_local` stay the search-oriented name.
into_u8(gc.line_label)};
return option;
}
void SettingsIndex::append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode)
{
for (std::string opt_key : config->keys()) {
const ConfigOptionDef &opt = config->def()->options.at(opt_key);
const bool in_filtered = opt.mode <= mode;
int cnt = 0;
if ((type == Preset::TYPE_SLA_MATERIAL || type == Preset::TYPE_PRINTER || type == Preset::TYPE_PRINT) && opt_key != "printable_area")
switch (config->option(opt_key)->type()) {
case coInts: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break;
case coBools: change_opt_key<ConfigOptionBools>(opt_key, config, cnt); break;
case coFloats: change_opt_key<ConfigOptionFloats>(opt_key, config, cnt); break;
case coStrings: change_opt_key<ConfigOptionStrings>(opt_key, config, cnt); break;
case coPercents: change_opt_key<ConfigOptionPercents>(opt_key, config, cnt); break;
case coFloatsOrPercents: change_opt_key<ConfigOptionVector<FloatOrPercent>>(opt_key, config, cnt); break;
case coPoints: change_opt_key<ConfigOptionPoints>(opt_key, config, cnt); break;
// BBS
case coEnums: change_opt_key<ConfigOptionInts>(opt_key, config, cnt); break;
default: break;
}
if (type == Preset::TYPE_FILAMENT && filament_options_with_variant.find(opt_key) != filament_options_with_variant.end())
opt_key += "#0";
wxString label = opt.full_label.empty() ? opt.label : opt.full_label;
std::string key = get_key(opt_key, type);
auto add = [&](const std::string &k) {
const GroupAndCategory &gc = m_groups_and_categories[k];
if (gc.group.IsEmpty() || gc.category.IsEmpty() || label.IsEmpty()) return;
const std::string tooltip = into_u8(_(opt.tooltip));
if (in_filtered)
m_options.emplace_back(make_option(k, type, label, gc, opt.mode, tooltip, false));
m_all_modes.emplace_back(make_option(k, type, label, gc, opt.mode, tooltip, false));
};
if (cnt == 0)
add(key);
else
for (int i = 0; i < cnt; ++i)
// ! It's very important to use "#". opt_key#n is a real option key used in GroupAndCategory
add(key + "#" + std::to_string(i));
}
}
void SettingsIndex::sort_options()
{
// Both views are label-sorted and multi_category-marked. They are separate consumers (the sidebar
// search and the Speed Dial); keeping them in sync here prevents the all-modes view from silently
// diverging in order or flags.
auto sort_and_mark = [](std::vector<Option> &v) {
std::sort(v.begin(), v.end(), [](const Option &o1, const Option &o2) { return o1.label < o2.label; });
Option *last = nullptr;
for (auto &opt : v) {
if (last && last->label == opt.label && last->group == opt.group && last->type == opt.type && last->category != opt.category) {
last->multi_category = true;
opt.multi_category = true;
}
last = &opt;
}
};
sort_and_mark(m_options);
sort_and_mark(m_all_modes);
}
void SettingsIndex::init(std::vector<InputInfo> input_values)
{
m_options.clear();
m_all_modes.clear();
for (auto i : input_values) append_options(i.config, i.type, i.mode);
sort_options();
}
bool SettingsIndex::apply(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode)
{
// m_all_modes is a separate consumer (the Speed Dial), so "nothing initialised yet" means both
// views are empty - the mode-filtered m_options can be empty while m_all_modes is not.
if (m_options.empty() && m_all_modes.empty()) return false;
m_options.erase(std::remove_if(m_options.begin(), m_options.end(), [type](Option opt) { return opt.type == type; }), m_options.end());
m_all_modes.erase(std::remove_if(m_all_modes.begin(), m_all_modes.end(), [type](Option opt) { return opt.type == type; }), m_all_modes.end());
append_options(config, type, mode);
sort_options();
return true;
}
const Option &SettingsIndex::get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const
{
auto not_found = [&variant_index]() -> const Option & {
static const Option empty_option;
variant_index = -2;
return empty_option;
};
variant_index = -1;
std::string opt_key2 = opt_key;
if (auto n = opt_key.find('#'); n != std::string::npos) {
variant_index = std::atoi(opt_key.c_str() + n + 1);
opt_key2 = opt_key.substr(0, n);
}
const std::wstring key = boost::nowide::widen(get_key(opt_key2, type));
auto it = std::lower_bound(m_options.begin(), m_options.end(), Option({key}));
if (it == m_options.end()) return not_found();
if (it->key == key) {
variant_index = -1;
} else {
const std::wstring prefix = key + L"#";
it = std::lower_bound(it, m_options.end(), Option({prefix}));
if (it == m_options.end() || it->key.compare(0, prefix.length(), prefix) != 0)
return not_found();
// Orca: Copy-parameters dialogs request the base key, without a vector index.
if (variant_index < 0) return *it;
const bool has_mode = type == Preset::TYPE_PRINTER && printer_options_with_variant_2.count(opt_key2) > 0;
const bool has_variant =
(type == Preset::TYPE_PRINT && print_options_with_variant.count(opt_key2) > 0) ||
(type == Preset::TYPE_FILAMENT && filament_options_with_variant.count(opt_key2) > 0) ||
(type == Preset::TYPE_PRINTER && printer_options_with_variant_1.count(opt_key2) > 0) || has_mode;
if (!has_variant || has_mode) {
// Orca: Machine limits store (Normal, Silent) pairs per variant; the UI registers only #0/#1.
const std::wstring indexed_key = has_mode ? prefix + std::to_wstring(variant_index % 2) :
boost::nowide::widen(get_key(opt_key, type));
it = std::lower_bound(it, m_options.end(), Option({indexed_key}));
if (it == m_options.end() || it->key != indexed_key)
return not_found();
if (!has_variant)
variant_index = -1;
}
}
return m_options[it - m_options.begin()];
}
static Option create_option(const std::string &opt_key, const wxString &label, Preset::Type type, const GroupAndCategory &gc)
{
return make_option(get_key(opt_key, type), type, label, gc, comSimple, std::string(), true);
}
Option SettingsIndex::get_option(const std::string &opt_key, const wxString &label, Preset::Type type) const
{
std::string key = get_key(opt_key, type);
auto it = std::lower_bound(m_options.begin(), m_options.end(), Option({boost::nowide::widen(key)}));
// BBS: return the 0th option when not found in searcher caused by mode difference
if (it == m_options.end()) return m_options[0];
if (it->key == boost::nowide::widen(key)) return m_options[it - m_options.begin()];
if (m_groups_and_categories.find(key) == m_groups_and_categories.end()) {
size_t pos = key.find('#');
if (pos == std::string::npos) return m_options[it - m_options.begin()];
std::string zero_opt_key = key.substr(0, pos + 1) + "0";
if (m_groups_and_categories.find(zero_opt_key) == m_groups_and_categories.end()) return m_options[it - m_options.begin()];
return create_option(opt_key, label, type, m_groups_and_categories.at(zero_opt_key));
}
const GroupAndCategory &gc = m_groups_and_categories.at(key);
if (gc.group.IsEmpty() || gc.category.IsEmpty()) return m_options[it - m_options.begin()];
return create_option(opt_key, label, type, gc);
}
void SettingsIndex::add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category, const wxString &icon)
{
// Update fields in place so a page rebuild (get_option after set_path) doesn't drop the
// previously recorded wiki path.
GroupAndCategory &gc = m_groups_and_categories[get_key(opt_key, type)];
gc.group = group;
gc.category = category;
gc.icon = icon;
}
void SettingsIndex::set_path(const std::string &opt_key, Preset::Type type, const std::string &path)
{
if (path.empty())
return;
m_groups_and_categories[get_key(opt_key, type)].path = path;
}
void SettingsIndex::set_line_label(const std::string &opt_key, Preset::Type type, const wxString &label)
{
if (label.IsEmpty())
return;
m_groups_and_categories[get_key(opt_key, type)].line_label = label;
}
} // namespace Search
} // namespace Slic3r
+123
View File
@@ -0,0 +1,123 @@
#ifndef slic3r_SettingsIndex_hpp_
#define slic3r_SettingsIndex_hpp_
#include <algorithm>
#include <cstddef>
#include <map>
#include <string>
#include <vector>
#include <wx/string.h>
#include <libslic3r/Config.hpp>
#include <libslic3r/Preset.hpp>
namespace Slic3r {
namespace Search {
struct InputInfo
{
DynamicPrintConfig *config{nullptr};
Preset::Type type{Preset::TYPE_INVALID};
ConfigOptionMode mode{comSimple};
};
struct GroupAndCategory
{
wxString group;
wxString category;
wxString icon; // icon of the group's own header, or empty
wxString line_label; // label the settings row actually draws (Line::label), or empty
std::string path; // wiki path (Line::label_path) of the option's line, or empty
};
// Title for a setting: the row label, qualified with the field leaf when the row packs several
// options (e.g. "Cool Plate \u2013 First layer"). Pure; inputs are already localized.
inline wxString compose_display_label(const wxString& line_label, const wxString& leaf_label, bool multi)
{
if (line_label.empty())
return leaf_label;
if (!multi || leaf_label.empty() || leaf_label == line_label)
return line_label;
return line_label + L" \u2013 " + leaf_label; // en dash separator
}
// Title to show. A single-option row uses its live label, which can be renamed at runtime
// (brim_width -> "Brim ear radius"); otherwise fall back to the precomposed label.
inline wxString resolve_setting_title(const wxString& precomposed, const wxString& live_label, bool live_multi)
{
if (!live_multi && !live_label.empty())
return live_label;
return precomposed;
}
struct Option
{
// bool operator<(const Option& other) const { return other.label > this->label; }
bool operator<(const Option &other) const { return other.key > this->key; }
// Fuzzy matching works at a character level. Thus matching with wide characters is a safer bet than with short characters,
// though for some languages (Chinese?) it may not work correctly.
std::wstring key;
Preset::Type type{Preset::TYPE_INVALID};
std::wstring label;
std::wstring label_local;
std::wstring group;
std::wstring group_local;
std::string group_icon; // SVG base name of the group's own header icon, or empty
std::wstring category;
std::wstring category_local;
bool multi_category { false };
ConfigOptionMode mode{comSimple}; // option's visibility threshold; drives the Speed Dial's mode prompt
std::string tooltip; // localized ConfigOptionDef::tooltip, or empty
std::string wiki_path; // Line::label_path for the option's row, or empty
std::string display_label; // label the settings row draws (localized); empty falls back to label
std::string opt_key() const;
};
// Catalog of settings and their metadata. Owns the group/category registry populated by the
// settings pages, plus two views of the options: the mode-filtered view the sidebar search
// queries, and every option regardless of mode for the Speed Dial.
class SettingsIndex
{
std::map<std::string, GroupAndCategory> m_groups_and_categories;
std::vector<Option> m_options; // mode-filtered view used by the sidebar search
std::vector<Option> m_all_modes; // every option regardless of mode, for the Speed Dial
void append_options(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode);
void sort_options();
public:
void init(std::vector<InputInfo> input_values);
// Rebuild the given type's options; returns false when the index was never initialised.
bool apply(DynamicPrintConfig *config, Preset::Type type, ConfigOptionMode mode);
void add_key(const std::string &opt_key, Preset::Type type, const wxString &group, const wxString &category,
const wxString &icon = wxEmptyString);
void set_path(const std::string &opt_key, Preset::Type type, const std::string &path);
// Record the label the option's row draws, so the Speed Dial names a setting like the page
// (ConfigOptionDef::label/full_label is a search name, not the row text).
void set_line_label(const std::string &opt_key, Preset::Type type, const wxString &label);
const std::vector<Option> &options() const { return m_options; }
const std::vector<Option> &all_options() const { return m_all_modes; }
const Option & option_at(size_t pos) const { return m_options[pos]; }
const Option &get_option(const std::string &opt_key, Preset::Type type, int &variant_index) const;
Option get_option(const std::string &opt_key, const wxString &label, Preset::Type type) const;
const GroupAndCategory &get_group_and_category(const std::string &opt_key) { return m_groups_and_categories[opt_key]; }
void sort_options_by_key()
{
std::sort(m_options.begin(), m_options.end(), [](const Option &o1, const Option &o2) { return o1.key < o2.key; });
}
void sort_options_by_label() { sort_options(); }
};
} // namespace Search
} // namespace Slic3r
#endif // slic3r_SettingsIndex_hpp_
+282 -46
View File
@@ -11,9 +11,16 @@
#include <algorithm>
#include <wx/dcmemory.h>
#include <wx/display.h>
#include <wx/region.h>
#include <wx/sizer.h>
#include <wx/stattext.h>
#include <wx/utils.h>
#ifdef __linux__
#include <gtk/gtk.h>
#endif
namespace Slic3r { namespace GUI {
@@ -21,8 +28,8 @@ namespace {
// ADJUST WIDTH HERE (DIP px). Fixed dialog width; was 360, now 1.5x. Height is not set here -
// the dialog auto-resizes to the page content (see resize_to_content + the list max-height in style.css).
constexpr int kPopupWidth = 540;
constexpr int kPopupMinHeight = 60; // just above the bare search-bar height, so the dialog hugs content
constexpr int kPopupWidth = 540;
constexpr int kPopupMinHeight = 60; // just above the bare search-bar height, so the dialog hugs content
constexpr int kPopupMaxHeight = 282;
int json_int_or(const nlohmann::json& j, const char* key, int fallback)
@@ -31,47 +38,157 @@ int json_int_or(const nlohmann::json& j, const char* key, int fallback)
return it != j.end() && it->is_number() ? it->get<int>() : fallback;
}
wxColour bg_color() { return wxGetApp().get_window_default_clr(); }
}
SpeedDialWebDialog::SpeedDialWebDialog(wxWindow* parent)
: WebViewHostDialog(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize,
wxBORDER_NONE | wxFRAME_NO_TASKBAR)
// Display name of a settings mode, for the mode-switch confirmation.
wxString mode_label(ConfigOptionMode mode)
{
SetBackgroundColour(bg_color());
Bind(wxEVT_ACTIVATE, [this](wxActivateEvent& event) {
if (!event.GetActive() && IsShown())
Hide();
event.Skip();
});
if (!create_webview("web/dialog/SpeedDial/index.html", wxEmptyString,
wxSize(kPopupWidth, kPopupMaxHeight), wxSize(kPopupWidth, kPopupMinHeight))) {
auto* sizer = new wxBoxSizer(wxVERTICAL);
sizer->Add(new wxStaticText(this, wxID_ANY, wxS("wxWebView unavailable")),
wxSizerFlags().Border(wxALL, 20));
SetSizer(sizer);
SetClientSize(FromDIP(wxSize(kPopupWidth, kPopupMinHeight)));
switch (mode) {
case comAdvanced: return _L("Advanced");
case comExpert: return _L("Expert");
case comDevelop: return _L("Developer");
default: return _L("Simple");
}
}
wxColour bg_color() { return wxGetApp().get_window_default_clr(); }
// Give the WebKitGTK widget itself input focus, not its GtkScrolledWindow container.
// (browser()->SetFocus() grabs focus on the container and doesn't reach the web content,
// so typing only works after the user clicks.) On Linux the native backend is the
// WebKitWebView widget; grab focus there directly. Elsewhere SetFocus() is correct.
void focus_webview(wxWebView* browser, bool page_ready)
{
if (!browser)
return;
#ifdef __linux__
if (void* nb = browser->GetNativeBackend())
gtk_widget_grab_focus((GtkWidget*) nb);
#else
browser->SetFocus();
#endif
if (page_ready)
browser->RunScript("focusInput();");
}
// Localized strings for the Speed Dial page, injected as a document-start user script. The page's
// T() reads window.ORCA_UI_STRINGS, so these flow through the same .po pipeline as the rest of the
// UI (the JS literals are only a fallback before the script runs / in the node vm test).
// %% is a literal '%': T() collapses it after substituting %s. Keep the shortcut tokens out of the
// translated text so the platform prefix (Alt+/⌥+, Ctrl+/⌘+) stays correct.
nlohmann::json speed_dial_ui_strings()
{
const std::string alt = GUI::shortkey_alt_prefix();
const std::string ctrl = GUI::shortkey_ctrl_prefix();
return {
{"shortcut_alt", alt},
{"shortcut_ctrl", ctrl},
{"sd_search", _u8L("Search actions")},
{"sd_clear", _u8L("Clear")},
{"sd_search_n", _u8L("Search %s actions")},
{"sd_recent", _u8L("Recent")},
{"sd_plugins", _u8L("Plugins")},
{"sd_other", _u8L("Other")},
{"sd_no_match_total", _u8L("No actions match (Total: %s)")},
{"sd_no_actions", _u8L("No actions yet")},
{"sd_no_tabs_match", _u8L("No tabs match")},
{"sd_no_tabs", _u8L("No tabs")},
{"sd_result_count", _u8L("Showing %s of %s actions")},
{"sd_result_count_all", _u8L("%s actions")},
{"sd_tab_count", _u8L("%s tabs")},
{"sd_tab_match_count", _u8L("%s matches")},
{"sd_favs_full", _u8L("Favourites are full (%s max)")},
{"sd_go_to_pct", _u8L("Go to %s%% of the layer range")},
{"sd_enter_pct", _u8L("Enter a layer percentage (0-100)")},
{"sd_go_layer_ph", _u8L("Go to layer %% (0-100)")},
{"sd_go_tab_ph", _u8L("Go to tab")},
{"sd_fav_slot", _u8L("Favourite %s (%s)")},
{"sd_pin_fav", _u8L("Pin to favourites (%s)")},
{"sd_unpin_fav", _u8L("Unpin from favourites (%s)")},
{"sd_remove_fav", _u8L("Remove from favourites")},
{"sd_move_left", _u8L("Move left")},
{"sd_move_right", _u8L("Move right")},
{"sd_unpin", _u8L("Unpin")},
{"sd_mode_advanced", _u8L("Advanced")},
{"sd_mode_expert", _u8L("Expert")},
{"sd_mode_develop", _u8L("Developer")},
{"sd_wiki_f1", _u8L("Wiki (F1)")},
{"sd_no_wiki", _u8L("No wiki page for this action")},
{"sd_show_details", _u8L("Show details")},
{"sd_hide_details", _u8L("Hide details")},
};
}
} // namespace
SpeedDialWebDialog::SpeedDialWebDialog(wxWindow* parent)
: WebViewHostDialog(parent,
wxID_ANY,
wxEmptyString,
wxDefaultPosition,
wxDefaultSize,
wxBORDER_NONE | wxFRAME_NO_TASKBAR | wxFRAME_FLOAT_ON_PARENT | wxFRAME_SHAPED)
{
SetBackgroundColour(bg_color());
Bind(wxEVT_ACTIVATE, [this](wxActivateEvent& event) {
// Focus the WebKit widget exactly when the WM makes the popup the active window
// (modeless focus is granted asynchronously, so a focus request made right after
// Show() is dropped). Also re-corrects focus on every re-open.
if (event.GetActive() && IsShown())
focus_webview(browser(), m_page_ready);
else if (!event.GetActive() && IsShown())
Hide();
event.Skip();
});
if (!create_webview("web/dialog/SpeedDial/index.html", wxEmptyString, wxSize(kPopupWidth, kPopupMaxHeight),
wxSize(kPopupWidth, kPopupMinHeight))) {
auto* sizer = new wxBoxSizer(wxVERTICAL);
sizer->Add(new wxStaticText(this, wxID_ANY, wxS("wxWebView unavailable")), wxSizerFlags().Border(wxALL, 20));
SetSizer(sizer);
SetClientSize(FromDIP(wxSize(kPopupWidth, kPopupMinHeight)));
}
// WebView2's browser accelerator keys include Ctrl +/-/0 and Ctrl+wheel zoom, which would resize
// the page inside the fixed-size popup. No-op on the other backends (wxWidgets 3.3 base virtual).
if (wxWebView* wv = browser())
wv->EnableBrowserAcceleratorKeys(false);
// Re-cut the shape whenever layout changes the client size. wxOSX SetShape resizes the
// NSWindow, which fires this synchronously; apply_rounded_shape() guards re-entry.
Bind(wxEVT_SIZE, [this](wxSizeEvent& event) {
event.Skip();
apply_rounded_shape();
});
apply_rounded_shape();
}
SpeedDialWebDialog::~SpeedDialWebDialog() { m_alive->store(false, std::memory_order_release); }
// Document-start hook: hand the page its translated strings before speeddial.js runs, so the first
// paint is already localized. The table is built when the dialog is created; a live language switch
// rebuilds the GUI (and with it this dialog), so the next open re-injects the new locale.
void SpeedDialWebDialog::add_user_scripts()
{
if (wxWebView* wv = browser()) {
const std::string js = "window.ORCA_UI_STRINGS = " +
speed_dial_ui_strings().dump(-1, ' ', false, nlohmann::json::error_handler_t::ignore) + ";";
wv->AddUserScript(wxString::FromUTF8(js));
}
}
void SpeedDialWebDialog::request_show()
{
if (IsShown()) {
Raise();
if (browser())
browser()->SetFocus();
focus_webview(browser(), m_page_ready);
return;
}
Show();
Raise();
apply_rounded_shape();
if (m_page_ready)
send_actions();
if (browser())
browser()->SetFocus();
// Grab focus now and again on wxEVT_ACTIVATE; grabbing directly on the WebKit widget is
// what makes typing reach the search field immediately on open.
focus_webview(browser(), m_page_ready);
}
void SpeedDialWebDialog::on_script_message(const nlohmann::json& payload)
@@ -95,23 +212,44 @@ void SpeedDialWebDialog::handle_web_command(const nlohmann::json& payload)
if (command == "request_actions") {
m_page_ready = true;
send_actions();
}
else if (command == "toggle_favourite")
wxGetApp().action_registry().set_favourite(payload.value("id", ""), payload.value("fav", false));
else if (command == "reorder_favourites") {
} else if (command == "toggle_favourite") {
// set_favourite() refuses once the bar hits kFavLimit; tell the page so it can undo the
// pin and show a "favourites are full" hint instead of silently losing the favourite.
const std::string fav_id = payload.value("id", "");
const bool ok = wxGetApp().action_registry().set_favourite(fav_id, payload.value("fav", false));
if (!ok)
call_web_handler({{"command", "favourite_full"}, {"limit", (int) ActionRegistry::kFavLimit}, {"id", fav_id}});
} else if (command == "reorder_favourites") {
std::vector<std::string> ids;
if (payload.contains("ids") && payload["ids"].is_array())
for (const auto& id : payload["ids"])
if (id.is_string())
ids.push_back(id.get<std::string>());
wxGetApp().action_registry().reorder_favourites(ids);
}
} else if (command == "set_tooltip_expanded")
wxGetApp().action_registry().set_tooltip_expanded(payload.value("expanded", true));
else if (command == "run_action")
run_action(payload.value("id", ""), payload.value("title", ""));
run_action(payload.value("id", ""), payload.value("title", ""), payload.value("param", ""));
else if (command == "open_wiki")
open_wiki(payload.value("id", ""));
else if (command == "search_tabs")
search_tabs();
else if (command == "resize")
resize_to_content(json_int_or(payload, "height", 0));
}
void SpeedDialWebDialog::search_tabs()
{
// Round-trip is async because the webview delivers script messages synchronously on the
// GTK/macOS stack; defer the (cheap) enumeration and push the result back to the page.
wxGetApp().CallAfter([this, alive = m_alive]() {
if (!alive->load(std::memory_order_acquire))
return;
auto tabs = wxGetApp().action_registry().tab_options();
call_web_handler({{"command", "tab_results"}, {"tabs", std::move(tabs)}});
});
}
void SpeedDialWebDialog::resize_to_content(int height)
{
if (height <= 0)
@@ -125,17 +263,101 @@ void SpeedDialWebDialog::resize_to_content(int height)
const int height_dip = std::max(kPopupMinHeight, std::min(height, max_dip));
SetClientSize(FromDIP(wxSize(kPopupWidth, height_dip)));
Layout();
#ifdef __WXOSX__
// WKWebView can lag the dialog's new client size; force the viewport to match so the page is
// never painted (and clipped by the rounded layer) below the footer.
if (wxWebView* wv = browser()) {
const wxSize client = GetClientSize();
if (wv->GetSize() != client)
wv->SetSize(client);
}
#endif
apply_rounded_shape();
}
void SpeedDialWebDialog::run_action(const std::string& id, const std::string& title)
// Rounded corners: the webview paints an opaque rectangle, so round the whole top-level window.
// GTK/MSW use a shape region (same mask trick as FilamentPickerDialog, binary edges, no
// anti-aliasing); macOS clips the native view layer instead, since SetShape cannot shape there.
void SpeedDialWebDialog::apply_rounded_shape()
{
// wxOSX SetShape resizes the NSWindow (setContentSize 10x10 then back), which synchronously
// fires wxEVT_SIZE -> apply_rounded_shape() -> SetShape() and recurses until the stack
// overflows. GTK/MSW set a region without resizing, so they are unaffected.
if (m_applying_shape)
return;
// BORDER_NONE means the window is all client area, so the client size is the shape size.
const wxSize size = GetClientSize();
if (size.GetWidth() <= 0 || size.GetHeight() <= 0)
return;
m_applying_shape = true;
#ifdef __WXOSX__
// wxOSX ignores the region (it only clears the window background), so round the native view.
set_window_corner_radius(this, FromDIP(m_corner_radius));
#else
m_shape_bmp.Create(size.GetWidth(), size.GetHeight(), 32);
if (m_shape_bmp.IsOk()) {
wxMemoryDC dc;
dc.SelectObject(m_shape_bmp);
dc.SetBackground(wxBrush(wxColour(0, 0, 0)));
dc.Clear();
dc.SetBrush(wxBrush(wxColour(255, 255, 255, 255)));
dc.SetPen(*wxTRANSPARENT_PEN);
dc.DrawRoundedRectangle(0, 0, size.GetWidth(), size.GetHeight(), FromDIP(m_corner_radius));
dc.SelectObject(wxNullBitmap);
wxRegion region(m_shape_bmp, wxColour(0, 0, 0));
if (region.IsOk())
SetShape(region);
}
#endif
m_applying_shape = false;
}
void SpeedDialWebDialog::on_dpi_changed(const wxRect&)
{
apply_rounded_shape();
Refresh();
}
void SpeedDialWebDialog::run_action(const std::string& id, const std::string& title, const std::string& param)
{
ActionRegistry& reg = wxGetApp().action_registry();
const AppAction* a = reg.by_id(id);
const AppAction* a = reg.by_id(id);
if (!a)
return;
const bool ask = reg.should_ask(id);
// Only plugin actions get the "Run plugin?" confirm. Built-in commands act immediately.
const bool ask = a->kind == AppActionKind::Plugin && reg.should_ask(id);
const std::string atitle = a->title();
const ConfigOptionMode required = a->required_mode;
// Settings the current mode hides require a switch first. Ask while the dial is still up; a
// cancel dismisses both (the dial also auto-hides when the modal takes activation).
if (requires_mode_switch(required, wxGetApp().get_mode())) {
const wxString setting = title.empty() ? from_u8(atitle) : from_u8(title);
if (required == comDevelop) {
RichMessageDialog dlg(wxGetApp().mainframe,
wxString::Format(_L("\"%s\" is a Developer setting. Enable Developer mode to edit it?"),
setting),
_L("Developer setting"), wxOK | wxCANCEL);
if (dlg.ShowModal() != wxID_OK)
return;
wxGetApp().enable_developer_mode();
} else {
RichMessageDialog dlg(wxGetApp().mainframe,
wxString::Format(_L("\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?"),
setting, mode_label(required), mode_label(wxGetApp().get_mode()), mode_label(required)),
_L("Switch settings mode"), wxOK | wxCANCEL);
if (dlg.ShowModal() != wxID_OK)
return;
wxGetApp().save_mode(required);
}
}
if (IsModal())
EndModal(wxID_CANCEL);
else
@@ -143,8 +365,7 @@ void SpeedDialWebDialog::run_action(const std::string& id, const std::string& ti
if (ask) {
const wxString label = title.empty() ? from_u8(atitle) : from_u8(title);
RichMessageDialog dlg(wxGetApp().mainframe, wxString::Format(_L("Run \"%s\"?"), label),
_L("Run plugin"), wxOK | wxCANCEL);
RichMessageDialog dlg(wxGetApp().mainframe, wxString::Format(_L("Run \"%s\"?"), label), _L("Run plugin"), wxOK | wxCANCEL);
dlg.ShowCheckBox(_L("Don't ask again for this action"));
if (dlg.ShowModal() != wxID_OK)
return;
@@ -152,27 +373,42 @@ void SpeedDialWebDialog::run_action(const std::string& id, const std::string& ti
wxGetApp().action_registry().suppress_ask(id);
}
wxGetApp().CallAfter([id] {
wxGetApp().CallAfter([id, param] {
if (wxGetApp().is_closing())
return;
AppActionRunResult result = wxGetApp().action_registry().run(id);
AppActionRunResult result = wxGetApp().action_registry().run(id, param);
if (result.level == AppActionRunResult::Level::Busy)
return;
if (!result.message.IsEmpty() && wxGetApp().plater())
wxGetApp().plater()->get_notification_manager()->push_notification(
NotificationType::CustomNotification,
result.level == AppActionRunResult::Level::Error ? NotificationManager::NotificationLevel::ErrorNotificationLevel :
NotificationManager::NotificationLevel::RegularNotificationLevel,
into_u8(result.message));
wxGetApp()
.plater()
->get_notification_manager()
->push_notification(NotificationType::CustomNotification,
result.level == AppActionRunResult::Level::Error ?
NotificationManager::NotificationLevel::ErrorNotificationLevel :
NotificationManager::NotificationLevel::RegularNotificationLevel,
into_u8(result.message));
});
}
void SpeedDialWebDialog::open_wiki(const std::string& id)
{
const AppAction* a = wxGetApp().action_registry().by_id(id);
if (!a || a->help_url.empty())
return;
Hide();
wxLaunchDefaultBrowser(from_u8(a->help_url));
}
void SpeedDialWebDialog::send_actions()
{
nlohmann::json snap = wxGetApp().action_registry().snapshot();
call_web_handler({{"command", "list_actions"},
{"actions", std::move(snap["actions"])},
{"favourites", std::move(snap["favourites"])}});
{"favourites", std::move(snap["favourites"])},
{"recent", std::move(snap["recent"])},
{"user_mode", std::move(snap["user_mode"])},
{"tooltip_expanded", std::move(snap["tooltip_expanded"])}});
}
}}
}} // namespace Slic3r::GUI
+13 -1
View File
@@ -7,6 +7,8 @@
#include <memory>
#include <string>
#include <wx/bitmap.h>
namespace Slic3r { namespace GUI {
class SpeedDialWebDialog : public WebViewHostDialog
@@ -17,13 +19,23 @@ public:
void request_show();
private:
void add_user_scripts() override;
void on_script_message(const nlohmann::json& payload) override;
void handle_web_command(const nlohmann::json& payload);
void resize_to_content(int height);
void run_action(const std::string& id, const std::string& title);
void run_action(const std::string& id, const std::string& title, const std::string& param = "");
void open_wiki(const std::string& id);
void send_actions();
void search_tabs();
void apply_rounded_shape();
void on_dpi_changed(const wxRect& suggested_rect) override;
bool m_page_ready{false};
// Rounded corners (shape region on GTK/MSW, native layer on macOS), since the webview is opaque.
int m_corner_radius{7};
wxBitmap m_shape_bmp;
// wxOSX SetShape resizes the window, which re-enters apply_rounded_shape() through wxEVT_SIZE.
bool m_applying_shape{false};
// Guards the CallAfter in on_script_message across dialog destruction, same as
// PluginsDialog::m_alive (PluginsDialog.hpp:249).
std::shared_ptr<std::atomic<bool>> m_alive = std::make_shared<std::atomic<bool>>(true);
+109 -21
View File
@@ -1736,16 +1736,46 @@ void Tab::toggle_option(const std::string& opt_key, bool toggle, int opt_index/*
void Tab::toggle_line(const std::string &opt_key, bool toggle, int opt_index)
{
if (!m_active_page) return;
Line *line = m_active_page->get_line(opt_key, opt_index);
if (line) line->toggle_visible = toggle;
// Apply to every page that owns the option, not just m_active_page. ConfigManipulation runs while
// each tab updates at preset load, so the Speed Dial sees the same visibility regardless of page.
for (const PageShp& page : m_pages) {
if (!page) continue;
if (Line *line = page->get_line(opt_key, opt_index))
line->toggle_visible = toggle;
}
};
void Tab::set_option_label(const std::string &opt_key, const wxString &label, int opt_index)
{
if (!m_active_page) return;
Line *line = m_active_page->get_line(opt_key, opt_index);
if (line) line->set_label(label);
// Same as toggle_line: a runtime rename (brim_width -> "Brim ear radius") must reach every page
// so the Speed Dial titles the setting before the page has been shown.
for (const PageShp& page : m_pages) {
if (!page) continue;
if (Line *line = page->get_line(opt_key, opt_index))
line->set_label(label);
}
}
Tab::SettingRowState Tab::setting_row_state(const std::string &opt_id) const
{
bool found = false;
for (const PageShp& page : m_pages) {
if (!page) continue;
for (const ConfigOptionsGroupShp& group : page->m_optgroups) {
if (!group) continue;
for (const Line& line : group->get_lines()) {
for (const Option& opt : line.get_options()) {
if (opt.opt_id != opt_id)
continue;
if (line.toggle_visible) // shown on any owning page is enough
return {true, line.label, line.get_options().size() > 1};
found = true;
}
}
}
}
// Never registered on a page -> visible, but with no row label to contribute.
return {!found, wxString(), false};
}
// To be called by custom widgets, load a value into a config,
@@ -1992,6 +2022,20 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
// reload scene to update timelapse wipe tower
if (opt_key == "timelapse_type") {
// Smooth timelapse parks the nozzle on the prime tower every layer, so it needs a tower on
// every layer. That is exactly what "No sparse layers" removes, and with both on the tower is
// planned full height and then dropped on emission. Drop "No sparse layers" and tell the user.
if (boost::any_cast<int>(value) == (int) TimelapseType::tlSmooth && m_config->opt_bool("wipe_tower_no_sparse_layers")) {
MessageDialog dlg(wxGetApp().plater(),
_L("Smooth timelapse needs a prime tower on every layer, which is not compatible with \"No sparse layers\". "
"\"No sparse layers\" has been turned off."),
_L("Warning"), wxICON_WARNING | wxOK);
dlg.ShowModal();
DynamicPrintConfig new_conf = *m_config;
new_conf.set_key_value("wipe_tower_no_sparse_layers", new ConfigOptionBool(false));
m_config_manipulation.apply(m_config, &new_conf);
}
bool wipe_tower_enabled = m_config->option<ConfigOptionBool>("enable_prime_tower")->value;
if (!wipe_tower_enabled && boost::any_cast<int>(value) == (int)TimelapseType::tlSmooth) {
MessageDialog dlg(wxGetApp().plater(), _L("A prime tower is required for smooth timelapse mode. There may be flaws on the model without prime tower. Do you want to enable the prime tower\?"),
@@ -2007,6 +2051,23 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
}
}
// Mirror of the timelapse_type branch above: enabling "No sparse layers" while smooth timelapse
// is active would leave the tower on every layer anyway, so fall back to traditional timelapse.
if (opt_key == "wipe_tower_no_sparse_layers" && boost::any_cast<bool>(value)) {
auto timelapse_type = m_config->option<ConfigOptionEnum<TimelapseType>>("timelapse_type");
if (timelapse_type && timelapse_type->value == TimelapseType::tlSmooth) {
MessageDialog dlg(wxGetApp().plater(),
_L("\"No sparse layers\" is not compatible with smooth timelapse, which needs a prime tower on every layer. "
"Timelapse has been switched to traditional mode."),
_L("Warning"), wxICON_WARNING | wxOK);
dlg.ShowModal();
DynamicPrintConfig new_conf = *m_config;
new_conf.set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(TimelapseType::tlTraditional));
m_config_manipulation.apply(m_config, &new_conf);
wxGetApp().plater()->update();
}
}
if (opt_key == "print_sequence" && m_config->opt_enum<PrintSequence>("print_sequence") == PrintSequence::ByObject) {
auto printer_structure_opt = m_preset_bundle->printers.get_edited_preset().config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure");
if (printer_structure_opt && printer_structure_opt->value == PrinterStructure::psI3) {
@@ -2763,6 +2824,7 @@ void TabPrint::build()
optgroup = page->new_optgroup(L("Overhangs"), L"param_overhang");
optgroup->append_single_option_line("detect_overhang_wall", "quality_settings_overhangs#detect-overhang-wall");
optgroup->append_single_option_line("unsupported_wall_last", "quality_settings_overhangs#unsupported-wall-last");
optgroup->append_single_option_line("make_overhang_printable", "quality_settings_overhangs#make-overhang-printable");
optgroup->append_single_option_line("make_overhang_printable_angle", "quality_settings_overhangs#maximum-angle");
optgroup->append_single_option_line("make_overhang_printable_hole_size", "quality_settings_overhangs#hole-area");
@@ -3026,6 +3088,8 @@ void TabPrint::build()
optgroup = page->new_optgroup(L("Advanced"), L"advanced");
optgroup->append_single_option_line("interlocking_beam", "multimaterial_settings_advanced#interlocking-beam");
optgroup->append_single_option_line("toolchange_ordering", "multimaterial_settings_advanced#toolchange-ordering");
optgroup->append_single_option_line("toolchange_cyclic_order", "multimaterial_settings_advanced#toolchange-order");
optgroup->append_single_option_line("toolchange_cyclic_first_layer", "multimaterial_settings_advanced#toolchange-order");
optgroup->append_single_option_line("interface_shells", "multimaterial_settings_advanced#interface-shells");
optgroup->append_single_option_line("mmu_segmented_region_max_width", "multimaterial_settings_advanced#maximum-width-of-segmented-region");
optgroup->append_single_option_line("mmu_segmented_region_interlocking_depth", "multimaterial_settings_advanced#interlocking-depth-of-segmented-region");
@@ -3927,7 +3991,7 @@ void TabPrintLayer::update_custom_dirty(std::vector<std::string> &dirty_options,
bool Tab::validate_custom_gcode(const wxString& title, const std::string& gcode)
{
std::vector<std::string> tags;
bool invalid = GCodeProcessor::contains_reserved_tags(gcode, 5, tags);
bool invalid = GCodeProcessor::contains_reserved_tags(gcode, 5, tags, wxGetApp().preset_bundle->is_bbl_vendor());
if (invalid) {
std::string lines = ":\n";
for (const std::string& keyword : tags)
@@ -3956,11 +4020,27 @@ static void validate_custom_gcode_cb(Tab* tab, ConfigOptionsGroupShp opt_group,
tab->on_value_change(opt_key, value);
}
// Orca: names the field the way its page does. The option label alone is ambiguous, as the machine's
// and the filament's custom G-code are both labelled "Start G-code".
static wxString custom_gcode_group_title(const Page* page, const t_config_option_key& opt_key)
{
if (page)
for (const auto& opt_group : page->m_optgroups)
for (const auto& opt : opt_group->opt_map())
if (opt.second.first == opt_key)
return opt_group->title;
return from_u8(opt_key);
}
void Tab::edit_custom_gcode(const t_config_option_key& opt_key)
{
EditGCodeDialog dlg = EditGCodeDialog(this, opt_key, get_custom_gcode(opt_key));
if (dlg.ShowModal() == wxID_OK) {
set_custom_gcode(opt_key, dlg.get_edited_gcode());
const std::string edited_gcode = dlg.get_edited_gcode();
// Orca: this dialog writes the value straight into the config, bypassing the field's change
// handler, so the reserved keyword check has to run here as it does when editing in place.
validate_custom_gcodes_was_shown = !validate_custom_gcode(custom_gcode_group_title(m_active_page, opt_key), edited_gcode);
set_custom_gcode(opt_key, edited_gcode);
update_dirty();
update();
}
@@ -5107,6 +5187,7 @@ void TabPrinter::build_fff()
optgroup = page->new_optgroup(L("Extruder Clearance"), "param_extruder_clearance");
optgroup->append_single_option_line("extruder_clearance_radius", "printer_basic_information_extruder_clearance#radius");
optgroup->append_single_option_line("extruder_clearance_dist_to_rod", "printer_basic_information_extruder_clearance#distance-to-rod");
optgroup->append_single_option_line("extruder_clearance_height_to_rod", "printer_basic_information_extruder_clearance#height-to-rod");
optgroup->append_single_option_line("extruder_clearance_height_to_lid", "printer_basic_information_extruder_clearance#height-to-lid");
@@ -5747,11 +5828,11 @@ if (is_marlin_flavor)
} else if (m_extruders_count_old == 1) {
first_extruder_title = wxString::Format("Extruder %d", 1);
}
auto & searcher = wxGetApp().sidebar().get_searcher();
auto & index = wxGetApp().sidebar().settings_index();
for (auto &group : m_pages[n_before_extruders]->m_optgroups) {
group->set_config_category_and_type(first_extruder_title, m_type);
for (auto &opt : group->opt_map())
searcher.add_key(opt.first + "#0", m_type, group->title, first_extruder_title);
index.add_key(opt.first + "#0", m_type, group->title, first_extruder_title, group->icon);
}
Thaw();
@@ -7835,10 +7916,10 @@ wxSizer* TabPrinter::create_bed_shape_widget(wxWindow* parent)
}));
{
Search::OptionsSearcher& searcher = wxGetApp().sidebar().get_searcher();
const Search::GroupAndCategory& gc = searcher.get_group_and_category("printable_area");
searcher.add_key("bed_custom_texture", m_type, gc.group, gc.category);
searcher.add_key("bed_custom_model", m_type, gc.group, gc.category);
Search::SettingsIndex& index = wxGetApp().sidebar().settings_index();
const Search::GroupAndCategory& gc = index.get_group_and_category("printable_area");
index.add_key("bed_custom_texture", m_type, gc.group, gc.category, gc.icon);
index.add_key("bed_custom_model", m_type, gc.group, gc.category, gc.icon);
}
return sizer;
@@ -8321,17 +8402,19 @@ void Tab::sync_excluder()
Preset & printer_preset = m_preset_bundle->printers.get_edited_preset();
auto nozzle_volumes = m_preset_bundle->project_config.option<ConfigOptionEnumsGeneric>("nozzle_volume_type");
auto extruders = printer_preset.config.option<ConfigOptionEnumsGeneric>("extruder_type");
// Motion ability options hold a (normal, silent) pair per variant, so switch_excluder indexes that page with stride 2.
const int stride = m_active_page->title().StartsWith("Motion ability") ? 2 : 1;
auto get_index_for_extruder =
[this, &extruders, variant_keys = extruder_variant_keys[m_type >= Preset::TYPE_COUNT ? Preset::TYPE_PRINT : m_type]](int extruder_id, NozzleVolumeType nozzle_type) {
[this, &extruders, stride, variant_keys = extruder_variant_keys[m_type >= Preset::TYPE_COUNT ? Preset::TYPE_PRINT : m_type]](int extruder_id, NozzleVolumeType nozzle_type) {
return m_config->get_index_for_extruder(extruder_id + 1, variant_keys.first,
ExtruderType(extruders->values[extruder_id]), nozzle_type, variant_keys.second);
ExtruderType(extruders->values[extruder_id]), nozzle_type, variant_keys.second, stride);
};
int active_index = get_current_active_extruder();
auto active_nozzle = get_actual_nozzle_volume_type(active_index);
int from_index = get_index_for_extruder(active_index, active_nozzle);
int dest_index = get_index_for_extruder(1 - active_index, active_nozzle);
auto from_str = std::to_string(from_index);
auto dest_str = std::to_string(dest_index);
if (from_index < 0 || dest_index < 0) // no variant column for this nozzle on one of the extruders
return;
auto dirty_options = m_presets->current_dirty_options(true);
DynamicConfig config_origin, config_to_apply;
for (int i = 0; i < dirty_options.size(); ++i) {
@@ -8344,16 +8427,21 @@ void Tab::sync_excluder()
if (field == nullptr || line == nullptr)
continue;
++n;
bool dirty = opt.substr(n) == from_str;
auto is_from_slot = [&](const std::string &dirty_opt) {
int slot = std::atoi(dirty_opt.c_str() + n);
return slot >= from_index && slot < from_index + stride;
};
bool dirty = is_from_slot(opt);
while (i + 1 < dirty_options.size() && dirty_options[i + 1].compare(0, n, opt, 0, n) == 0) {
dirty |= dirty_options[i + 1].substr(n) == from_str;
dirty |= is_from_slot(dirty_options[i + 1]);
++i;
}
if (dirty) {
auto key = opt.substr(0, n - 1);
auto option = dynamic_cast<ConfigOptionVectorBase*>(m_config->option(key));
auto option2 = dynamic_cast<ConfigOptionVectorBase*>(option->clone());
option2->set_at(option, dest_index, from_index);
for (int s = 0; s < stride; ++s)
option2->set_at(option, dest_index + s, from_index + s);
if (*option == *option2) {
delete option2;
continue;
+10
View File
@@ -401,6 +401,16 @@ public:
void toggle_option(const std::string &opt_key, bool toggle, int opt_index = -1);
void toggle_line(const std::string &opt_key, bool toggle, int opt_index = -1); // BBS: hide some line
void set_option_label(const std::string &opt_key, const wxString &label, int opt_index = -1);
// Live state of the settings row that owns an option, read from the built pages.
struct SettingRowState
{
bool visible{true}; // false when ConfigManipulation hides the row
wxString label; // Line::label the row draws (may change at runtime)
bool multi{false}; // row packs several options, so label is precomposed
};
SettingRowState setting_row_state(const std::string &opt_id) const;
wxSizer* description_line_widget(wxWindow* parent, ogStaticText** StaticText, wxString text = wxEmptyString);
bool current_preset_is_dirty() const;
bool saved_preset_is_dirty() const;
+53 -31
View File
@@ -1485,12 +1485,20 @@ std::string UnsavedChangesDialog::subreplace(std::string resource_str, std::stri
void UnsavedChangesDialog::update_tree(Preset::Type type, DynamicConfig * config, int from, int to)
{
Search::OptionsSearcher &searcher = wxGetApp().sidebar().get_searcher();
searcher.sort_options_by_key();
Search::SettingsIndex &index = wxGetApp().sidebar().settings_index();
index.sort_options_by_key();
for (const std::string &opt_key : config->keys()) {
int variant_index = -2;
const Search::Option &option = searcher.get_option(opt_key, type, variant_index);
Search::Option option = index.get_option(opt_key, type, variant_index);
if (variant_index == -2) {
// Orca: Every transferred setting must remain visible even when it is absent from the search index.
const ConfigOptionDef* def = print_config_def.get(opt_key);
const std::string label = def ? (def->full_label.empty() ? def->label : def->full_label) : std::string();
option.label_local = (label.empty() ? from_u8(opt_key) : _L(label)).ToStdWstring();
option.category_local = (def && !def->category.empty() ?
Tab::translate_category(from_u8(def->category), type) : _L("Others")).ToStdWstring();
}
auto category = option.category_local;
auto opt = dynamic_cast<ConfigOptionVectorBase*>(config->option(opt_key));
std::string value_from = opt->vserialize()[from];
@@ -1502,8 +1510,8 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, DynamicConfig * config
void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* presets_)
{
Search::OptionsSearcher& searcher = wxGetApp().sidebar().get_searcher();
searcher.sort_options_by_key();
Search::SettingsIndex& index = wxGetApp().sidebar().settings_index();
index.sort_options_by_key();
// list of the presets with unsaved changes
std::vector<PresetCollection*> presets_list;
@@ -1518,6 +1526,8 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* pres
else
presets_list.emplace_back(presets_);
const bool multiple_extruders = wxGetApp().preset_bundle->get_printer_extruder_count() > 1;
// Display a dialog showing the dirty options in a human readable form.
for (PresetCollection* presets : presets_list)
{
@@ -1553,29 +1563,41 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* pres
auto variant_key = Preset::get_iot_type_string(type) + "_extruder_variant";
auto id_key = Preset::get_iot_type_string(type) + "_extruder_id";
auto extruder_variant = dynamic_cast<ConfigOptionStrings const *>(old_config.option(variant_key));
auto extruder_id = dynamic_cast<ConfigOptionInts const *>(old_config.option(id_key));
// Orca: Dirty indices belong to the edited config, which may contain newly added variants.
auto extruder_variant = dynamic_cast<ConfigOptionStrings const *>(new_config.option(variant_key));
auto extruder_id = dynamic_cast<ConfigOptionInts const *>(new_config.option(id_key));
for (const std::string& opt_key : dirty_options) {
int variant_index = -2;
const Search::Option &option = searcher.get_option(opt_key, type, variant_index);
if (option.opt_key() != opt_key && variant_index < -1) {
const Search::Option &option = index.get_option(opt_key, type, variant_index);
if (variant_index == -2) {
// When founded option isn't the correct one.
// It can be for dirty_options: "default_print_profile", "printer_model", "printer_settings_id",
// because of they don't exist in searcher
// because of they don't exist in the index
continue;
}
auto category = option.category_local;
if (variant_index >= 0) {
if (printer_options_with_variant_2.count(opt_key.substr(0, opt_key.find_last_of('#'))) > 0)
variant_index /= 2;
if (boost::nowide::narrow(category).find("Extruder ") == 0)
category = category.substr(0, 8);
if (extruder_id)
category = category + (wxString(" {") + (extruder_id->values[variant_index] == 1 ? _L("Left: ") : _L("Right: "))
+ L(extruder_variant->values[variant_index]) + "}");
else
category = category + (wxString(" {") + L(extruder_variant->values[variant_index]) + "}");
wxString category = option.category_local;
wxString label = option.label_local;
if (type == Preset::TYPE_PRINTER && variant_index >= 0 &&
printer_options_with_variant_2.count(get_pure_opt_key(opt_key)) > 0) {
// Orca: silent_mode is obsolete on import, but its option and two-column UI still exist.
// Keep mode labels for configs that explicitly enable it; omit them in the default single-mode UI.
if (new_config.opt_bool("silent_mode"))
label += " (" + (variant_index % 2 == 0 ? _L("Normal") : _L("Silent")) + ")";
variant_index /= 2;
}
if (variant_index >= 0 && extruder_variant && variant_index < extruder_variant->size()) {
// Orca: Match the untranslated category and use the same extruder names as the printer tabs.
if (option.category.compare(0, 9, L"Extruder ") == 0)
category = _L("Extruder");
wxString variant_label = L(extruder_variant->values[variant_index]);
// Orca: An extruder name only disambiguates variants on printers with multiple extruders.
if (multiple_extruders && extruder_id && variant_index < extruder_id->size() && extruder_id->values[variant_index] > 0) {
const wxString extruder_name = Tab::translate_category(
wxString::Format("Extruder %d", extruder_id->values[variant_index]), Preset::TYPE_PRINTER);
variant_label = extruder_name + " (" + variant_label + ")";
}
category = variant_label + ": " + category;
}
/*m_tree->Append(opt_key, type, option.category_local, option.group_local, option.label_local,
@@ -1584,14 +1606,14 @@ void UnsavedChangesDialog::update_tree(Preset::Type type, PresetCollection* pres
//PresetItem pi = {opt_key, type, 1983};
//m_presetitems.push_back()
PresetItem pi = {type, opt_key, category, option.group_local, option.label_local, get_string_value(opt_key, old_config), get_string_value(opt_key, new_config)};
PresetItem pi = {type, opt_key, category, option.group_local, label, get_string_value(opt_key, old_config), get_string_value(opt_key, new_config)};
m_presetitems.push_back(pi);
}
}
// Revert sort of searcher back
searcher.sort_options_by_label();
// Revert sort of index back
index.sort_options_by_label();
}
void UnsavedChangesDialog::on_dpi_changed(const wxRect& suggested_rect)
@@ -2043,8 +2065,8 @@ void DiffPresetDialog::update_bottom_info(wxString bottom_info)
void DiffPresetDialog::update_tree()
{
Search::OptionsSearcher& searcher = wxGetApp().sidebar().get_searcher();
searcher.sort_options_by_key();
Search::SettingsIndex& index = wxGetApp().sidebar().settings_index();
index.sort_options_by_key();
m_tree->Clear();
wxString bottom_info = "";
@@ -2124,14 +2146,14 @@ void DiffPresetDialog::update_tree()
wxString right_val = get_string_value(opt_key, right_congig);
const std::string lookup_key = get_pure_opt_key(opt_key);
Search::Option option = searcher.get_option(lookup_key, get_full_label(lookup_key, left_config), type);
Search::Option option = index.get_option(lookup_key, get_full_label(lookup_key, left_config), type);
if (get_pure_opt_key(option.opt_key()) != lookup_key)
option = searcher.get_option(opt_key, get_full_label(opt_key, left_config), type);
option = index.get_option(opt_key, get_full_label(opt_key, left_config), type);
if (get_pure_opt_key(option.opt_key()) != lookup_key) {
// When the found option is not the requested one.
// This can happen for dirty_options such as:
// "default_print_profile", "printer_model", "printer_settings_id",
// because they do not exist in the searcher.
// because they do not exist in the index.
continue;
}
m_tree->Append(opt_key, type, option.category_local, option.group_local, option.label_local,
@@ -2155,8 +2177,8 @@ void DiffPresetDialog::update_tree()
Refresh();
}
// Revert sort of searcher back
searcher.sort_options_by_label();
// Revert sort of index back
index.sort_options_by_label();
}
void DiffPresetDialog::on_dpi_changed(const wxRect&)
+109 -121
View File
@@ -25,36 +25,29 @@ END_EVENT_TABLE()
* calling Refresh()/Update().
*/
Button::Button()
: paddingSize(10, 8)
Button::Button() : paddingSize(10, 8)
{
background_color = StateColor(
std::make_pair(0xF0F0F1, (int) StateColor::Disabled),
std::make_pair(0x52c7b8, (int) StateColor::Hovered | StateColor::Checked),
std::make_pair(0x009688, (int) StateColor::Checked),
std::make_pair(*wxLIGHT_GREY, (int) StateColor::Hovered),
std::make_pair(*wxWHITE, (int) StateColor::Normal));
text_color = StateColor(
std::make_pair(*wxLIGHT_GREY, (int) StateColor::Disabled),
std::make_pair(*wxBLACK, (int) StateColor::Normal));
background_color = StateColor(std::make_pair(0xF0F0F1, (int) StateColor::Disabled),
std::make_pair(0x52c7b8, (int) StateColor::Hovered | StateColor::Checked),
std::make_pair(0x009688, (int) StateColor::Checked),
std::make_pair(*wxLIGHT_GREY, (int) StateColor::Hovered),
std::make_pair(*wxWHITE, (int) StateColor::Normal));
text_color = StateColor(std::make_pair(*wxLIGHT_GREY, (int) StateColor::Disabled), std::make_pair(*wxBLACK, (int) StateColor::Normal));
}
Button::Button(wxWindow* parent, wxString text, wxString icon, long style, int iconSize, wxWindowID btn_id)
: Button()
{
Create(parent, text, icon, style, iconSize, btn_id);
}
Button::Button(wxWindow* parent, wxString text, wxString icon, long style, int iconSize, wxWindowID btn_id) : Button()
{ Create(parent, text, icon, style, iconSize, btn_id); }
bool Button::Create(wxWindow* parent, wxString text, wxString icon, long style, int iconSize, wxWindowID btn_id)
{
StaticBox::Create(parent, btn_id, wxDefaultPosition, wxDefaultSize, style);
state_handler.attach(std::vector<StateColor const*>{&text_color});
state_handler.update_binds();
//BBS set default font
// BBS set default font
SetFont(Label::Body_14);
wxWindow::SetLabel(text);
if (!icon.IsEmpty()) {
//BBS set button icon default size to 20
// BBS set button icon default size to 20
this->active_icon = ScalableBitmap(this, icon.ToStdString(), iconSize > 0 ? iconSize : 20);
}
messureSize();
@@ -82,14 +75,12 @@ void Button::SetIcon(const wxString& icon)
{
auto tmpBitmap = ScalableBitmap(this, icon.ToStdString(), this->active_icon.px_cnt());
if (!icon.IsEmpty()) {
//BBS set button icon default size to 20
// BBS set button icon default size to 20
if (!tmpBitmap.bmp().IsSameAs(this->active_icon.bmp())) {
this->active_icon = tmpBitmap;
Refresh();
}
}
else
{
} else {
this->active_icon = ScalableBitmap();
Refresh();
}
@@ -97,7 +88,7 @@ void Button::SetIcon(const wxString& icon)
void Button::SetIcon(const wxBitmap& icon)
{
this->active_icon = ScalableBitmap();
this->active_icon = ScalableBitmap();
this->active_icon.bmp() = icon;
messureSize();
Refresh();
@@ -121,6 +112,13 @@ void Button::SetPaddingSize(const wxSize& size)
messureSize();
}
void Button::SetIconSpacing(int spacing)
{
m_icon_spacing = spacing;
messureSize();
Refresh();
}
void Button::SetTextColor(StateColor const& color)
{
text_color = color;
@@ -128,7 +126,7 @@ void Button::SetTextColor(StateColor const& color)
Refresh();
}
void Button::SetTextColorNormal(wxColor const &color)
void Button::SetTextColorNormal(wxColor const& color)
{
text_color.setColorForStates(color, 0);
Refresh();
@@ -145,22 +143,22 @@ bool Button::Enable(bool enable)
return result;
}
void Button::SetCanFocus(bool canFocus) {
void Button::SetCanFocus(bool canFocus)
{
StaticBox::SetCanFocus(canFocus);
this->canFocus = canFocus;
}
void Button::SetValue(bool state)
{
if (GetValue() == state) return;
if (GetValue() == state)
return;
state_handler.set_state(state ? StateHandler::Checked : 0, StateHandler::Checked);
}
bool Button::GetValue() const { return state_handler.states() & StateHandler::Checked; }
void Button::SetCenter(bool isCenter)
{
this->isCenter = isCenter; }
void Button::SetCenter(bool isCenter) { this->isCenter = isCenter; }
void Button::SetIndicator(bool on)
{
@@ -186,38 +184,33 @@ wxString btn_disabled[10] = {"#DFDFDF", "#DFDFDF", "#DFDFDF", "#DFDFDF", "#DFDFD
void Button::SetStyle(const ButtonStyle style, const ButtonType type)
{
if (type == ButtonType::Compact) {
this->SetPaddingSize(FromDIP(wxSize(8,3)));
if (type == ButtonType::Compact) {
this->SetPaddingSize(FromDIP(wxSize(8, 3)));
this->SetCornerRadius(this->FromDIP(8));
this->SetFont(Label::Body_10);
}
else if (type == ButtonType::Window) {
this->SetSize(FromDIP(wxSize(58,24)));
this->SetMinSize(FromDIP(wxSize(58,24)));
} else if (type == ButtonType::Window) {
this->SetSize(FromDIP(wxSize(58, 24)));
this->SetMinSize(FromDIP(wxSize(58, 24)));
this->SetCornerRadius(this->FromDIP(12));
this->SetFont(Label::Body_12);
}
else if (type == ButtonType::Choice) {
this->SetMinSize(FromDIP(wxSize(100,32)));
this->SetPaddingSize(FromDIP(wxSize(12,8)));
} else if (type == ButtonType::Choice) {
this->SetMinSize(FromDIP(wxSize(100, 32)));
this->SetPaddingSize(FromDIP(wxSize(12, 8)));
this->SetCornerRadius(this->FromDIP(4));
this->SetFont(Label::Body_14);
}
else if (type == ButtonType::Parameter) {
this->SetMinSize(FromDIP(wxSize(120,26)));
this->SetSize(FromDIP(wxSize(120,26)));
} else if (type == ButtonType::Parameter) {
this->SetMinSize(FromDIP(wxSize(120, 26)));
this->SetSize(FromDIP(wxSize(120, 26)));
this->SetCornerRadius(this->FromDIP(4));
this->SetFont(Label::Body_14);
}
else if (type == ButtonType::Icon) {
this->SetPaddingSize(FromDIP(wxSize(5,5)));
this->SetMinSize(FromDIP(wxSize(26,26)));
this->SetSize(FromDIP(wxSize(26,26)));
} else if (type == ButtonType::Icon) {
this->SetPaddingSize(FromDIP(wxSize(5, 5)));
this->SetMinSize(FromDIP(wxSize(26, 26)));
this->SetSize(FromDIP(wxSize(26, 26)));
this->SetCornerRadius(this->FromDIP(4));
}
else if (type == ButtonType::Expanded) {
this->SetMinSize(FromDIP(wxSize(-1,32)));
this->SetPaddingSize(FromDIP(wxSize(12,8)));
} else if (type == ButtonType::Expanded) {
this->SetMinSize(FromDIP(wxSize(-1, 32)));
this->SetPaddingSize(FromDIP(wxSize(12, 8)));
this->SetCornerRadius(this->FromDIP(4));
this->SetFont(Label::Body_14);
}
@@ -226,39 +219,33 @@ void Button::SetStyle(const ButtonStyle style, const ButtonType type)
bool is_dark = StateColor::darkModeColorFor("#FFFFFF") != wxColour("#FFFFFF");
auto clr_arr = style == ButtonStyle::Regular ? btn_regular :
style == ButtonStyle::Confirm ? btn_confirm :
style == ButtonStyle::Alert ? btn_alert :
auto clr_arr = style == ButtonStyle::Regular ? btn_regular :
style == ButtonStyle::Confirm ? btn_confirm :
style == ButtonStyle::Alert ? btn_alert :
style == ButtonStyle::Disabled ? btn_disabled :
btn_regular ;
btn_regular;
auto bg_color = StateColor(
std::pair(wxColour(clr_arr[0]), (int)StateColor::Disabled),
std::pair(wxColour(clr_arr[1]), (int)StateColor::Pressed),
std::pair(wxColour(clr_arr[2]), (int)StateColor::Hovered),
std::pair(wxColour(clr_arr[3]), (int)StateColor::Normal),
std::pair(wxColour(clr_arr[4]), (int)StateColor::Enabled)
);
auto bg_color = StateColor(std::pair(wxColour(clr_arr[0]), (int) StateColor::Disabled),
std::pair(wxColour(clr_arr[1]), (int) StateColor::Pressed),
std::pair(wxColour(clr_arr[2]), (int) StateColor::Hovered),
std::pair(wxColour(clr_arr[3]), (int) StateColor::Normal),
std::pair(wxColour(clr_arr[4]), (int) StateColor::Enabled));
bg_color.setTakeFocusedAsHovered(false);
this->SetBackgroundColor(bg_color);
wxColour focus_clr = clr_arr[is_dark ? 8 : 9];
auto border_color = StateColor(
std::pair(wxColour(clr_arr[0]), (int)StateColor::Disabled),
std::pair(wxColour(clr_arr[2]), (int)(StateColor::Hovered | ~StateColor::Focused)),
std::pair(wxColour(focus_clr ), (int)StateColor::Focused),
std::pair(wxColour(clr_arr[3]), (int)StateColor::Normal)
);
auto border_color = StateColor(std::pair(wxColour(clr_arr[0]), (int) StateColor::Disabled),
std::pair(wxColour(clr_arr[2]), (int) (StateColor::Hovered | ~StateColor::Focused)),
std::pair(wxColour(focus_clr), (int) StateColor::Focused),
std::pair(wxColour(clr_arr[3]), (int) StateColor::Normal));
border_color.setTakeFocusedAsHovered(false);
this->SetBorderColor(border_color);
this->SetTextColor(StateColor(
std::pair(wxColour(clr_arr[5]), (int)StateColor::Disabled),
std::pair(wxColour(clr_arr[7]), (int)StateColor::Hovered),
std::pair(wxColour(clr_arr[6]), (int)StateColor::Normal)
));
this->SetTextColor(StateColor(std::pair(wxColour(clr_arr[5]), (int) StateColor::Disabled),
std::pair(wxColour(clr_arr[7]), (int) StateColor::Hovered),
std::pair(wxColour(clr_arr[6]), (int) StateColor::Normal)));
m_has_style = true;
m_style = style;
m_type = type;
m_style = style;
m_type = type;
}
void Button::Rescale()
@@ -269,7 +256,7 @@ void Button::Rescale()
messureSize();
if(m_has_style)
if (m_has_style)
SetStyle(m_style, m_type);
Refresh();
@@ -290,7 +277,7 @@ void Button::paintEvent(wxPaintEvent& evt)
void Button::render(wxDC& dc)
{
StaticBox::render(dc);
int states = state_handler.states();
int states = state_handler.states();
wxSize size = GetSize();
dc.SetBrush(*wxTRANSPARENT_BRUSH);
// calc content size
@@ -298,15 +285,15 @@ void Button::render(wxDC& dc)
wxSize textSize = this->textSize.GetSize();
const ScalableBitmap& icon = active_icon;
wxSize padding = this->paddingSize;
int spacing = 5;
wxSize padding = this->paddingSize;
int spacing = m_icon_spacing;
// Wrap text
auto text = GetLabel();
if (vertical && textSize.x + padding.x * 2 > size.x) {
Label::split_lines(dc, size.x - padding.x * 2, text, text, 2);
textSize = dc.GetMultiLineTextExtent(text);
if (padding.x * 2 + textSize.x > size.x) {
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END, size.x - padding.x * 2);
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END, size.x - padding.x * 2);
textSize = dc.GetMultiLineTextExtent(text);
}
}
@@ -316,7 +303,7 @@ void Button::render(wxDC& dc)
const bool gap_reserved = szContent.y > 0;
if (icon.bmp().IsOk()) {
if (gap_reserved) {
//BBS norrow size between text and icon
// BBS norrow size between text and icon
if (vertical)
szContent.y += spacing;
else
@@ -325,10 +312,12 @@ void Button::render(wxDC& dc)
szIcon = icon.GetBmpSize();
if (vertical) {
szContent.y += szIcon.y;
if (szIcon.x > szContent.x) szContent.x = szIcon.x;
if (szIcon.x > szContent.x)
szContent.x = szIcon.x;
} else {
szContent.x += szIcon.x;
if (szIcon.y > szContent.y) szContent.y = szIcon.y;
if (szIcon.y > szContent.y)
szContent.y = szIcon.y;
}
if (szContent.x > size.x) {
int d = std::min(padding.x, (szContent.x - size.x) / 2);
@@ -344,10 +333,11 @@ void Button::render(wxDC& dc)
szContent.x += dot + FromDIP(6);
}
// move to center
wxRect rcContent = { {0, 0}, size };
wxRect rcContent = {{0, 0}, size};
if (isCenter) {
wxSize offset = (size - szContent) / 2;
if (offset.x < 0) offset.x = 0;
if (offset.x < 0)
offset.x = 0;
rcContent.Deflate(offset.x, offset.y);
}
// start draw
@@ -358,7 +348,7 @@ void Button::render(wxDC& dc)
else
pt.y += (rcContent.height - szIcon.y) / 2;
dc.DrawBitmap(icon.bmp(), pt);
//BBS norrow size between text and icon
// BBS norrow size between text and icon
if (vertical) {
pt.y += szIcon.y + (gap_reserved ? spacing : 0);
pt.x = rcContent.x;
@@ -403,19 +393,21 @@ void Button::messureSize()
wxSize szContent = textSize.GetSize();
if (this->active_icon.bmp().IsOk()) {
if (szContent.y > 0) {
//BBS norrow size between text and icon
// BBS narrow size between text and icon
if (vertical)
szContent.y += 5;
szContent.y += m_icon_spacing;
else
szContent.x += 5;
szContent.x += m_icon_spacing;
}
wxSize szIcon = this->active_icon.GetBmpSize();
if (vertical) {
szContent.y += szIcon.y;
if (szIcon.x > szContent.x) szContent.x = szIcon.x;
if (szIcon.x > szContent.x)
szContent.x = szIcon.x;
} else {
szContent.x += szIcon.x;
if (szIcon.y > szContent.y) szContent.y = szIcon.y;
if (szIcon.y > szContent.y)
szContent.y = szIcon.y;
}
}
if (m_show_indicator) {
@@ -467,13 +459,13 @@ void Button::mouseReleased(wxMouseEvent& event)
}
}
void Button::mouseCaptureLost(wxMouseCaptureLostEvent &event)
void Button::mouseCaptureLost(wxMouseCaptureLostEvent& event)
{
wxMouseEvent evt;
mouseReleased(evt);
}
void Button::keyDownUp(wxKeyEvent &event)
void Button::keyDownUp(wxKeyEvent& event)
{
if (event.GetKeyCode() == WXK_SPACE || event.GetKeyCode() == WXK_RETURN) {
wxMouseEvent evt(event.GetEventType() == wxEVT_KEY_UP ? wxEVT_LEFT_UP : wxEVT_LEFT_DOWN);
@@ -482,8 +474,8 @@ void Button::keyDownUp(wxKeyEvent &event)
return;
}
if (event.GetEventType() == wxEVT_KEY_DOWN &&
(event.GetKeyCode() == WXK_TAB || event.GetKeyCode() == WXK_LEFT || event.GetKeyCode() == WXK_RIGHT
|| event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_DOWN))
(event.GetKeyCode() == WXK_TAB || event.GetKeyCode() == WXK_LEFT || event.GetKeyCode() == WXK_RIGHT ||
event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_DOWN))
HandleAsNavigationKey(event);
else
event.Skip();
@@ -500,7 +492,9 @@ void Button::sendButtonEvent()
WXLRESULT Button::MSWWindowProc(WXUINT nMsg, WXWPARAM wParam, WXLPARAM lParam)
{
if (nMsg == WM_GETDLGCODE) { return DLGC_WANTMESSAGE; }
if (nMsg == WM_GETDLGCODE) {
return DLGC_WANTMESSAGE;
}
if (nMsg == WM_KEYDOWN) {
wxKeyEvent event(CreateKeyEvent(wxEVT_KEY_DOWN, wParam, lParam));
switch (wParam) {
@@ -521,8 +515,7 @@ void Button::EnableTooltipEvenDisabled()
{
#if defined(_MSC_VER) || defined(_WIN32)
auto parent = this->GetParent();
if (parent)
{
if (parent) {
parent->Bind(wxEVT_MOTION, &Button::OnParentMotion, this);
parent->Bind(wxEVT_LEAVE_WINDOW, &Button::OnParentLeave, this);
};
@@ -532,22 +525,21 @@ void Button::EnableTooltipEvenDisabled()
void Button::OnParentMotion(wxMouseEvent& event)
{
auto parent = this->GetParent();
if (!parent) return event.Skip();
if (!parent)
return event.Skip();
wxPoint pos = parent->ClientToScreen(event.GetPosition());
wxPoint pos = parent->ClientToScreen(event.GetPosition());
wxRect screen_rect = this->GetScreenRect();
wxString tip = this->GetToolTipText();
if (!tip.IsEmpty() && !this->IsEnabled() && screen_rect.Contains(pos))
{
if (!tipWindow)
{
wxString tip = this->GetToolTipText();
if (!tip.IsEmpty() && !this->IsEnabled() && screen_rect.Contains(pos)) {
if (!tipWindow) {
tipWindow = wxTipWindow::New(this, tip);
if (!tipWindow) return event.Skip();
if (!tipWindow)
return event.Skip();
tipWindow->Enable(false);
}
if (tipWindow->GetLabel() != tip)
{
if (tipWindow->GetLabel() != tip) {
tipWindow->SetLabel(tip);
}
@@ -555,11 +547,8 @@ void Button::OnParentMotion(wxMouseEvent& event)
// using wxGetMousePosition() which returns (0,0) on Wayland.
tipWindow->Position(this->ClientToScreen(wxPoint(0, 0)), this->GetSize());
tipWindow->Popup();
}
else
{
if (tipWindow)
{
} else {
if (tipWindow) {
tipWindow->Dismiss();
tipWindow->Destroy();
tipWindow = nullptr;
@@ -572,15 +561,14 @@ void Button::OnParentMotion(wxMouseEvent& event)
void Button::OnParentLeave(wxMouseEvent& event)
{
auto parent = this->GetParent();
if (!parent) return event.Skip();
if (!parent)
return event.Skip();
if (tipWindow)
{
wxPoint pos = parent->ClientToScreen(event.GetPosition());
if (tipWindow) {
wxPoint pos = parent->ClientToScreen(event.GetPosition());
wxRect screen_rect = this->GetScreenRect();
wxString tip = this->GetToolTipText();
if (!screen_rect.Contains(pos))
{
wxString tip = this->GetToolTipText();
if (!screen_rect.Contains(pos)) {
tipWindow->Dismiss();
tipWindow->Destroy();
tipWindow = nullptr;
+3
View File
@@ -36,6 +36,7 @@ class Button : public StaticBox
wxRect textSize;
wxSize minSize; // set by outer
wxSize paddingSize;
int m_icon_spacing = 5;
ScalableBitmap active_icon;
StateColor text_color;
@@ -70,6 +71,8 @@ public:
void SetPaddingSize(const wxSize& size);
void SetIconSpacing(int spacing);
void SetStyle(const ButtonStyle style /*= ButtonStyle::Regular*/, const ButtonType type /*= ButtonType::None*/);
void SetTextColor(StateColor const& color);
+33 -13
View File
@@ -9,6 +9,7 @@
#include "../wxExtensions.hpp"
#include "Button.hpp"
#include "Label.hpp"
#include "ComboBox.hpp"
#include "StaticBox.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/Utils.hpp"
@@ -77,13 +78,16 @@ ManualNozzleCountDialog::ManualNozzleCountDialog(
wxPanel *content = new wxPanel(this);
content->SetBackgroundColour(*wxWHITE);
wxBitmap nozzle_bmp = create_scaled_bitmap("hotend_thumbnail", nullptr, FromDIP(60));
wxBitmap nozzle_bmp = create_scaled_bitmap("hotend_thumbnail", nullptr, 60);
auto *nozzle_icon = new wxStaticBitmap(content, wxID_ANY, nozzle_bmp);
wxBoxSizer *content_sizer = new wxBoxSizer(wxHORIZONTAL);
content->SetSizer(content_sizer);
wxBoxSizer *choice_sizer = new wxBoxSizer(wxVERTICAL);
choice_sizer->Add(new wxStaticText(content, wxID_ANY, _L("Please set nozzle count")), 0, wxALL | wxALIGN_LEFT, FromDIP(10));
auto nozzle_label = new wxStaticText(content, wxID_ANY, _L("Please set nozzle count"));
nozzle_label->SetFont(Label::Body_14);
nozzle_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#262E30")));
choice_sizer->Add(nozzle_label, 0, wxTOP | wxRIGHT, FromDIP(15));
wxArrayString nozzle_choices;
for (int i = 0; i <= max_nozzle_count; ++i)
@@ -92,16 +96,32 @@ ManualNozzleCountDialog::ManualNozzleCountDialog(
// A Hybrid extruder mixes Standard and High Flow nozzles, so it gets both count choices; the concrete
// types get exactly one.
if (volume_type == nvtStandard || volume_type == nvtHybrid) {
choice_sizer->Add(new wxStaticText(content, wxID_ANY, _L(get_nozzle_volume_type_string(nvtStandard))), 0, wxALL | wxALIGN_LEFT, FromDIP(5));
m_standard_choice = new wxChoice(content, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(100), -1), nozzle_choices);
wxBoxSizer *standard_sizer = new wxBoxSizer(wxHORIZONTAL);
auto standard_label = new wxStaticText(content, wxID_ANY, _L(get_nozzle_volume_type_string(nvtStandard)), wxDefaultPosition, wxSize(FromDIP(100), -1));
standard_label->SetFont(Label::Body_14);
standard_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636")));
standard_sizer->Add(standard_label, 0, wxALIGN_CENTER_VERTICAL);
m_standard_choice = new ComboBox(content, wxID_ANY, "", wxDefaultPosition, wxSize(FromDIP(80), -1), 0, nullptr, wxCB_READONLY);
std::vector<wxString>::iterator iter;
for (iter = nozzle_choices.begin(); iter != nozzle_choices.end(); iter++)
m_standard_choice->Append(*iter);
m_standard_choice->SetSelection(standard_count);
choice_sizer->Add(m_standard_choice, 0, wxLEFT | wxBOTTOM | wxRIGHT, FromDIP(10));
standard_sizer->Add(m_standard_choice, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5));
choice_sizer->Add(standard_sizer, 0, wxTOP | wxRIGHT, FromDIP(15));
}
if (volume_type == nvtHighFlow || volume_type == nvtHybrid) {
choice_sizer->Add(new wxStaticText(content, wxID_ANY, _L(get_nozzle_volume_type_string(nvtHighFlow))), 0, wxALL | wxALIGN_LEFT, FromDIP(5));
m_highflow_choice = new wxChoice(content, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(100), -1), nozzle_choices);
wxBoxSizer *highflow_sizer = new wxBoxSizer(wxHORIZONTAL);
auto highflow_label = new wxStaticText(content, wxID_ANY, _L(get_nozzle_volume_type_string(nvtHighFlow)), wxDefaultPosition, wxSize(FromDIP(100), -1));
highflow_label->SetFont(Label::Body_14);
highflow_label->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#363636")));
highflow_sizer->Add(highflow_label, 0, wxALIGN_CENTER_VERTICAL);
m_highflow_choice = new ComboBox(content, wxID_ANY, "", wxDefaultPosition, wxSize(FromDIP(80), -1), 0, nullptr, wxCB_READONLY);
std::vector<wxString>::iterator iter;
for (iter = nozzle_choices.begin(); iter != nozzle_choices.end(); iter++)
m_highflow_choice->Append(*iter);
m_highflow_choice->SetSelection(highflow_count);
choice_sizer->Add(m_highflow_choice, 0, wxLEFT | wxBOTTOM | wxRIGHT, FromDIP(10));
highflow_sizer->Add(m_highflow_choice, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(5));
choice_sizer->Add(highflow_sizer, 0, wxTOP | wxRIGHT, FromDIP(15));
}
m_error_label = new wxStaticText(this, wxID_ANY, "");
@@ -128,12 +148,12 @@ ManualNozzleCountDialog::ManualNozzleCountDialog(
};
if (m_standard_choice)
m_standard_choice->Bind(wxEVT_CHOICE, [this, update_nozzle_error](wxCommandEvent &e) {
m_standard_choice->Bind(wxEVT_COMBOBOX, [this, update_nozzle_error](wxCommandEvent &e) {
update_nozzle_error(m_standard_choice->GetSelection(), m_highflow_choice ? m_highflow_choice->GetSelection() : 0);
e.Skip();
});
if (m_highflow_choice)
m_highflow_choice->Bind(wxEVT_CHOICE, [this, update_nozzle_error](wxCommandEvent &e) {
m_highflow_choice->Bind(wxEVT_COMBOBOX, [this, update_nozzle_error](wxCommandEvent &e) {
update_nozzle_error(m_standard_choice ? m_standard_choice->GetSelection() : 0, m_highflow_choice->GetSelection());
e.Skip();
});
@@ -142,13 +162,13 @@ ManualNozzleCountDialog::ManualNozzleCountDialog(
content_sizer->Add(choice_sizer, 0, wxALIGN_CENTRE_VERTICAL);
m_confirm_btn = new Button(this, _L("Confirm"));
m_confirm_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Window);
m_confirm_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Choice);
m_confirm_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent &) { EndModal(wxID_OK); });
wxBoxSizer *main_sizer = new wxBoxSizer(wxVERTICAL);
main_sizer->Add(content, 1, wxEXPAND);
main_sizer->Add(m_error_label, 0, wxALL, FromDIP(5));
main_sizer->Add(m_confirm_btn, 0, wxALIGN_CENTER_HORIZONTAL | wxBOTTOM, FromDIP(20));
main_sizer->Add(m_error_label, 0, wxALIGN_RIGHT | wxALL, FromDIP(5));
main_sizer->Add(m_confirm_btn, 0, wxALIGN_RIGHT | wxALL, FromDIP(15));
SetSizerAndFit(main_sizer);
CentreOnParent();
+3 -3
View File
@@ -31,12 +31,12 @@
#include <unordered_map>
#include <vector>
class wxChoice;
class wxStaticText;
class wxStaticBitmap;
class Button; // global widget (src/slic3r/GUI/Widgets/Button.hpp), not in the Slic3r::GUI namespace
class Label; // global widget (src/slic3r/GUI/Widgets/Label.hpp)
class StaticBox;
class ComboBox;
namespace Slic3r {
class PresetBundle;
@@ -70,8 +70,8 @@ public:
int GetNozzleCount(NozzleVolumeType volume_type) const;
private:
wxChoice *m_standard_choice{nullptr};
wxChoice *m_highflow_choice{nullptr};
ComboBox *m_standard_choice{nullptr};
ComboBox *m_highflow_choice{nullptr};
Button *m_confirm_btn{nullptr};
wxStaticText *m_error_label{nullptr};
};
+10 -2
View File
@@ -53,6 +53,13 @@ void StaticBox::SetCornerRadius(double radius)
Refresh();
}
// ORCA use when adding widgets to top to show it like LabeledStaticBox
void StaticBox::SetTopMargin(int margin)
{
this->top_margin = margin;
Refresh();
}
void StaticBox::SetBorderStyle(wxPenStyle style)
{
border_style = style;
@@ -198,7 +205,8 @@ void StaticBox::doRender(wxDC& dc)
int states = state_handler.states();
if (background_color2.count() == 0) {
if ((border_width && border_color.count() > 0) || background_color.count() > 0) {
wxRect rc(0, 0, size.x, size.y);
int topM = top_margin > 0 ? top_margin : 0;
wxRect rc(0, topM, size.x, size.y - topM);
if (border_width && border_color.count() > 0) {
const double scale = dc.GetContentScaleFactor();
@@ -245,6 +253,6 @@ void StaticBox::doRender(wxDC& dc)
if (badge.bmp().IsOk()) {
auto s = badge.bmp().GetScaledSize();
dc.DrawBitmap(badge.bmp(), size.x - s.x, 0);
dc.DrawBitmap(badge.bmp(), size.x - s.x, top_margin > 0 ? top_margin : 0);
}
}
+3
View File
@@ -27,6 +27,8 @@ public:
void SetBorderWidth(int width);
void SetTopMargin(int margin); // ORCA
void SetBorderColor(StateColor const & color);
void SetBorderColorNormal(wxColor const &color);
@@ -57,6 +59,7 @@ protected:
protected:
double radius;
int border_width = 1;
int top_margin = 0;
wxPenStyle border_style = wxPENSTYLE_SOLID;
StateHandler state_handler;
StateColor border_color;
-28
View File
@@ -1,28 +0,0 @@
#include "StaticGroup.hpp"
StaticGroup::StaticGroup(wxWindow *parent, wxWindowID id, const wxString &label)
: LabeledStaticBox(parent, label)
{
SetBackgroundColour(*wxWHITE);
SetForegroundColour("#CECECE");
}
void StaticGroup::ShowBadge(bool show)
{
if (show && badge.name() != "badge") {
badge = ScalableBitmap(this, "badge", 18);
Refresh();
} else if (!show && !badge.name().empty()) {
badge = ScalableBitmap{};
Refresh();
}
}
void StaticGroup::DrawBorderAndLabel(wxDC& dc)
{
LabeledStaticBox::DrawBorderAndLabel(dc);
if (badge.bmp().IsOk()) {
auto s = badge.bmp().GetScaledSize();
dc.DrawBitmap(badge.bmp(), GetSize().x - s.x, std::max(0, m_pos.y) + m_label_height / 2);
}
}
-19
View File
@@ -1,19 +0,0 @@
#ifndef slic3r_GUI_StaticGroup_hpp_
#define slic3r_GUI_StaticGroup_hpp_
#include "../wxExtensions.hpp"
#include "LabeledStaticBox.hpp"
class StaticGroup : public LabeledStaticBox
{
public:
StaticGroup(wxWindow *parent, wxWindowID id, const wxString &label);
void ShowBadge(bool show);
private:
void DrawBorderAndLabel(wxDC& dc) override;
ScalableBitmap badge;
};
#endif // !slic3r_GUI_StaticGroup_hpp_
+2 -2
View File
@@ -275,9 +275,9 @@ void TempInput::Warning(bool warn, WarningType type)
wxString warning_string;
if (type == WarningType::WARNING_TOO_HIGH)
warning_string = _L("The maximum temperature cannot exceed ") + wxString::Format("%d", max_temp);
warning_string = wxString::Format(_L("The maximum temperature cannot exceed %d"), max_temp);
else if (type == WarningType::WARNING_TOO_LOW)
warning_string = _L("The minmum temperature should not be less than ") + wxString::Format("%d", min_temp);
warning_string = wxString::Format(_L("The minimum temperature should not be less than %d"), min_temp);
warning_text->SetLabel(warning_string);
warning_text->Wrap(-1);
warning_text->Fit();
+5 -13
View File
@@ -1,4 +1,5 @@
#include "CrealityHostDiscovery.hpp"
#include "CrealityPrint.hpp"
#include "cxmdns.h"
#include "Http.hpp"
@@ -11,25 +12,16 @@ namespace Slic3r {
namespace {
struct ModelEntry { const char* code; const char* name; };
constexpr ModelEntry kCfsCapableModels[] = {
{"F008", "K2 Plus"},
{"F012", "K2 Pro"},
{"F021", "K2"},
};
// Model capability/name lookups live in CrealityPrint (one table shared
// with the print host) so discovery can't drift out of sync again.
bool is_cfs_capable(const std::string& code)
{
for (const auto& m : kCfsCapableModels)
if (code == m.code) return true;
return false;
return CrealityPrint::model_supports_multi_color(code);
}
std::string model_name_for(const std::string& code)
{
for (const auto& m : kCfsCapableModels)
if (code == m.code) return m.name;
return {};
return CrealityPrint::model_display_name(code);
}
// Extract the device suffix from a service name like
+75 -27
View File
@@ -2,6 +2,7 @@
#include <algorithm>
#include <map>
#include <unordered_set>
#include <sstream>
#include <exception>
#include <boost/format.hpp>
@@ -217,29 +218,36 @@ static void ws_connect(net::io_context& ioc, websocket::stream<beast::tcp_stream
std::string(BOOST_BEAST_VERSION_STRING) + " websocket-client-coro");
}));
ws.handshake(host, "/");
#ifdef _WIN32
DWORD recv_timeout = 3000;
#else
struct timeval recv_timeout = {3, 0};
#endif
setsockopt(beast::get_lowest_layer(ws).socket().native_handle(),
SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&recv_timeout), sizeof(recv_timeout));
// K1-family firmware requires TEXT-mode WebSocket frames; K2-family accepts both.
// SO_RCVTIMEO is a no-op with Boost.Asio on macOS/Linux (kqueue/epoll); use
// expires_after() on the tcp_stream layer instead (managed per-call by the caller).
ws.text(true);
}
static std::string ws_send_and_read(websocket::stream<beast::tcp_stream>& ws, const json& cmd, const std::string& expected_key, int max_reads = 20)
{
ws.write(net::buffer(to_string(cmd)));
for (int i = 0; i < max_reads; i++) {
// total caps the loop if the printer sends a continuous stream of heartbeats
for (int reads = 0, total = 0; reads < max_reads && total < max_reads * 3; ++total) {
beast::flat_buffer buf;
beast::error_code ec;
ws.read(buf, ec);
if (ec == net::error::would_block)
// would_block = SO_RCVTIMEO fired (Windows/raw sockets)
// beast::error::timeout = expires_after() fired (Boost.Beast, macOS/Linux)
if (ec == net::error::would_block || ec == beast::error::timeout)
break;
if (ec)
throw beast::system_error{ec};
std::string msg = beast::buffers_to_string(buf.data());
// K1-family firmware sends periodic heartbeat pings; ack them so the
// printer does not close the connection, then keep reading.
if (msg.find("heart_beat") != std::string::npos) {
beast::error_code wr_ec;
ws.write(net::buffer(std::string("ok")), wr_ec);
continue;
}
++reads;
if (msg.find(expected_key) != std::string::npos)
return msg;
}
@@ -247,6 +255,19 @@ static std::string ws_send_and_read(websocket::stream<beast::tcp_stream>& ws, co
return {};
}
// K1-family firmware stores gcodes under /usr/data and uses a stricter WebSocket
// protocol (TEXT frames, heartbeat acks). Add verified K1-family model IDs here.
static bool is_k1_family(const std::string& model)
{
static const std::unordered_set<std::string> k1_models = {
"K1",
"K1 SE",
"K1C",
"K1_CFS-C",
};
return k1_models.count(model) > 0;
}
void CrealityPrint::query_model() const
{
if (!m_model.empty())
@@ -256,29 +277,48 @@ void CrealityPrint::query_model() const
test(msg);
}
bool CrealityPrint::supports_multi_color_print() const
// CFS-capable models. One table for capability checks, display names and
// LAN-discovery labelling -- keep additions here only.
static const std::map<std::string, std::string>& cfs_capable_models()
{
query_model();
// K2-platform printers with CFS support
return m_model == "F008" // K2 Plus
|| m_model == "F012" // K2 Pro
|| m_model == "F021" // K2
|| m_model == "F022"; // SPARKX i7
}
std::string CrealityPrint::model_name() const
{
static const std::map<std::string, std::string> names = {
static const std::map<std::string, std::string> models = {
{"F008", "K2 Plus"},
{"F012", "K2 Pro"},
{"F021", "K2"},
{"F022", "SPARKX i7"},
{"K1", "K1"},
{"K1 SE", "K1 SE"},
{"K1C", "K1C"},
{"K1_CFS-C", "K1_CFS-C"},
};
return models;
}
bool CrealityPrint::model_supports_multi_color(const std::string& model)
{
return cfs_capable_models().count(model) > 0;
}
std::string CrealityPrint::model_display_name(const std::string& model)
{
auto& names = cfs_capable_models();
auto it = names.find(model);
return it != names.end() ? it->second : std::string{};
}
bool CrealityPrint::supports_multi_color_print() const
{
query_model();
return model_supports_multi_color(m_model);
}
std::string CrealityPrint::model_name() const
{
query_model();
if (m_model.empty())
return "unreachable";
auto it = names.find(m_model);
return it != names.end() ? it->second : "unknown (" + m_model + ")";
std::string name = model_display_name(m_model);
return !name.empty() ? name : "unknown (" + m_model + ")";
}
std::string CrealityPrint::query_boxes_info() const
@@ -288,9 +328,15 @@ std::string CrealityPrint::query_boxes_info() const
websocket::stream<beast::tcp_stream> ws{ioc};
ws_connect(ioc, ws, m_host, "9999");
beast::get_lowest_layer(ws).expires_after(std::chrono::seconds(10));
json boxs_query = {{"method", "get"}, {"params", {{"boxsInfo", 1}}}};
std::string result = ws_send_and_read(ws, boxs_query, "boxsInfo");
ws.close(websocket::close_code::normal);
// K1 SE closes its side after responding; use the error_code overload so
// the resulting EOF does not throw and discard the already-received result.
beast::error_code close_ec;
beast::get_lowest_layer(ws).expires_after(std::chrono::seconds(3));
ws.close(websocket::close_code::normal, close_ec);
return result;
} catch (std::exception const& e) {
BOOST_LOG_TRIVIAL(error) << "CrealityPrint: Failed to query boxsInfo: " << e.what();
@@ -301,7 +347,8 @@ std::string CrealityPrint::query_boxes_info() const
bool CrealityPrint::start_print(wxString &msg, const std::string &filename, const std::map<std::string, std::string>& extended_info) const
{
try {
const std::string gcode_path = "/mnt/UDISK/printer_data/gcodes/" + filename;
const std::string data_root = is_k1_family(m_model) ? "/usr/data" : "/mnt/UDISK";
const std::string gcode_path = data_root + "/printer_data/gcodes/" + filename;
net::io_context ioc;
websocket::stream<beast::tcp_stream> ws{ioc};
@@ -379,7 +426,7 @@ bool CrealityPrint::start_print(wxString &msg, const std::string &filename, cons
json cmd = {
{"method", "set"},
{"params", {
{"opGcodeFile", "printprt:/usr/data/printer_data/gcodes/" + filename}
{"opGcodeFile", "printprt:" + gcode_path}
}}
};
ws.write(net::buffer(to_string(cmd)));
@@ -396,6 +443,7 @@ bool CrealityPrint::start_print(wxString &msg, const std::string &filename, cons
// Same reason: the printer may have already closed the connection. A close
// error here is not a failure — the start command was sent above.
beast::error_code close_ec;
beast::get_lowest_layer(ws).expires_after(std::chrono::seconds(3));
ws.close(websocket::close_code::normal, close_ec);
return true;
} catch(std::exception const& e) {
+6
View File
@@ -31,6 +31,12 @@ public:
PrintHostPostUploadActions get_post_upload_actions() const override;
bool upload(PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn) const override;
bool supports_multi_color_print() const;
// Single source of truth for the CFS-capable model table, shared with
// LAN discovery (CrealityHostDiscovery). Model is the /info "model"
// value: an F-code on the K2 platform, a literal name on K1-family.
static bool model_supports_multi_color(const std::string& model);
static std::string model_display_name(const std::string& model);
std::string query_boxes_info() const;
std::string model_name() const;
@@ -1,7 +1,6 @@
#pragma once
#include <functional>
#include <memory>
#include <string>
#include <vector>
+12 -1
View File
@@ -1,11 +1,11 @@
#ifndef __I_CLOUD_SERVICE_AGENT_HPP__
#define __I_CLOUD_SERVICE_AGENT_HPP__
#include "ICameraSignalingChannel.hpp"
#include "bambu_networking.hpp"
#include "CloudProvider.hpp"
#include "../../libslic3r/ProjectTask.hpp"
#include <string>
#include <string_view>
#include <map>
#include <vector>
#include <functional>
@@ -328,6 +328,17 @@ public:
*/
virtual int get_camera_url(std::string dev_id, std::function<void(std::string)> callback) = 0;
/**
* Create a camera signaling channel object for P2P camera streaming over WebRTC.
*
*/
virtual std::unique_ptr<ICameraSignalingChannel>
create_camera_signaling_channel(const std::string& dev_id)
{
(void) dev_id;
return nullptr;
}
/**
* Fetch staff-picked designs from model mall.
*/
-9
View File
@@ -445,15 +445,6 @@ public:
* Only meaningful when get_camera_stream_mode() returns an HTTP, HTTPS, or RTSP mode.
*/
virtual std::string get_camera_url() const { return {}; }
// Optional native camera signaling. Plugin agents retain the default
// nullptr until a plugin-facing WebRTC contract is defined.
virtual std::unique_ptr<ICameraSignalingChannel>
create_camera_signaling_channel(const std::string& dev_id)
{
(void) dev_id;
return nullptr;
}
};
} // namespace Slic3r

Some files were not shown because too many files have changed in this diff Show More