mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-10 09:11:08 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3071d525ee |
@@ -396,6 +396,8 @@ public:
|
|||||||
Slic3r::TaskManager* getTaskManager() { return m_task_manager; }
|
Slic3r::TaskManager* getTaskManager() { return m_task_manager; }
|
||||||
HMSQuery* get_hms_query() { return hms_query; }
|
HMSQuery* get_hms_query() { return hms_query; }
|
||||||
NetworkAgent* getAgent() { return m_agent; }
|
NetworkAgent* getAgent() { return m_agent; }
|
||||||
|
// Version that wrote the app config before this run; empty when there was no app config.
|
||||||
|
const boost::optional<Semver>& last_config_version() const { return m_last_config_version; }
|
||||||
|
|
||||||
// Reconcile the live printer agent with the stored preset selection.
|
// Reconcile the live printer agent with the stored preset selection.
|
||||||
void switch_printer_agent();
|
void switch_printer_agent();
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ public:
|
|||||||
const std::string& get_vendor() const;
|
const std::string& get_vendor() const;
|
||||||
const std::string& get_renderer() const;
|
const std::string& get_renderer() const;
|
||||||
|
|
||||||
|
// False until a GL context has been made current and queried; the getters below detect
|
||||||
|
// on first use, which needs a current context.
|
||||||
|
bool is_detected() const { return m_detected; }
|
||||||
bool is_core_profile() const { return m_core_profile; }
|
bool is_core_profile() const { return m_core_profile; }
|
||||||
|
|
||||||
bool is_mesa() const;
|
bool is_mesa() const;
|
||||||
|
|||||||
+51
-11
@@ -19318,6 +19318,38 @@ void Plater::send_gcode_finish(wxString name)
|
|||||||
auto out_str = GUI::format(_L("The file %s has been sent to the printer's storage space and can be viewed on the printer."), name);
|
auto out_str = GUI::format(_L("The file %s has been sent to the printer's storage space and can be viewed on the printer."), name);
|
||||||
p->notification_manager->push_exporting_finished_notification(out_str, "", false);
|
p->notification_manager->push_exporting_finished_notification(out_str, "", false);
|
||||||
}
|
}
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// export_3mf() assigns archive paths to previously unsaved SVGs. Puts them back, so an export that
|
||||||
|
// is not a project save does not change what a later project save writes.
|
||||||
|
class SvgArchivePathsRestorer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit SvgArchivePathsRestorer(Model& model)
|
||||||
|
{
|
||||||
|
for (ModelObject* object : model.objects)
|
||||||
|
for (ModelVolume* volume : object->volumes)
|
||||||
|
if (volume != nullptr && volume->emboss_shape.has_value() && volume->emboss_shape->svg_file.has_value()) {
|
||||||
|
std::string* path_in_3mf = &volume->emboss_shape->svg_file->path_in_3mf;
|
||||||
|
m_paths.emplace_back(path_in_3mf, *path_in_3mf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
~SvgArchivePathsRestorer() { restore(); }
|
||||||
|
SvgArchivePathsRestorer(const SvgArchivePathsRestorer&) = delete;
|
||||||
|
SvgArchivePathsRestorer& operator=(const SvgArchivePathsRestorer&) = delete;
|
||||||
|
|
||||||
|
void restore()
|
||||||
|
{
|
||||||
|
for (const auto& [path_in_3mf, previous_path] : m_paths)
|
||||||
|
*path_in_3mf = previous_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<std::pair<std::string*, std::string>> m_paths;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void Plater::export_core_3mf()
|
void Plater::export_core_3mf()
|
||||||
{
|
{
|
||||||
wxString path = p->get_export_file(FT_3MF);
|
wxString path = p->get_export_file(FT_3MF);
|
||||||
@@ -19326,6 +19358,23 @@ void Plater::export_core_3mf()
|
|||||||
export_3mf(path_u8, SaveStrategy::Silence);
|
export_3mf(path_u8, SaveStrategy::Silence);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Plater::export_3mf_copy(const boost::filesystem::path& output_path)
|
||||||
|
{
|
||||||
|
Model& model = p->model;
|
||||||
|
SvgArchivePathsRestorer svg_paths(model);
|
||||||
|
|
||||||
|
// With no design info, export_3mf() writes the signed-in account's user id as the designer; an
|
||||||
|
// empty one keeps it out. export_3mf() also drops design info without a designer name afterwards.
|
||||||
|
const std::shared_ptr<ModelDesignInfo> design_info = model.design_info;
|
||||||
|
if (design_info == nullptr)
|
||||||
|
model.design_info = std::make_shared<ModelDesignInfo>();
|
||||||
|
ScopeGuard restore_design_info([&model, design_info]() { model.design_info = design_info; });
|
||||||
|
|
||||||
|
// The project save's layout, plus Silence so the project file name stays as it is. Unlike a save
|
||||||
|
// it never adds FullPathSources: a copy is for sharing, and those are paths on this machine.
|
||||||
|
return export_3mf(output_path, SaveStrategy::SplitModel | SaveStrategy::ShareMesh | SaveStrategy::Silence) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Export the current project as a "published" 3MF: a pure export that never touches the
|
// Export the current project as a "published" 3MF: a pure export that never touches the
|
||||||
// project's file name, dirty state, backup path or title, and attaches the published metadata
|
// project's file name, dirty state, backup path or title, and attaches the published metadata
|
||||||
// to the model only for the duration of the export (a later Save Project is a normal 3MF).
|
// to the model only for the duration of the export (a later Save Project is a normal 3MF).
|
||||||
@@ -19375,19 +19424,10 @@ int Plater::export_published_3mf(const std::vector<std::string>& published_keys,
|
|||||||
std::string();
|
std::string();
|
||||||
const std::string prev_payload = had_payload ? model.model_info->metadata_items.at(ORCA_PUBLISHED_CONFIG_TAG) : std::string();
|
const std::string prev_payload = had_payload ? model.model_info->metadata_items.at(ORCA_PUBLISHED_CONFIG_TAG) : std::string();
|
||||||
|
|
||||||
// export_3mf() assigns archive paths to previously unsaved SVGs. Preserve those fields too,
|
SvgArchivePathsRestorer svg_paths(model);
|
||||||
// otherwise a publish changes what a later normal project save writes.
|
|
||||||
std::vector<std::pair<std::string*, std::string>> previous_svg_paths;
|
|
||||||
for (ModelObject* object : model.objects)
|
|
||||||
for (ModelVolume* volume : object->volumes)
|
|
||||||
if (volume != nullptr && volume->emboss_shape.has_value() && volume->emboss_shape->svg_file.has_value()) {
|
|
||||||
std::string* path_in_3mf = &volume->emboss_shape->svg_file->path_in_3mf;
|
|
||||||
previous_svg_paths.emplace_back(path_in_3mf, *path_in_3mf);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto restore_temporary_state = [&]() {
|
auto restore_temporary_state = [&]() {
|
||||||
for (const auto& [path_in_3mf, previous_path] : previous_svg_paths)
|
svg_paths.restore();
|
||||||
*path_in_3mf = previous_path;
|
|
||||||
|
|
||||||
if (!had_model_info) {
|
if (!had_model_info) {
|
||||||
model.model_info = nullptr;
|
model.model_info = nullptr;
|
||||||
|
|||||||
@@ -565,6 +565,9 @@ public:
|
|||||||
void export_gcode_3mf(bool export_all = false);
|
void export_gcode_3mf(bool export_all = false);
|
||||||
void send_gcode_finish(wxString name);
|
void send_gcode_finish(wxString name);
|
||||||
void export_core_3mf();
|
void export_core_3mf();
|
||||||
|
// Write the project to output_path as a copy: the project's file name, dirty state and model stay
|
||||||
|
// as they are, and the signed-in account is not added as the designer. False when the write fails.
|
||||||
|
bool export_3mf_copy(const boost::filesystem::path& output_path);
|
||||||
// Export a "published" 3MF embedding the author-selected settings in the file metadata; a
|
// Export a "published" 3MF embedding the author-selected settings in the file metadata; a
|
||||||
// pure export that leaves the in-memory project untouched.
|
// pure export that leaves the in-memory project untouched.
|
||||||
int export_published_3mf(const std::vector<std::string>& published_keys, const std::vector<Slic3r::PublishedMaterialEntry>& material_keys);
|
int export_published_3mf(const std::vector<std::string>& published_keys, const std::vector<Slic3r::PublishedMaterialEntry>& material_keys);
|
||||||
|
|||||||
@@ -1,16 +1,36 @@
|
|||||||
#include "PluginHostBindings.hpp"
|
#include "PluginHostBindings.hpp"
|
||||||
|
#include <pybind11/cast.h>
|
||||||
#include <pybind11/pybind11.h>
|
#include <pybind11/pybind11.h>
|
||||||
|
#include <pybind11/pytypes.h>
|
||||||
|
|
||||||
#include <libslic3r/Model.hpp>
|
#include <libslic3r/Model.hpp>
|
||||||
#include <libslic3r/PresetBundle.hpp>
|
#include <libslic3r/PresetBundle.hpp>
|
||||||
|
#include <libslic3r/AppConfig.hpp>
|
||||||
|
#include <libslic3r/Semver.hpp>
|
||||||
|
#include <libslic3r_version.h>
|
||||||
|
#include <slic3r/GUI/BuildCommit.hpp>
|
||||||
|
#include <slic3r/GUI/DeviceCore/DevManager.h>
|
||||||
|
#include <slic3r/GUI/DeviceManager.hpp>
|
||||||
#include <slic3r/GUI/GUI.hpp>
|
#include <slic3r/GUI/GUI.hpp>
|
||||||
#include <slic3r/GUI/GUI_App.hpp>
|
#include <slic3r/GUI/GUI_App.hpp>
|
||||||
|
#include <slic3r/GUI/OpenGLManager.hpp>
|
||||||
#include <slic3r/GUI/Plater.hpp>
|
#include <slic3r/GUI/Plater.hpp>
|
||||||
|
#include <slic3r/Utils/CloudProvider.hpp>
|
||||||
|
#include <slic3r/Utils/NetworkAgent.hpp>
|
||||||
|
#include <slic3r/Utils/NetworkAgentFactory.hpp>
|
||||||
|
#include <slic3r/Utils/bambu_networking.hpp>
|
||||||
|
|
||||||
|
#include <boost/algorithm/string/predicate.hpp>
|
||||||
|
#include <boost/filesystem/path.hpp>
|
||||||
|
#include <boost/optional/optional.hpp>
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <wx/app.h>
|
#include <wx/app.h>
|
||||||
|
#include <wx/string.h>
|
||||||
|
#include <wx/thread.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
namespace py = pybind11;
|
namespace py = pybind11;
|
||||||
|
|
||||||
@@ -41,6 +61,94 @@ PresetBundle* current_preset_bundle()
|
|||||||
return preset_bundle;
|
return preset_bundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GUI::GUI_App& current_app()
|
||||||
|
{
|
||||||
|
if (wxTheApp == nullptr)
|
||||||
|
throw std::runtime_error("OrcaSlicer application is not initialized");
|
||||||
|
return GUI::wxGetApp();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The plater and the device list are only safe to touch from the UI thread, and the 3MF export
|
||||||
|
// renders thumbnails, which needs the UI thread's GL context.
|
||||||
|
void require_main_thread(const char* function_name)
|
||||||
|
{
|
||||||
|
if (!wxIsMainThread())
|
||||||
|
throw std::runtime_error(std::string(function_name) + "() must be called from the UI thread");
|
||||||
|
}
|
||||||
|
|
||||||
|
void export_3mf_copy(GUI::Plater& plater, const std::string& path)
|
||||||
|
{
|
||||||
|
require_main_thread("export_3mf_copy");
|
||||||
|
if (!boost::iends_with(path, ".3mf"))
|
||||||
|
throw py::value_error("export_3mf_copy() needs a path ending in .3mf");
|
||||||
|
// The thumbnails are rendered by the 3D view, which may not exist yet while plugins load.
|
||||||
|
if (!GUI::OpenGLManager::get_gl_info().is_detected())
|
||||||
|
throw std::runtime_error("export_3mf_copy() needs the 3D view, which is not ready yet");
|
||||||
|
// The file is written from C++, so raise the audit event Python's open(path, "w") raises: the
|
||||||
|
// plugin gets the same deny list, permissions and prompt as for writing the file itself.
|
||||||
|
if (PySys_Audit("open", "ssi", path.c_str(), "w", 0) < 0)
|
||||||
|
throw py::error_already_set();
|
||||||
|
if (!plater.export_3mf_copy(GUI::into_path(GUI::from_u8(path))))
|
||||||
|
throw std::runtime_error("Failed to write " + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
py::object gl_info()
|
||||||
|
{
|
||||||
|
const GUI::OpenGLManager::GLInfo& info = GUI::OpenGLManager::get_gl_info();
|
||||||
|
if (!info.is_detected())
|
||||||
|
return py::none();
|
||||||
|
py::dict out;
|
||||||
|
out["vendor"] = info.get_vendor();
|
||||||
|
out["renderer"] = info.get_renderer();
|
||||||
|
out["version"] = info.get_version();
|
||||||
|
out["glsl_version"] = info.get_glsl_version();
|
||||||
|
out["core_profile"] = info.is_core_profile();
|
||||||
|
return std::move(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
py::dict app_info()
|
||||||
|
{
|
||||||
|
GUI::GUI_App& app = current_app();
|
||||||
|
// The app config has no lock of its own; the UI thread is where it is written.
|
||||||
|
require_main_thread("app_info");
|
||||||
|
py::dict out;
|
||||||
|
out["version"] = SoftFever_VERSION;
|
||||||
|
out["build"] = GUI::build_commit_label;
|
||||||
|
out["mode"] = app.is_editor() ? "editor" : "gcode viewer";
|
||||||
|
out["language"] = GUI::into_u8(app.current_language_code_safe());
|
||||||
|
// The app config file is deny-listed for plugins; these are the parts a bug report needs.
|
||||||
|
const boost::optional<Semver>& config_version = app.last_config_version();
|
||||||
|
out["app_config_version"] = config_version && config_version->valid() ? config_version->to_string_sf() : std::string();
|
||||||
|
out["stealth_mode"] = app.app_config != nullptr && app.app_config->get_stealth_mode();
|
||||||
|
out["is_signed_in"] = app.is_user_login(ORCA_CLOUD_PROVIDER);
|
||||||
|
out["is_bambu_signed_in"] = app.is_user_login(BBL_CLOUD_PROVIDER);
|
||||||
|
out["network_plugin_version"] = NetworkAgent::is_network_module_loaded() ? NetworkAgent::get_version() : std::string();
|
||||||
|
const NetworkLibraryLoadError load_error = NetworkAgent::get_load_error();
|
||||||
|
out["network_plugin_error"] = load_error.has_error ? load_error.message : std::string();
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
py::object selected_printer()
|
||||||
|
{
|
||||||
|
GUI::GUI_App& app = current_app();
|
||||||
|
require_main_thread("selected_printer");
|
||||||
|
DeviceManager* devices = app.getDeviceManager();
|
||||||
|
MachineObject* machine = devices != nullptr ? devices->get_selected_machine() : nullptr;
|
||||||
|
if (machine == nullptr)
|
||||||
|
return py::none();
|
||||||
|
// Agent, model, link and firmware only: no name, serial, address or access code. Other agents
|
||||||
|
// fill the model and firmware with placeholders, so those come from Bambu printers only.
|
||||||
|
const bool bambu = machine->printer_agent_id == BBL_PRINTER_AGENT_ID;
|
||||||
|
py::dict out;
|
||||||
|
out["agent"] = machine->printer_agent_id;
|
||||||
|
out["model_id"] = bambu ? machine->printer_type : std::string();
|
||||||
|
out["connection"] = machine->connection_type();
|
||||||
|
out["online"] = machine->is_online();
|
||||||
|
out["connected"] = machine->is_connected();
|
||||||
|
out["firmware"] = bambu ? machine->get_ota_version() : std::string();
|
||||||
|
return std::move(out);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// Access to the live GUI application: the Plater and the module-level
|
// Access to the live GUI application: the Plater and the module-level
|
||||||
@@ -52,7 +160,15 @@ void host_bindings::register_app(py::module_& host)
|
|||||||
.def("model", static_cast<Model& (GUI::Plater::*)()>(&GUI::Plater::model), py::return_value_policy::reference_internal)
|
.def("model", static_cast<Model& (GUI::Plater::*)()>(&GUI::Plater::model), py::return_value_policy::reference_internal)
|
||||||
.def("is_project_dirty", &GUI::Plater::is_project_dirty)
|
.def("is_project_dirty", &GUI::Plater::is_project_dirty)
|
||||||
.def("is_presets_dirty", &GUI::Plater::is_presets_dirty)
|
.def("is_presets_dirty", &GUI::Plater::is_presets_dirty)
|
||||||
.def("inside_snapshot_capture", &GUI::Plater::inside_snapshot_capture);
|
.def("inside_snapshot_capture", &GUI::Plater::inside_snapshot_capture)
|
||||||
|
// Path the project was last opened from or saved to; empty while it has never been saved.
|
||||||
|
.def("project_path", [](GUI::Plater& plater) {
|
||||||
|
require_main_thread("project_path");
|
||||||
|
return GUI::into_u8(plater.get_project_filename(".3mf"));
|
||||||
|
})
|
||||||
|
.def("export_3mf_copy", &export_3mf_copy, py::arg("path"),
|
||||||
|
"Write the current project, unsaved changes included, to path as a 3MF copy. The project's "
|
||||||
|
"file name and saved state are not changed, and the signed-in account is not written as the designer.");
|
||||||
|
|
||||||
host.def("plater", ¤t_plater, py::return_value_policy::reference);
|
host.def("plater", ¤t_plater, py::return_value_policy::reference);
|
||||||
host.def("model", []() -> Model& {
|
host.def("model", []() -> Model& {
|
||||||
@@ -68,6 +184,16 @@ void host_bindings::register_app(py::module_& host)
|
|||||||
throw std::runtime_error("OrcaSlicer application is not initialized");
|
throw std::runtime_error("OrcaSlicer application is not initialized");
|
||||||
return GUI::into_u8(GUI::wxGetApp().current_language_code_safe());
|
return GUI::into_u8(GUI::wxGetApp().current_language_code_safe());
|
||||||
});
|
});
|
||||||
|
host.def("app_info", &app_info,
|
||||||
|
"The running app: version, build, mode, language, app_config_version (the version that wrote the app config "
|
||||||
|
"before this run, empty when there was none), stealth_mode, is_signed_in (Orca Cloud), is_bambu_signed_in (Bambu Cloud), network_plugin_version (empty "
|
||||||
|
"when the Bambu network plugin is not loaded) and network_plugin_error (empty unless it failed to load).");
|
||||||
|
host.def("gl_info", &gl_info,
|
||||||
|
"OpenGL vendor, renderer, version, glsl_version and core_profile of the 3D view; None before it has been created.");
|
||||||
|
host.def("selected_printer", &selected_printer,
|
||||||
|
"Agent, model_id, connection ('lan' or 'cloud'), online, connected and firmware of the printer selected in the "
|
||||||
|
"device list; model_id and firmware are empty for non-Bambu agents. None when none is selected. Print hosts "
|
||||||
|
"configured in the printer preset (OctoPrint and the like) are not in the device list.");
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Slic3r
|
} // namespace Slic3r
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ TEST_CASE("Plugin host API reports unavailable GUI objects before Orca app initi
|
|||||||
{
|
{
|
||||||
py::object host = import_orca_module().attr("host");
|
py::object host = import_orca_module().attr("host");
|
||||||
|
|
||||||
for (const char* function_name : { "preset_bundle", "plater", "model" }) {
|
for (const char* function_name : { "preset_bundle", "plater", "model", "app_info", "selected_printer" }) {
|
||||||
CAPTURE(function_name);
|
CAPTURE(function_name);
|
||||||
try {
|
try {
|
||||||
host.attr(function_name)();
|
host.attr(function_name)();
|
||||||
@@ -145,6 +145,17 @@ TEST_CASE("Plugin host API reports unavailable GUI objects before Orca app initi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Plugin host API reports no GL details before the 3D view exists", "[PluginHost][Python]")
|
||||||
|
{
|
||||||
|
py::object host = import_orca_module().attr("host");
|
||||||
|
|
||||||
|
CHECK(host.attr("gl_info")().is_none());
|
||||||
|
|
||||||
|
py::object plater_type = host.attr("Plater");
|
||||||
|
CHECK(has_attr(plater_type, "project_path"));
|
||||||
|
CHECK(has_attr(plater_type, "export_3mf_copy"));
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("Plugin host API exposes the UI module and guards it before Orca app initialization", "[PluginHost][Python]")
|
TEST_CASE("Plugin host API exposes the UI module and guards it before Orca app initialization", "[PluginHost][Python]")
|
||||||
{
|
{
|
||||||
py::object host = import_orca_module().attr("host");
|
py::object host = import_orca_module().attr("host");
|
||||||
|
|||||||
Reference in New Issue
Block a user