mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 02:41:17 +00:00
Merge branch 'feat/printer-agent-infra' into feat/printer-agent-impl
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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_
|
||||
@@ -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_
|
||||
@@ -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
@@ -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
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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" ||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
+659
-154
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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__
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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_
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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); }
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
@@ -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(¬_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(¬_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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
|
||||
@@ -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_
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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};
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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_
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user