mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-26 20:32:06 +00:00
merge
This commit is contained in:
@@ -596,10 +596,19 @@ set(SLIC3R_GUI_SOURCES
|
||||
plugin/PythonPluginBridge.hpp
|
||||
plugin/PythonPluginInterface.hpp
|
||||
plugin/PyPluginPackage.hpp
|
||||
plugin/PluginHostApi.cpp
|
||||
plugin/PluginHostApi.hpp
|
||||
plugin/PluginHostUi.cpp
|
||||
plugin/PluginHostUi.hpp
|
||||
plugin/PluginBindingUtils.hpp
|
||||
plugin/host/PluginHost.cpp
|
||||
plugin/host/PluginHost.hpp
|
||||
plugin/host/PluginHostBindings.hpp
|
||||
plugin/host/PluginHostApp.cpp
|
||||
plugin/host/PluginHostGeometry.cpp
|
||||
plugin/host/PluginHostMesh.cpp
|
||||
plugin/host/PluginHostMesh.hpp
|
||||
plugin/host/PluginHostModel.cpp
|
||||
plugin/host/PluginHostPresets.cpp
|
||||
plugin/host/PluginHostSlicing.cpp
|
||||
plugin/host/PluginHostUi.cpp
|
||||
plugin/host/PluginHostUi.hpp
|
||||
plugin/CloudPluginService.cpp
|
||||
plugin/CloudPluginService.hpp
|
||||
plugin/PluginFsUtils.cpp
|
||||
@@ -612,21 +621,23 @@ set(SLIC3R_GUI_SOURCES
|
||||
plugin/PluginLoader.cpp
|
||||
plugin/PluginLoader.hpp
|
||||
plugin/PluginDescriptor.hpp
|
||||
plugin/PluginHooks.cpp
|
||||
plugin/PluginHooks.hpp
|
||||
plugin/PluginManager.cpp
|
||||
plugin/PluginManager.hpp
|
||||
plugin/PluginAuditManager.cpp
|
||||
plugin/PluginAuditManager.hpp
|
||||
plugin/PluginResolver.cpp
|
||||
plugin/PluginResolver.hpp
|
||||
plugin/pluginTypes/gcode/GCodePluginCapability.hpp
|
||||
plugin/pluginTypes/gcode/GCodePluginCapability.cpp
|
||||
plugin/pluginTypes/gcode/GCodePluginCapabilityTrampoline.hpp
|
||||
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp
|
||||
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp
|
||||
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapabilityTrampoline.hpp
|
||||
plugin/pluginTypes/script/ScriptPluginCapability.hpp
|
||||
plugin/pluginTypes/script/ScriptPluginCapability.cpp
|
||||
plugin/pluginTypes/script/ScriptPluginCapabilityTrampoline.hpp
|
||||
plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp
|
||||
plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp
|
||||
plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapabilityTrampoline.hpp
|
||||
pchheader.cpp
|
||||
pchheader.hpp
|
||||
Utils/ASCIIFolding.cpp
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
#include "libslic3r/Utils.hpp"
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
#include "slic3r/plugin/PluginHostUi.hpp"
|
||||
#include "slic3r/plugin/host/PluginHostUi.hpp"
|
||||
#include "slic3r/plugin/PythonInterpreter.hpp"
|
||||
|
||||
#include "GUI.hpp"
|
||||
@@ -2700,6 +2700,54 @@ std::string get_system_info()
|
||||
return out.str();
|
||||
}
|
||||
|
||||
// wx/app-level plugin wiring, kept in one place: subscriptions to plugin
|
||||
// loader events that drive GUI policy (plugins dialog refresh, network-agent
|
||||
// registration, plate revalidation). The libslic3r dispatch hooks are NOT
|
||||
// wired here -- PluginManager::initialize() installs those via
|
||||
// plugin_hooks::install().
|
||||
void GUI_App::init_plugin_gui_wiring()
|
||||
{
|
||||
PluginManager& plugin_mgr = PluginManager::instance();
|
||||
|
||||
auto refresh_plugins_dialog = [] {
|
||||
if (!wxTheApp)
|
||||
return;
|
||||
|
||||
GUI_App* app = &GUI::wxGetApp();
|
||||
if (app->is_closing())
|
||||
return;
|
||||
|
||||
app->CallAfter([app] {
|
||||
if (!app->is_closing() && app->m_plugins_dlg)
|
||||
app->m_plugins_dlg->update_plugin_dialog_ui();
|
||||
});
|
||||
};
|
||||
|
||||
plugin_mgr.get_loader().subscribe_on_load_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
|
||||
plugin_mgr.get_loader().subscribe_on_unload_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
|
||||
plugin_mgr.get_loader().subscribe_on_load_callback(NetworkAgentFactory::register_python_plugin);
|
||||
plugin_mgr.get_loader().subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin);
|
||||
plugin_mgr.get_loader().subscribe_on_capability_load_callback(
|
||||
[refresh_plugins_dialog](const PluginCapabilityIdentifier& capability) {
|
||||
if (capability.type == PluginCapabilityType::PrinterConnection)
|
||||
NetworkAgentFactory::register_python_printer_agent(capability.plugin_key, capability.name);
|
||||
refresh_plugins_dialog();
|
||||
// A newly loaded capability may satisfy a missing-plugin notification; re-validate the
|
||||
// current plate (on the UI thread) so the notification clears once its plugin is available.
|
||||
if (wxTheApp && !wxGetApp().is_closing())
|
||||
wxGetApp().CallAfter([]() {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->revalidate_current_plate_if_plugins_missing();
|
||||
});
|
||||
});
|
||||
plugin_mgr.get_loader().subscribe_on_capability_unload_callback(
|
||||
[refresh_plugins_dialog](const PluginCapabilityIdentifier& capability) {
|
||||
if (capability.type == PluginCapabilityType::PrinterConnection)
|
||||
NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name);
|
||||
refresh_plugins_dialog();
|
||||
});
|
||||
}
|
||||
|
||||
bool GUI_App::on_init_inner()
|
||||
{
|
||||
wxLog::SetActiveTarget(new wxBoostLog());
|
||||
@@ -3103,25 +3151,12 @@ bool GUI_App::on_init_inner()
|
||||
on_init_network();
|
||||
|
||||
// Initialize plugins after network then register on_load callbacks so once the plugin loads finish, it gets registered automatically.
|
||||
// initialize() also installs the libslic3r hooks (capability resolver,
|
||||
// slicing-pipeline dispatcher) via plugin_hooks::install() -- no
|
||||
// per-capability wiring belongs here.
|
||||
PluginManager& plugin_mgr = PluginManager::instance();
|
||||
plugin_mgr.initialize();
|
||||
|
||||
ConfigBase::set_resolve_capability_fn([&plugin_mgr](const std::string& cap_name, const std::string& cap_type) {
|
||||
auto plugin_cap = plugin_mgr.get_loader().try_get_plugin_capability_by_name_and_type(cap_name, plugin_capability_type_from_string(cap_type));
|
||||
if (!plugin_cap)
|
||||
return std::string();
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
if (!plugin_mgr.get_catalog().try_get_plugin_descriptor(plugin_cap->plugin_key, descriptor))
|
||||
return std::string();
|
||||
|
||||
// Cloud plugins are resolved at runtime via the UUID in the middle field, so the first
|
||||
// field keeps the friendly display name. Local plugins are looked up by plugin_key (the
|
||||
// first field, with an empty UUID), so emit the plugin_key to keep them resolvable.
|
||||
const std::string identity = descriptor.is_cloud_plugin() ? descriptor.name : descriptor.plugin_key;
|
||||
return identity + ';' + descriptor.cloud_uuid() + ';' + cap_name;
|
||||
});
|
||||
|
||||
// Set cloud plugin directory from previous session so cloud-installed
|
||||
// plugins are discovered even before the network agent is ready.
|
||||
const std::string preset_folder = app_config->get("preset_folder");
|
||||
@@ -3132,43 +3167,7 @@ bool GUI_App::on_init_inner()
|
||||
|
||||
plugin_mgr.discover_plugins(false, true);
|
||||
|
||||
auto refresh_plugins_dialog = [] {
|
||||
if (!wxTheApp)
|
||||
return;
|
||||
|
||||
GUI_App* app = &GUI::wxGetApp();
|
||||
if (app->is_closing())
|
||||
return;
|
||||
|
||||
app->CallAfter([app] {
|
||||
if (!app->is_closing() && app->m_plugins_dlg)
|
||||
app->m_plugins_dlg->update_plugin_dialog_ui();
|
||||
});
|
||||
};
|
||||
|
||||
plugin_mgr.get_loader().subscribe_on_load_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
|
||||
plugin_mgr.get_loader().subscribe_on_unload_callback([refresh_plugins_dialog](const std::string&) { refresh_plugins_dialog(); });
|
||||
plugin_mgr.get_loader().subscribe_on_load_callback(NetworkAgentFactory::register_python_plugin);
|
||||
plugin_mgr.get_loader().subscribe_on_unload_callback(NetworkAgentFactory::deregister_python_plugin);
|
||||
plugin_mgr.get_loader().subscribe_on_capability_load_callback(
|
||||
[refresh_plugins_dialog](const PluginCapabilityIdentifier& capability) {
|
||||
if (capability.type == PluginCapabilityType::PrinterConnection)
|
||||
NetworkAgentFactory::register_python_printer_agent(capability.plugin_key, capability.name);
|
||||
refresh_plugins_dialog();
|
||||
// A newly loaded capability may satisfy a missing-plugin notification; re-validate the
|
||||
// current plate (on the UI thread) so the notification clears once its plugin is available.
|
||||
if (wxTheApp && !wxGetApp().is_closing())
|
||||
wxGetApp().CallAfter([]() {
|
||||
if (Plater* plater = wxGetApp().plater())
|
||||
plater->revalidate_current_plate_if_plugins_missing();
|
||||
});
|
||||
});
|
||||
plugin_mgr.get_loader().subscribe_on_capability_unload_callback(
|
||||
[refresh_plugins_dialog](const PluginCapabilityIdentifier& capability) {
|
||||
if (capability.type == PluginCapabilityType::PrinterConnection)
|
||||
NetworkAgentFactory::deregister_python_printer_agent(capability.plugin_key, capability.name);
|
||||
refresh_plugins_dialog();
|
||||
});
|
||||
init_plugin_gui_wiring();
|
||||
|
||||
for (const std::string& plugin_key : plugin_mgr.get_catalog().get_enabled_plugin_keys()) {
|
||||
if (!plugin_mgr.get_loader().is_plugin_loaded(plugin_key)) {
|
||||
|
||||
@@ -772,6 +772,9 @@ private:
|
||||
bool on_init_network(bool try_backup = false);
|
||||
void init_networking_callbacks();
|
||||
void init_app_config();
|
||||
// GUI-side subscriptions to plugin loader events (dialog refresh,
|
||||
// network-agent registration, plate revalidation).
|
||||
void init_plugin_gui_wiring();
|
||||
void remove_old_networking_plugins();
|
||||
void drain_pending_events(int timeout_ms);
|
||||
bool wait_for_network_idle(int timeout_ms);
|
||||
|
||||
@@ -1217,9 +1217,9 @@ void PluginsDialog::run_script_plugin(const std::string& plugin_key, const std::
|
||||
// ModelObject*/ModelVolume*/ModelInstance* aliases into host data and can mint ObjectIDs,
|
||||
// which libslic3r requires on the main thread (ObjectID.hpp's non-atomic s_last_id). Running
|
||||
// here makes those reads/instantiations legal and means nothing mutates the model underneath
|
||||
// a run. The trade-off is that a slow execute() freezes the UI: the contract (see
|
||||
// plugin_development.md) is to keep execute() quick and offload heavy work to the plugin's own
|
||||
// threading.Thread. orca.host.ui calls already no-op their main-thread marshaling here.
|
||||
// a run. The trade-off is that a slow execute() freezes the UI, so the contract is to keep
|
||||
// execute() quick and offload heavy work to the plugin's own threading.Thread. orca.host.ui
|
||||
// calls already no-op their main-thread marshaling here.
|
||||
{
|
||||
wxBusyCursor busy;
|
||||
try {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
// file lives in the GUI layer (libslic3r must not depend on pybind11 / PluginManager).
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "slic3r/plugin/PluginManager.hpp"
|
||||
#include "slic3r/plugin/pluginTypes/gcode/GCodePluginCapability.hpp"
|
||||
#include "slic3r/plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp"
|
||||
#include "slic3r/plugin/PythonInterpreter.hpp"
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
@@ -241,27 +241,39 @@ void gcode_add_line_number(const std::string& path, const DynamicPrintConfig& co
|
||||
fs.close();
|
||||
}
|
||||
|
||||
// Run the configured post-processing plugins on `gcode_path` in place. Plugins are executed in-process
|
||||
// through the embedded Python interpreter. Throws Slic3r::RuntimeError on any failure; the caller is
|
||||
// responsible for removing the working copy (see run_post_process_scripts' catch block).
|
||||
// Entries are bare capability names; the top-level plugins manifest carries the full plugin refs.
|
||||
// Run the configured slicing-pipeline plugins on `gcode_path` in place, at their Step.psGCodePostProcess
|
||||
// seam. This is the same capability that runs at the geometry seams inside Print::process(); here it is
|
||||
// dispatched a final time on the exported G-code, so a plugin can edit slices AND the final G-code from
|
||||
// one class. Plugins are executed in-process through the embedded Python interpreter. Throws
|
||||
// Slic3r::RuntimeError on any failure; the caller removes the working copy (see run_post_process_scripts'
|
||||
// catch block). Entries are bare capability names; the top-level plugins manifest carries the full refs.
|
||||
// A geometry-only plugin simply returns success here (it filters on ctx.step), so it costs nothing beyond
|
||||
// one no-op call, but note any configured pipeline plugin still engages this post-process path (i.e. the
|
||||
// non-BBL ".pp" working copy) even if it does no G-code work.
|
||||
static void run_post_process_plugins(const ConfigOptionStrings& capabilities,
|
||||
const ConfigOptionStrings* plugins,
|
||||
const std::string& gcode_path,
|
||||
const std::string& host,
|
||||
const std::string& output_name)
|
||||
const std::string& output_name,
|
||||
const DynamicPrintConfig& config)
|
||||
{
|
||||
// Let plugins observe the (possibly script-updated) target file name, mirroring the script env.
|
||||
boost::nowide::setenv("SLIC3R_PP_OUTPUT_NAME", output_name.c_str(), 1);
|
||||
|
||||
const boost::filesystem::path gcode_file(gcode_path);
|
||||
|
||||
auto execute_fn = [&](std::shared_ptr<GCodePluginCapability> cap, const PluginCapabilityRef& ref) {
|
||||
GCodePluginContext ctx;
|
||||
auto execute_fn = [&](std::shared_ptr<SlicingPipelinePluginCapability> cap, const PluginCapabilityRef& ref) {
|
||||
SlicingPipelineContext ctx;
|
||||
ctx.orca_version = SoftFever_VERSION;
|
||||
ctx.step = SlicingPipelineStepPlugin::psGCodePostProcess;
|
||||
ctx.gcode_path = gcode_path;
|
||||
ctx.host = host;
|
||||
ctx.output_name = output_name;
|
||||
ctx.full_config = &config; // no live Print here; config_value() reads this
|
||||
// Hand the plugin its own [tool.orcaslicer.plugin.settings] as ctx.params (same plugin_key the
|
||||
// capability was resolved by), mirroring the in-pipeline dispatcher in GUI_App.cpp.
|
||||
const std::string plugin_key = ref.uuid.empty() ? ref.name : ref.uuid;
|
||||
ctx.params = PluginManager::instance().get_loader().get_plugin_settings(plugin_key);
|
||||
|
||||
ExecutionResult exec_result;
|
||||
try {
|
||||
@@ -298,11 +310,12 @@ static void run_post_process_plugins(const ConfigOptionStrings& capabilities,
|
||||
BOOST_LOG_TRIVIAL(info) << "Post-processing plugin " << ref.capability_name << " completed successfully";
|
||||
};
|
||||
|
||||
execute_capabilities_from_refs<GCodePluginCapability>(capabilities, plugins, PluginCapabilityType::PostProcessing, execute_fn);
|
||||
execute_capabilities_from_refs<SlicingPipelinePluginCapability>(capabilities, plugins, PluginCapabilityType::SlicingPipeline, execute_fn);
|
||||
}
|
||||
|
||||
// Run post-processing scripts ("post_process") and/or post-processing plugins ("post_process_plugin")
|
||||
// if defined. Both run on the same working copy of the G-code (the ".pp" temp when make_copy), so a
|
||||
// Run post-processing scripts ("post_process") and/or the slicing-pipeline plugins' psGCodePostProcess
|
||||
// step ("slicing_pipeline_plugin") if defined. Both run on the same working copy of the G-code (the
|
||||
// ".pp" temp when make_copy), so a
|
||||
// plugin never opens the original file the G-code viewer keeps memory-mapped (a writable open of the
|
||||
// mapped file fails on Windows with a sharing violation).
|
||||
// Returns true if a script or plugin was executed.
|
||||
@@ -317,11 +330,13 @@ static void run_post_process_plugins(const ConfigOptionStrings& capabilities,
|
||||
bool run_post_process_scripts(
|
||||
std::string& src_path, bool make_copy, const std::string& host, std::string& output_name, const DynamicPrintConfig& config)
|
||||
{
|
||||
// post_process / post_process_plugin are absent in SLA mode, hence the null checks.
|
||||
const auto* post_process = config.opt<ConfigOptionStrings>("post_process");
|
||||
const auto* post_process_plugin = config.opt<ConfigOptionStrings>("post_process_plugin");
|
||||
const bool have_scripts = post_process != nullptr && !post_process->values.empty();
|
||||
const bool have_plugins = post_process_plugin != nullptr && !post_process_plugin->values.empty();
|
||||
// post_process / slicing_pipeline_plugin are absent in SLA mode, hence the null checks. G-code
|
||||
// post-processing is now the psGCodePostProcess step of the slicing-pipeline plugin, so the same
|
||||
// slicing_pipeline_plugin option drives both the geometry seams and this final G-code seam.
|
||||
const auto* post_process = config.opt<ConfigOptionStrings>("post_process");
|
||||
const auto* slicing_pipeline_plugin = config.opt<ConfigOptionStrings>("slicing_pipeline_plugin");
|
||||
const bool have_scripts = post_process != nullptr && !post_process->values.empty();
|
||||
const bool have_plugins = slicing_pipeline_plugin != nullptr && !slicing_pipeline_plugin->values.empty();
|
||||
if (!have_scripts && !have_plugins)
|
||||
return false;
|
||||
|
||||
@@ -469,7 +484,7 @@ bool run_post_process_scripts(
|
||||
// Run plugins after the scripts so they observe any output_name the scripts produced. A thrown
|
||||
// exception is handled by the catch below, which removes the temp copy.
|
||||
if (have_plugins) {
|
||||
run_post_process_plugins(*post_process_plugin, config.opt<ConfigOptionStrings>("plugins"), path, host, output_name);
|
||||
run_post_process_plugins(*slicing_pipeline_plugin, config.opt<ConfigOptionStrings>("plugins"), path, host, output_name, config);
|
||||
}
|
||||
} catch (...) {
|
||||
remove_output_name_file();
|
||||
|
||||
@@ -9,9 +9,10 @@
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Run post-processing scripts (the "post_process" option) and/or post-processing plugins (the
|
||||
// "post_process_plugin" option) if defined. Lives in the GUI layer because plugins are executed
|
||||
// through the embedded-Python PluginManager, which libslic3r must not depend on.
|
||||
// Run post-processing scripts (the "post_process" option) and/or the slicing-pipeline plugins'
|
||||
// Step.psGCodePostProcess seam (the "slicing_pipeline_plugin" option) if defined. Lives in the GUI
|
||||
// layer because plugins are executed through the embedded-Python PluginManager, which libslic3r must
|
||||
// not depend on.
|
||||
// Returns true if a script or plugin was executed.
|
||||
// Returns false if neither a post-processing script nor plugin was defined.
|
||||
// Throws an exception on error.
|
||||
|
||||
@@ -1790,6 +1790,13 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
|
||||
return;
|
||||
}
|
||||
|
||||
// Keep this preset's "plugins" manifest in sync when a plugin picker changes, so the edited preset
|
||||
// always carries resolved "name;uuid;capability" references that full_config() and save_to_json()
|
||||
// then pass downstream as-is -- no separate rebuild anywhere else.
|
||||
if (const ConfigOptionDef* opt_def = m_config->def()->get(opt_key);
|
||||
opt_def && opt_def->is_plugin_backed())
|
||||
m_config->update_plugin_manifest();
|
||||
|
||||
if (opt_key == "gcode_flavor" && m_type == Preset::TYPE_PRINTER) {
|
||||
if (auto printer_tab = dynamic_cast<TabPrinter*>(this))
|
||||
printer_tab->on_gcode_flavor_changed();
|
||||
@@ -3073,9 +3080,9 @@ void TabPrint::build()
|
||||
option.opt.height = 15;
|
||||
optgroup->append_single_option_line(option, "others_settings_post_processing_scripts");
|
||||
|
||||
optgroup = page->new_optgroup(L("Post-processing Plugin"), L"param_gcode", 0);
|
||||
optgroup = page->new_optgroup(L("Slicing Pipeline Plugin"), L"param_gcode", 0);
|
||||
optgroup->hide_labels();
|
||||
option = optgroup->get_option("post_process_plugin");
|
||||
option = optgroup->get_option("slicing_pipeline_plugin");
|
||||
option.opt.full_width = true;
|
||||
optgroup->append_single_option_line(option, "others_settings_plugin_picker");
|
||||
|
||||
|
||||
106
src/slic3r/plugin/PluginBindingUtils.hpp
Normal file
106
src/slic3r/plugin/PluginBindingUtils.hpp
Normal file
@@ -0,0 +1,106 @@
|
||||
#pragma once
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/numpy.h>
|
||||
#include "libslic3r/Config.hpp" // ConfigBase
|
||||
#include "libslic3r/Point.hpp" // Point/Point3 packing asserts, Vec3d, Transform3d
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Point/Point3 must be tightly packed for zero-copy views. coord_t = int64_t.
|
||||
static_assert(sizeof(Point) == 2 * sizeof(coord_t), "Point must be 2 packed coord_t");
|
||||
static_assert(sizeof(Point3) == 3 * sizeof(coord_t), "Point3 must be 3 packed coord_t");
|
||||
|
||||
// Run a builder that constructs numpy objects, translating the "numpy missing"
|
||||
// ImportError into an actionable message (plugins must declare numpy as a dep).
|
||||
template<typename Builder>
|
||||
pybind11::object with_numpy(Builder&& build)
|
||||
{
|
||||
namespace py = pybind11;
|
||||
try {
|
||||
return std::forward<Builder>(build)();
|
||||
} catch (py::error_already_set& err) {
|
||||
if (err.matches(PyExc_ImportError))
|
||||
throw py::import_error("numpy is required to access geometry/mesh arrays; "
|
||||
"add dependencies = [\"numpy\"] to your plugin metadata");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Zero-copy, read-only (rows, N) numpy view over `data`, whose lifetime is tied
|
||||
// to `base` (the array's base object). T is the element scalar (coord_t = int64
|
||||
// for slicing coords, float for mesh vertices). rows == 0 / null data yields a
|
||||
// fresh empty (0, N) array with no base.
|
||||
template<typename T, int N>
|
||||
pybind11::array make_readonly_rows(pybind11::handle base, const T* data, pybind11::ssize_t rows)
|
||||
{
|
||||
namespace py = pybind11;
|
||||
if (rows == 0 || data == nullptr) {
|
||||
py::array_t<T> empty(std::vector<py::ssize_t>{ 0, (py::ssize_t) N });
|
||||
// Mark the fresh empty array read-only so every return path of this
|
||||
// helper yields a read-only view.
|
||||
empty.attr("setflags")(py::arg("write") = false);
|
||||
return std::move(empty);
|
||||
}
|
||||
py::array_t<T> arr(
|
||||
{ rows, (py::ssize_t) N },
|
||||
{ (py::ssize_t)(N * sizeof(T)), (py::ssize_t) sizeof(T) },
|
||||
data, base);
|
||||
// A base-carrying array is writable by default in pybind11; force read-only.
|
||||
arr.attr("setflags")(py::arg("write") = false);
|
||||
return std::move(arr);
|
||||
}
|
||||
|
||||
// Zero-copy, WRITABLE (rows, N) numpy view over `data`, lifetime tied to `base`.
|
||||
// Twin of make_readonly_rows: a base-carrying pybind array is writable by default,
|
||||
// so we simply do not clear the write flag. Writing through the view mutates the
|
||||
// underlying C++ buffer in place. rows == 0 / null data yields a fresh empty (0, N)
|
||||
// array (writable, no base).
|
||||
template<typename T, int N>
|
||||
pybind11::array make_writable_rows(pybind11::handle base, T* data, pybind11::ssize_t rows)
|
||||
{
|
||||
namespace py = pybind11;
|
||||
if (rows == 0 || data == nullptr)
|
||||
return py::array_t<T>(std::vector<py::ssize_t>{ 0, (py::ssize_t) N });
|
||||
return py::array_t<T>(
|
||||
{ rows, (py::ssize_t) N },
|
||||
{ (py::ssize_t)(N * sizeof(T)), (py::ssize_t) sizeof(T) },
|
||||
data, base);
|
||||
}
|
||||
|
||||
// Serialize one config key to a Python string, or None if the key is absent.
|
||||
// Works on any ConfigBase (resolved DynamicPrintConfig snapshots,
|
||||
// PrintObjectConfig, PrintRegionConfig, preset configs).
|
||||
inline pybind11::object config_value_or_none(const ConfigBase& config, const std::string& key)
|
||||
{
|
||||
if (!config.has(key))
|
||||
return pybind11::none();
|
||||
return pybind11::cast(config.opt_serialize(key));
|
||||
}
|
||||
|
||||
// Plugins receive 3D vectors as plain Python tuples (x, y, z) so the API stays
|
||||
// Pythonic and free of an Eigen/numpy runtime dependency.
|
||||
inline pybind11::tuple vec3_to_tuple(const Vec3d& v)
|
||||
{
|
||||
return pybind11::make_tuple(v.x(), v.y(), v.z());
|
||||
}
|
||||
|
||||
// 4x4 row-major float64 copy of an affine transform. Eigen stores column-major,
|
||||
// so fill element-wise to produce correct C-order data. Requires numpy.
|
||||
inline pybind11::object mat4_to_numpy(const Transform3d& transform)
|
||||
{
|
||||
namespace py = pybind11;
|
||||
return with_numpy([&] {
|
||||
py::array_t<double> array({ py::ssize_t(4), py::ssize_t(4) });
|
||||
auto view = array.mutable_unchecked<2>();
|
||||
const auto& matrix = transform.matrix();
|
||||
for (int i = 0; i < 4; ++i)
|
||||
for (int j = 0; j < 4; ++j)
|
||||
view(i, j) = matrix(i, j);
|
||||
return py::object(std::move(array));
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
@@ -61,6 +62,7 @@ struct PluginDescriptor
|
||||
std::string entry_path; // Full path to the installed plugin entry file
|
||||
std::string entry_package; // Import package/module used for package-based loading
|
||||
std::vector<std::string> dependencies; // Python dependency requirements declared by plugin package metadata
|
||||
std::map<std::string, std::string> settings; // [tool.orcaslicer.plugin.settings] table -> per-plugin params (ctx.params)
|
||||
std::vector<PluginChangelog> changelog; // Cloud release changelog, sorted newest-first when available.
|
||||
|
||||
std::string error; // Blocking error message. Non-empty means the plugin is in an error state.
|
||||
|
||||
129
src/slic3r/plugin/PluginHooks.cpp
Normal file
129
src/slic3r/plugin/PluginHooks.cpp
Normal file
@@ -0,0 +1,129 @@
|
||||
#include "PluginHooks.hpp"
|
||||
|
||||
#include "PluginManager.hpp"
|
||||
#include "PythonInterpreter.hpp"
|
||||
#include "PythonPluginInterface.hpp"
|
||||
#include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp"
|
||||
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/Exception.hpp"
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r_version.h"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r::plugin_hooks {
|
||||
namespace {
|
||||
|
||||
// Manifest resolver: turns the bare capability name a preset stores into the full
|
||||
// "name;uuid;capability" reference the dispatchers consume (see
|
||||
// ConfigBase::collect_plugin_manifest / update_plugin_manifest).
|
||||
void install_capability_resolver()
|
||||
{
|
||||
ConfigBase::set_resolve_capability_fn([](const std::string& cap_name, const std::string& cap_type) {
|
||||
PluginManager& plugin_mgr = PluginManager::instance();
|
||||
auto plugin_cap = plugin_mgr.get_loader().try_get_plugin_capability_by_name_and_type(cap_name, plugin_capability_type_from_string(cap_type));
|
||||
if (!plugin_cap)
|
||||
return std::string();
|
||||
|
||||
PluginDescriptor descriptor;
|
||||
if (!plugin_mgr.get_catalog().try_get_plugin_descriptor(plugin_cap->plugin_key, descriptor))
|
||||
return std::string();
|
||||
|
||||
// Cloud plugins are resolved at runtime via the UUID in the middle field, so the first
|
||||
// field keeps the friendly display name. Local plugins are looked up by plugin_key (the
|
||||
// first field, with an empty UUID), so emit the plugin_key to keep them resolvable.
|
||||
const std::string identity = descriptor.is_cloud_plugin() ? descriptor.name : descriptor.plugin_key;
|
||||
return identity + ';' + descriptor.cloud_uuid() + ';' + cap_name;
|
||||
});
|
||||
}
|
||||
|
||||
// Print::process() fires this hook at each pipeline seam on the slicing worker
|
||||
// thread; here we run the picker-selected SlicingPipeline capabilities. Per
|
||||
// capability we acquire the GIL, honor cancellation, and convert a plugin
|
||||
// failure into a (non-critical) SlicingError so it surfaces as a slicing-error
|
||||
// notification rather than the fatal-crash dialog.
|
||||
void install_slicing_pipeline_hook()
|
||||
{
|
||||
Print::set_slicing_pipeline_hook_fn(
|
||||
[](Print& print, const PrintObject* object, SlicingPipelineStepPlugin step) {
|
||||
const auto* caps = print.config().option<ConfigOptionStrings>("slicing_pipeline_plugin");
|
||||
// `plugins` is a dynamic-only manifest key (not a static PrintConfig member), so it
|
||||
// must be read from the full/dynamic config -- reading it off print.config() (the
|
||||
// static PrintConfig) always yields nullptr and skips every capability. Mirrors the
|
||||
// post-process path (PostProcessor.cpp, via BackgroundSlicingProcess::full_print_config()).
|
||||
const auto* plugs = print.full_print_config().option<ConfigOptionStrings>("plugins");
|
||||
if (caps == nullptr || caps->values.empty())
|
||||
return;
|
||||
|
||||
execute_capabilities_from_refs<SlicingPipelinePluginCapability>(
|
||||
*caps, plugs, PluginCapabilityType::SlicingPipeline,
|
||||
[&](std::shared_ptr<SlicingPipelinePluginCapability> cap, const PluginCapabilityRef& ref) {
|
||||
ExecutionResult r;
|
||||
try {
|
||||
// GIL is acquired per capability (not once for the whole dispatch) so it
|
||||
// is released between capabilities.
|
||||
PythonGILState gil;
|
||||
// throw_if_canceled() is protected on PrintBase; canceled() is the public
|
||||
// equivalent check (same cancel flag), so honor cancellation via it.
|
||||
if (print.canceled())
|
||||
throw CanceledException();
|
||||
SlicingPipelineContext ctx;
|
||||
ctx.orca_version = SoftFever_VERSION;
|
||||
ctx.step = step;
|
||||
ctx.print = &print;
|
||||
ctx.object = object;
|
||||
// hand the plugin its own [tool.orcaslicer.plugin.settings] as ctx.params
|
||||
// (same plugin_key the capability was resolved by, so it always matches).
|
||||
const std::string plugin_key = ref.uuid.empty() ? ref.name : ref.uuid;
|
||||
ctx.params = PluginManager::instance().get_loader().get_plugin_settings(plugin_key);
|
||||
r = cap->execute(ctx);
|
||||
} catch (const CanceledException&) {
|
||||
throw; // cancellation must reach process(), never become a slicing error
|
||||
} catch (const std::exception& ex) {
|
||||
// A Python raise reaches here as pybind11::error_already_set; surface it as a
|
||||
// (non-critical) slicing error instead of a crash.
|
||||
throw SlicingError(std::string("Slicing pipeline plugin '") +
|
||||
ref.capability_name + "' error: " + ex.what());
|
||||
}
|
||||
if (r.status == PluginResult::FatalError)
|
||||
throw SlicingError(std::string("Slicing pipeline plugin '") +
|
||||
ref.capability_name + "' error: " + r.message);
|
||||
// log a non-empty success/skipped message instead of dropping it. This is
|
||||
// log-only by design: every pipeline hook fires AFTER set_done() (see Print.cpp),
|
||||
// so the Print-level m_step_active is -1 here. Calling active_step_add_warning()
|
||||
// would then index m_state[-1] (out-of-bounds; the guarding assert is compiled
|
||||
// out in Release), so it must NOT be called from a pipeline hook.
|
||||
if (!r.message.empty()) {
|
||||
static const char* const kStepNames[] = {
|
||||
"posSlice", "posPerimeters", "posEstimateCurledExtrusions", "posPrepareInfill", "posInfill",
|
||||
"posIroning", "posContouring", "posSupportMaterial", "posDetectOverhangsForLift",
|
||||
"posSimplifyPath", "psWipeTower", "psSkirtBrim", "psGCodePostProcess"
|
||||
}; // order must match SlicingPipelineStepPlugin
|
||||
const char* step_name = static_cast<size_t>(step) < sizeof(kStepNames) / sizeof(kStepNames[0])
|
||||
? kStepNames[static_cast<int>(step)] : "Unknown";
|
||||
BOOST_LOG_TRIVIAL(info) << "Slicing pipeline plugin '" << ref.capability_name
|
||||
<< "' [" << step_name << "]: " << r.message;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void install()
|
||||
{
|
||||
install_capability_resolver();
|
||||
install_slicing_pipeline_hook();
|
||||
}
|
||||
|
||||
void uninstall()
|
||||
{
|
||||
ConfigBase::set_resolve_capability_fn(nullptr);
|
||||
Print::set_slicing_pipeline_hook_fn(nullptr);
|
||||
}
|
||||
|
||||
} // namespace Slic3r::plugin_hooks
|
||||
21
src/slic3r/plugin/PluginHooks.hpp
Normal file
21
src/slic3r/plugin/PluginHooks.hpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
// The plugin layer's installers for the hooks libslic3r exposes. libslic3r
|
||||
// stays free of any plugin/Python dependency: it exposes static setter seams
|
||||
// (ConfigBase::set_resolve_capability_fn, Print::set_slicing_pipeline_hook_fn,
|
||||
// ...) and this unit injects the dispatchers -- one file-local installer per
|
||||
// hook, aggregated by install(). Capabilities dispatched from the GUI layer
|
||||
// (e.g. PostProcessor.cpp) call execute_capabilities_from_refs at their own
|
||||
// call site and need no hook here.
|
||||
|
||||
namespace Slic3r::plugin_hooks {
|
||||
|
||||
// Install every hook. Called once from PluginManager::initialize().
|
||||
void install();
|
||||
|
||||
// Reset every hook to null so none can enter Python after the interpreter
|
||||
// finalizes. Called from PluginManager::shutdown(); callers must have stopped
|
||||
// background slicing first (resetting a hook while process() runs is a race).
|
||||
void uninstall();
|
||||
|
||||
} // namespace Slic3r::plugin_hooks
|
||||
@@ -1,535 +0,0 @@
|
||||
#include "PluginHostApi.hpp"
|
||||
#include "PluginHostUi.hpp"
|
||||
|
||||
#include <libslic3r/BoundingBox.hpp>
|
||||
#include <libslic3r/Model.hpp>
|
||||
#include <libslic3r/Preset.hpp>
|
||||
#include <libslic3r/PresetBundle.hpp>
|
||||
#include <libslic3r/TriangleMesh.hpp>
|
||||
#include <slic3r/GUI/GUI_App.hpp>
|
||||
#include <slic3r/GUI/Plater.hpp>
|
||||
|
||||
#include <pybind11/numpy.h>
|
||||
#include <pybind11/stl.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
namespace Slic3r {
|
||||
namespace {
|
||||
|
||||
GUI::Plater* current_plater()
|
||||
{
|
||||
if (wxTheApp == nullptr)
|
||||
throw std::runtime_error("OrcaSlicer application is not initialized");
|
||||
|
||||
GUI::Plater* plater = GUI::wxGetApp().plater();
|
||||
if (plater == nullptr)
|
||||
throw std::runtime_error("Plater is not available");
|
||||
|
||||
return plater;
|
||||
}
|
||||
|
||||
PresetBundle* current_preset_bundle()
|
||||
{
|
||||
if (wxTheApp == nullptr)
|
||||
throw std::runtime_error("OrcaSlicer application is not initialized");
|
||||
|
||||
PresetBundle* preset_bundle = GUI::wxGetApp().preset_bundle;
|
||||
if (preset_bundle == nullptr)
|
||||
throw std::runtime_error("Preset bundle is not available");
|
||||
|
||||
return preset_bundle;
|
||||
}
|
||||
|
||||
py::object config_value_or_none(const DynamicPrintConfig& config, const std::string& key)
|
||||
{
|
||||
if (!config.has(key))
|
||||
return py::none();
|
||||
return py::cast(config.opt_serialize(key));
|
||||
}
|
||||
|
||||
// Plugins receive 3D vectors as plain Python tuples (x, y, z) so the API stays
|
||||
// Pythonic and free of an Eigen/numpy runtime dependency.
|
||||
py::tuple vec3_to_tuple(const Vec3d& v)
|
||||
{
|
||||
return py::make_tuple(v.x(), v.y(), v.z());
|
||||
}
|
||||
|
||||
// Build a BoundingBoxf3 from precomputed (float) triangle-mesh stats min/max.
|
||||
BoundingBoxf3 bbox_from_stats(const TriangleMeshStats& stats)
|
||||
{
|
||||
if (stats.number_of_facets == 0)
|
||||
return BoundingBoxf3();
|
||||
return BoundingBoxf3(stats.min.cast<double>(), stats.max.cast<double>());
|
||||
}
|
||||
|
||||
// --- Mesh geometry helpers -------------------------------------------------
|
||||
|
||||
// Zero-copy export of its.vertices / its.indices relies on these Eigen
|
||||
// row-vectors being tightly packed (no padding between the 3 components).
|
||||
static_assert(sizeof(stl_vertex) == 3 * sizeof(float),
|
||||
"stl_vertex must be a packed float[3] for zero-copy numpy export");
|
||||
static_assert(sizeof(stl_triangle_vertex_indices) == 3 * sizeof(std::int32_t),
|
||||
"triangle index must be a packed int32[3] for zero-copy numpy export");
|
||||
|
||||
// Immutable snapshot of a ModelVolume's mesh. Holding a strong reference to the
|
||||
// const mesh keeps any zero-copy numpy views valid even if the volume's mesh is
|
||||
// later replaced on the main thread.
|
||||
struct HostTriangleMesh
|
||||
{
|
||||
std::shared_ptr<const TriangleMesh> mesh;
|
||||
const indexed_triangle_set& its() const { return mesh->its; }
|
||||
};
|
||||
|
||||
// Run a builder that constructs numpy objects, translating the "numpy missing"
|
||||
// ImportError into an actionable message (plugins must declare numpy as a dep).
|
||||
template<typename Builder>
|
||||
py::object with_numpy(Builder&& build)
|
||||
{
|
||||
try {
|
||||
return std::forward<Builder>(build)();
|
||||
} catch (py::error_already_set& err) {
|
||||
if (err.matches(PyExc_ImportError))
|
||||
throw py::import_error("numpy is required to access mesh arrays/matrices; "
|
||||
"add dependencies = [\"numpy\"] to your plugin metadata");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Read-only, zero-copy (rows, 3) numpy view over a packed T[rows][3] buffer.
|
||||
// The array owns a capsule that pins `mesh` alive for the view's lifetime.
|
||||
template<typename T>
|
||||
py::array make_readonly_rows3(const std::shared_ptr<const TriangleMesh>& mesh,
|
||||
const T* data, py::ssize_t rows)
|
||||
{
|
||||
if (rows == 0 || data == nullptr)
|
||||
return py::array_t<T>(std::vector<py::ssize_t>{0, 3});
|
||||
|
||||
auto* owner = new std::shared_ptr<const TriangleMesh>(mesh);
|
||||
py::capsule base(owner, [](void* p) {
|
||||
delete reinterpret_cast<std::shared_ptr<const TriangleMesh>*>(p);
|
||||
});
|
||||
|
||||
py::array_t<T> array(
|
||||
{ rows, py::ssize_t(3) },
|
||||
{ py::ssize_t(3 * sizeof(T)), py::ssize_t(sizeof(T)) },
|
||||
data,
|
||||
base);
|
||||
// A capsule-based array is writable by default in pybind11; the underlying
|
||||
// mesh is const, so force the view read-only.
|
||||
array.attr("setflags")(py::arg("write") = false);
|
||||
return array;
|
||||
}
|
||||
|
||||
// 4x4 row-major float64 copy of an affine transform. Eigen stores column-major,
|
||||
// so fill element-wise to produce correct C-order data.
|
||||
py::object mat4_to_numpy(const Transform3d& transform)
|
||||
{
|
||||
return with_numpy([&] {
|
||||
py::array_t<double> array({ py::ssize_t(4), py::ssize_t(4) });
|
||||
auto view = array.mutable_unchecked<2>();
|
||||
const auto& matrix = transform.matrix();
|
||||
for (int i = 0; i < 4; ++i)
|
||||
for (int j = 0; j < 4; ++j)
|
||||
view(i, j) = matrix(i, j);
|
||||
return py::object(std::move(array));
|
||||
});
|
||||
}
|
||||
|
||||
py::list current_filament_presets(PresetBundle& bundle)
|
||||
{
|
||||
py::list presets;
|
||||
for (const std::string& preset_name : bundle.filament_presets) {
|
||||
Preset* preset = bundle.filaments.find_preset(preset_name);
|
||||
if (preset == nullptr)
|
||||
presets.append(py::none());
|
||||
else
|
||||
presets.append(py::cast(preset, py::return_value_policy::reference));
|
||||
}
|
||||
return presets;
|
||||
}
|
||||
|
||||
PresetCollection& printer_presets(PresetBundle& bundle)
|
||||
{
|
||||
return static_cast<PresetCollection&>(bundle.printers);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void PluginHostApi::RegisterBindings(pybind11::module_& module)
|
||||
{
|
||||
auto host = module.def_submodule("host", "Host application API");
|
||||
|
||||
py::enum_<Preset::Type>(host, "PresetType")
|
||||
.value("Invalid", Preset::TYPE_INVALID)
|
||||
.value("Print", Preset::TYPE_PRINT)
|
||||
.value("SlaPrint", Preset::TYPE_SLA_PRINT)
|
||||
.value("Filament", Preset::TYPE_FILAMENT)
|
||||
.value("SlaMaterial", Preset::TYPE_SLA_MATERIAL)
|
||||
.value("Printer", Preset::TYPE_PRINTER)
|
||||
.value("PhysicalPrinter", Preset::TYPE_PHYSICAL_PRINTER)
|
||||
.value("Plate", Preset::TYPE_PLATE)
|
||||
.value("Model", Preset::TYPE_MODEL);
|
||||
|
||||
py::class_<Preset, std::unique_ptr<Preset, py::nodelete>>(host, "Preset")
|
||||
.def_readonly("type", &Preset::type)
|
||||
.def_readonly("name", &Preset::name)
|
||||
.def_readonly("alias", &Preset::alias)
|
||||
.def_readonly("file", &Preset::file)
|
||||
.def_readonly("is_default", &Preset::is_default)
|
||||
.def_readonly("is_external", &Preset::is_external)
|
||||
.def_readonly("is_system", &Preset::is_system)
|
||||
.def_readonly("is_visible", &Preset::is_visible)
|
||||
.def_readonly("is_dirty", &Preset::is_dirty)
|
||||
.def_readonly("is_compatible", &Preset::is_compatible)
|
||||
.def_readonly("is_project_embedded", &Preset::is_project_embedded)
|
||||
.def_readonly("bundle_id", &Preset::bundle_id)
|
||||
.def("is_user", &Preset::is_user)
|
||||
.def("is_from_bundle", &Preset::is_from_bundle)
|
||||
.def("label", &Preset::label, py::arg("no_alias") = false)
|
||||
.def("config_keys", [](const Preset& preset) { return preset.config.keys(); })
|
||||
.def("config_value", [](const Preset& preset, const std::string& key) {
|
||||
return config_value_or_none(preset.config, key);
|
||||
});
|
||||
|
||||
py::class_<PresetCollection, std::unique_ptr<PresetCollection, py::nodelete>>(host, "PresetCollection")
|
||||
.def("size", &PresetCollection::size)
|
||||
.def("get_selected_preset", [](PresetCollection& collection) -> Preset& {
|
||||
return collection.get_selected_preset();
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("selected_preset", [](PresetCollection& collection) -> Preset& {
|
||||
return collection.get_selected_preset();
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("get_selected_preset_name", &PresetCollection::get_selected_preset_name)
|
||||
.def("selected_preset_name", &PresetCollection::get_selected_preset_name)
|
||||
.def("get_edited_preset", [](PresetCollection& collection) -> Preset& {
|
||||
return collection.get_edited_preset();
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("edited_preset", [](PresetCollection& collection) -> Preset& {
|
||||
return collection.get_edited_preset();
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("preset", [](PresetCollection& collection, size_t index) -> Preset& {
|
||||
if (index >= collection.size())
|
||||
throw py::index_error("preset index out of range");
|
||||
return collection.preset(index);
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("find_preset", [](PresetCollection& collection, const std::string& name) -> Preset* {
|
||||
return collection.find_preset(name);
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("preset_names", [](const PresetCollection& collection) {
|
||||
std::vector<std::string> names;
|
||||
names.reserve(collection.get_presets().size());
|
||||
for (const Preset& preset : collection.get_presets())
|
||||
names.push_back(preset.name);
|
||||
return names;
|
||||
});
|
||||
|
||||
py::class_<PresetBundle, std::unique_ptr<PresetBundle, py::nodelete>>(host, "PresetBundle")
|
||||
.def_property_readonly("prints", [](PresetBundle& bundle) -> PresetCollection& {
|
||||
return bundle.prints;
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def_property_readonly("printers", &printer_presets, py::return_value_policy::reference_internal)
|
||||
.def_property_readonly("filaments", [](PresetBundle& bundle) -> PresetCollection& {
|
||||
return bundle.filaments;
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def_property_readonly("sla_prints", [](PresetBundle& bundle) -> PresetCollection& {
|
||||
return bundle.sla_prints;
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def_property_readonly("sla_materials", [](PresetBundle& bundle) -> PresetCollection& {
|
||||
return bundle.sla_materials;
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("current_process_preset", [](PresetBundle& bundle) -> Preset& {
|
||||
return bundle.prints.get_edited_preset();
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("current_print_preset", [](PresetBundle& bundle) -> Preset& {
|
||||
return bundle.prints.get_edited_preset();
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("current_printer_preset", [](PresetBundle& bundle) -> Preset& {
|
||||
return bundle.printers.get_edited_preset();
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("current_filament_preset_names", [](PresetBundle& bundle) {
|
||||
return bundle.filament_presets;
|
||||
})
|
||||
.def("current_filament_presets", ¤t_filament_presets)
|
||||
.def("full_config_keys", [](const PresetBundle& bundle) {
|
||||
return bundle.full_config().keys();
|
||||
})
|
||||
.def("full_config_value", [](const PresetBundle& bundle, const std::string& key) {
|
||||
return config_value_or_none(bundle.full_config(), key);
|
||||
});
|
||||
|
||||
// Axis-aligned bounding box, returned by value (a copy) so its lifetime is
|
||||
// independent of the model object it was computed from. Coordinates are in mm.
|
||||
py::class_<BoundingBoxf3>(host, "BoundingBox", "Axis-aligned bounding box in millimetres")
|
||||
.def_property_readonly("defined", [](const BoundingBoxf3& bb) { return bb.defined; })
|
||||
.def_property_readonly("min", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.min); })
|
||||
.def_property_readonly("max", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.max); })
|
||||
.def_property_readonly("size", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.size()); })
|
||||
.def_property_readonly("center", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.center()); })
|
||||
.def_property_readonly("radius", [](const BoundingBoxf3& bb) { return bb.radius(); });
|
||||
|
||||
py::class_<HostTriangleMesh>(host, "TriangleMesh",
|
||||
"Immutable snapshot of a ModelVolume's mesh in local (untransformed) coordinates, mm.")
|
||||
.def("vertex_count", [](const HostTriangleMesh& mesh) { return mesh.its().vertices.size(); })
|
||||
.def("triangle_count", [](const HostTriangleMesh& mesh) { return mesh.its().indices.size(); })
|
||||
.def("facets_count", [](const HostTriangleMesh& mesh) { return mesh.its().indices.size(); })
|
||||
.def("is_empty", [](const HostTriangleMesh& mesh) { return mesh.its().indices.empty(); })
|
||||
// Read-only, zero-copy (N, 3) float32 view of vertex positions. Requires numpy.
|
||||
.def("vertices", [](const HostTriangleMesh& mesh) {
|
||||
return with_numpy([&] {
|
||||
const indexed_triangle_set& its = mesh.its();
|
||||
return make_readonly_rows3<float>(
|
||||
mesh.mesh,
|
||||
its.vertices.empty() ? nullptr : its.vertices.front().data(),
|
||||
static_cast<py::ssize_t>(its.vertices.size()));
|
||||
});
|
||||
}, "Read-only zero-copy (N, 3) float32 ndarray of vertex positions (local mm). Requires numpy.")
|
||||
// Read-only, zero-copy (M, 3) int32 view of triangle vertex indices. Requires numpy.
|
||||
.def("triangles", [](const HostTriangleMesh& mesh) {
|
||||
return with_numpy([&] {
|
||||
const indexed_triangle_set& its = mesh.its();
|
||||
return make_readonly_rows3<std::int32_t>(
|
||||
mesh.mesh,
|
||||
its.indices.empty() ? nullptr : its.indices.front().data(),
|
||||
static_cast<py::ssize_t>(its.indices.size()));
|
||||
});
|
||||
}, "Read-only zero-copy (M, 3) int32 ndarray of triangle vertex indices. Requires numpy.")
|
||||
// One normalized normal per triangle as an (M, 3) float32 copy. Requires numpy.
|
||||
.def("face_normals", [](const HostTriangleMesh& mesh) {
|
||||
return with_numpy([&] {
|
||||
std::vector<Vec3f> normals = its_face_normals(mesh.its());
|
||||
py::array_t<float> array({ static_cast<py::ssize_t>(normals.size()), py::ssize_t(3) });
|
||||
if (!normals.empty()) {
|
||||
auto view = array.mutable_unchecked<2>();
|
||||
for (size_t i = 0; i < normals.size(); ++i) {
|
||||
view(i, 0) = normals[i].x();
|
||||
view(i, 1) = normals[i].y();
|
||||
view(i, 2) = normals[i].z();
|
||||
}
|
||||
}
|
||||
return py::object(std::move(array));
|
||||
});
|
||||
}, "Per-triangle normalized normals as an (M, 3) float32 ndarray (copy). Requires numpy.")
|
||||
// numpy-free element access, bounds-checked.
|
||||
.def("vertex", [](const HostTriangleMesh& mesh, size_t index) {
|
||||
const std::vector<stl_vertex>& vertices = mesh.its().vertices;
|
||||
if (index >= vertices.size())
|
||||
throw py::index_error("vertex index out of range");
|
||||
const stl_vertex& vertex = vertices[index];
|
||||
return py::make_tuple(vertex.x(), vertex.y(), vertex.z());
|
||||
})
|
||||
.def("triangle", [](const HostTriangleMesh& mesh, size_t index) {
|
||||
const std::vector<stl_triangle_vertex_indices>& indices = mesh.its().indices;
|
||||
if (index >= indices.size())
|
||||
throw py::index_error("triangle index out of range");
|
||||
const stl_triangle_vertex_indices& triangle = indices[index];
|
||||
return py::make_tuple(triangle[0], triangle[1], triangle[2]);
|
||||
})
|
||||
.def("volume", [](const HostTriangleMesh& mesh) { return mesh.mesh->stats().volume; })
|
||||
.def("bounding_box", [](const HostTriangleMesh& mesh) { return bbox_from_stats(mesh.mesh->stats()); })
|
||||
.def("is_manifold", [](const HostTriangleMesh& mesh) { return mesh.mesh->stats().manifold(); });
|
||||
|
||||
py::enum_<ModelVolumeType>(host, "ModelVolumeType")
|
||||
.value("Invalid", ModelVolumeType::INVALID)
|
||||
.value("ModelPart", ModelVolumeType::MODEL_PART)
|
||||
.value("NegativeVolume", ModelVolumeType::NEGATIVE_VOLUME)
|
||||
.value("ParameterModifier", ModelVolumeType::PARAMETER_MODIFIER)
|
||||
.value("SupportBlocker", ModelVolumeType::SUPPORT_BLOCKER)
|
||||
.value("SupportEnforcer", ModelVolumeType::SUPPORT_ENFORCER);
|
||||
|
||||
py::class_<ModelVolume, std::unique_ptr<ModelVolume, py::nodelete>>(host, "ModelVolume")
|
||||
.def("id", [](const ModelVolume& volume) { return volume.id().id; })
|
||||
.def_readonly("name", &ModelVolume::name)
|
||||
.def("type", &ModelVolume::type)
|
||||
.def("is_model_part", &ModelVolume::is_model_part)
|
||||
.def("is_modifier", &ModelVolume::is_modifier)
|
||||
.def("is_negative_volume", &ModelVolume::is_negative_volume)
|
||||
.def("is_support_enforcer", &ModelVolume::is_support_enforcer)
|
||||
.def("is_support_blocker", &ModelVolume::is_support_blocker)
|
||||
.def("is_support_modifier", &ModelVolume::is_support_modifier)
|
||||
// Extruder ID is 1-based for FFF, -1 for SLA or support volumes.
|
||||
.def("extruder_id", &ModelVolume::extruder_id)
|
||||
.def("offset", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_offset()); })
|
||||
.def("rotation", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_rotation()); })
|
||||
.def("scaling_factor", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_scaling_factor()); })
|
||||
.def("mirror", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_mirror()); })
|
||||
// 4x4 float64 affine matrix mapping this volume into its parent object frame. Requires numpy.
|
||||
.def("matrix", [](const ModelVolume& volume) { return mat4_to_numpy(volume.get_matrix()); },
|
||||
"Volume-to-object 4x4 float64 affine matrix (copy). Requires numpy.")
|
||||
.def("facets_count", [](const ModelVolume& volume) { return volume.mesh().facets_count(); })
|
||||
// Raw (untransformed) mesh volume in mm^3; -1 if it was never computed.
|
||||
.def("volume", [](const ModelVolume& volume) { return volume.mesh().stats().volume; })
|
||||
// Bounding box of the raw (untransformed) mesh, in the volume's local frame.
|
||||
.def("bounding_box", [](const ModelVolume& volume) { return bbox_from_stats(volume.mesh().stats()); })
|
||||
.def("is_manifold", [](const ModelVolume& volume) { return volume.mesh().stats().manifold(); })
|
||||
// Full mesh geometry (vertices/triangles) as an immutable snapshot.
|
||||
.def("mesh", [](const ModelVolume& volume) {
|
||||
return HostTriangleMesh{ volume.get_mesh_shared_ptr() };
|
||||
}, "Return the volume's TriangleMesh (local coordinates) for vertex/triangle access.")
|
||||
.def("mesh_errors_count", [](const ModelVolume& volume) { return volume.get_repaired_errors_count(); })
|
||||
.def("is_fdm_support_painted", &ModelVolume::is_fdm_support_painted)
|
||||
.def("is_seam_painted", &ModelVolume::is_seam_painted)
|
||||
.def("is_mm_painted", &ModelVolume::is_mm_painted)
|
||||
.def("is_fuzzy_skin_painted", &ModelVolume::is_fuzzy_skin_painted)
|
||||
.def("config_keys", [](const ModelVolume& volume) { return volume.config.keys(); })
|
||||
.def("config_value", [](const ModelVolume& volume, const std::string& key) {
|
||||
return config_value_or_none(volume.config.get(), key);
|
||||
});
|
||||
|
||||
py::class_<ModelInstance, std::unique_ptr<ModelInstance, py::nodelete>>(host, "ModelInstance")
|
||||
.def("id", [](const ModelInstance& instance) { return instance.id().id; })
|
||||
.def_readonly("printable", &ModelInstance::printable)
|
||||
// True only if the object is printable, this instance is printable and it
|
||||
// currently sits fully inside the print volume (set during slicing).
|
||||
.def("is_printable", &ModelInstance::is_printable)
|
||||
.def("offset", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_offset()); })
|
||||
.def("rotation", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_rotation()); })
|
||||
.def("scaling_factor", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_scaling_factor()); })
|
||||
.def("mirror", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_mirror()); })
|
||||
// 4x4 float64 affine matrix mapping the object into world space. Requires numpy.
|
||||
// World vertices = instance.matrix() @ volume.matrix() applied to mesh vertices.
|
||||
.def("matrix", [](const ModelInstance& instance) { return mat4_to_numpy(instance.get_matrix()); },
|
||||
"Object-to-world 4x4 float64 affine matrix (copy). Requires numpy.")
|
||||
.def("is_left_handed", &ModelInstance::is_left_handed)
|
||||
// Assemble-view placement. Each instance carries a second transform used only by
|
||||
// the Assemble view, set from stored 3mf assemble data or derived from the regular
|
||||
// transform. Until then (is_assemble_initialized() false) it is identity.
|
||||
.def("is_assemble_initialized", [](ModelInstance& instance) { return instance.is_assemble_initialized(); })
|
||||
.def("assemble_offset", [](const ModelInstance& instance) {
|
||||
return vec3_to_tuple(instance.get_assemble_transformation().get_offset());
|
||||
})
|
||||
.def("assemble_rotation", [](const ModelInstance& instance) {
|
||||
return vec3_to_tuple(instance.get_assemble_transformation().get_rotation());
|
||||
})
|
||||
// 4x4 float64 affine matrix placing the object in the Assemble view. Requires numpy.
|
||||
.def("assemble_matrix", [](const ModelInstance& instance) {
|
||||
return mat4_to_numpy(instance.get_assemble_transformation().get_matrix());
|
||||
}, "Assemble-view 4x4 float64 affine matrix (copy). Requires numpy.")
|
||||
// Offset from the instance origin to its position within the source assembly,
|
||||
// recorded at import time (e.g. from a STEP assembly).
|
||||
.def("offset_to_assembly", [](const ModelInstance& instance) {
|
||||
return vec3_to_tuple(instance.get_offset_to_assembly());
|
||||
})
|
||||
// World-space bounding box of this instance.
|
||||
.def("bounding_box", [](ModelInstance& instance) {
|
||||
const ModelObject* object = instance.get_object();
|
||||
if (object == nullptr)
|
||||
return BoundingBoxf3();
|
||||
return object->instance_bounding_box(instance);
|
||||
});
|
||||
|
||||
py::class_<ModelObject, std::unique_ptr<ModelObject, py::nodelete>>(host, "ModelObject")
|
||||
.def("id", [](const ModelObject& object) { return object.id().id; })
|
||||
.def_readonly("name", &ModelObject::name)
|
||||
.def_readonly("module_name", &ModelObject::module_name)
|
||||
.def_readonly("input_file", &ModelObject::input_file)
|
||||
// Import-time flag only: the GUI's printable toggle writes the per-instance
|
||||
// ModelInstance::printable and never updates this field, so derive an
|
||||
// object's effective state from its instances.
|
||||
.def_readonly("printable", &ModelObject::printable)
|
||||
.def("instance_count", [](const ModelObject& object) {
|
||||
return object.instances.size();
|
||||
})
|
||||
.def("volume_count", [](const ModelObject& object) {
|
||||
return object.volumes.size();
|
||||
})
|
||||
.def("instances", [](ModelObject& object) {
|
||||
py::list instances;
|
||||
for (ModelInstance* instance : object.instances)
|
||||
instances.append(py::cast(instance, py::return_value_policy::reference));
|
||||
return instances;
|
||||
})
|
||||
.def("instance", [](ModelObject& object, size_t index) -> ModelInstance* {
|
||||
if (index >= object.instances.size())
|
||||
throw py::index_error("instance index out of range");
|
||||
return object.instances[index];
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("volumes", [](ModelObject& object) {
|
||||
py::list volumes;
|
||||
for (ModelVolume* volume : object.volumes)
|
||||
volumes.append(py::cast(volume, py::return_value_policy::reference));
|
||||
return volumes;
|
||||
})
|
||||
.def("volume", [](ModelObject& object, size_t index) -> ModelVolume* {
|
||||
if (index >= object.volumes.size())
|
||||
throw py::index_error("volume index out of range");
|
||||
return object.volumes[index];
|
||||
}, py::return_value_policy::reference_internal)
|
||||
// World-space bounding box over all instances of this object.
|
||||
.def("bounding_box", [](const ModelObject& object) { return object.bounding_box_exact(); })
|
||||
// Bounding box of the object's raw (untransformed) part meshes — its intrinsic size.
|
||||
.def("raw_mesh_bounding_box", [](const ModelObject& object) { return object.raw_mesh_bounding_box(); })
|
||||
.def("min_z", &ModelObject::min_z)
|
||||
.def("max_z", &ModelObject::max_z)
|
||||
.def("facets_count", [](const ModelObject& object) { return object.facets_count(); })
|
||||
.def("parts_count", [](const ModelObject& object) { return object.parts_count(); })
|
||||
.def("materials_count", [](const ModelObject& object) { return object.materials_count(); })
|
||||
.def("mesh_errors_count", [](const ModelObject& object) { return object.get_repaired_errors_count(); })
|
||||
.def("is_multiparts", &ModelObject::is_multiparts)
|
||||
.def("is_cut", &ModelObject::is_cut)
|
||||
.def("has_custom_layering", &ModelObject::has_custom_layering)
|
||||
.def("is_fdm_support_painted", &ModelObject::is_fdm_support_painted)
|
||||
.def("is_seam_painted", &ModelObject::is_seam_painted)
|
||||
.def("is_mm_painted", &ModelObject::is_mm_painted)
|
||||
.def("is_fuzzy_skin_painted", &ModelObject::is_fuzzy_skin_painted)
|
||||
.def("config_keys", [](const ModelObject& object) {
|
||||
return object.config.keys();
|
||||
})
|
||||
.def("config_value", [](const ModelObject& object, const std::string& key) {
|
||||
return config_value_or_none(object.config.get(), key);
|
||||
});
|
||||
|
||||
py::class_<Model, std::unique_ptr<Model, py::nodelete>>(host, "Model")
|
||||
.def("id", [](const Model& model) { return model.id().id; })
|
||||
.def("object_count", [](const Model& model) {
|
||||
return model.objects.size();
|
||||
})
|
||||
.def("object", [](Model& model, size_t index) -> ModelObject* {
|
||||
if (index >= model.objects.size())
|
||||
throw py::index_error("model object index out of range");
|
||||
return model.objects[index];
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("objects", [](Model& model) {
|
||||
py::list objects;
|
||||
for (ModelObject* object : model.objects)
|
||||
objects.append(py::cast(object, py::return_value_policy::reference));
|
||||
return objects;
|
||||
})
|
||||
// World-space bounding box of the whole model. bounding_box() is exact;
|
||||
// bounding_box_approx() is faster and cached.
|
||||
.def("bounding_box", [](const Model& model) { return model.bounding_box_exact(); })
|
||||
.def("bounding_box_approx", [](const Model& model) { return model.bounding_box_approx(); })
|
||||
.def("max_z", &Model::max_z)
|
||||
.def("material_count", [](const Model& model) { return model.materials.size(); })
|
||||
.def("is_fdm_support_painted", &Model::is_fdm_support_painted)
|
||||
.def("is_seam_painted", &Model::is_seam_painted)
|
||||
.def("is_mm_painted", &Model::is_mm_painted)
|
||||
.def("is_fuzzy_skin_painted", &Model::is_fuzzy_skin_painted)
|
||||
.def("current_plate_index", [](const Model& model) { return model.curr_plate_index; })
|
||||
.def("designer", [](const Model& model) {
|
||||
return model.design_info ? model.design_info->Designer : std::string();
|
||||
})
|
||||
.def("design_id", [](const Model& model) { return model.stl_design_id; });
|
||||
|
||||
py::class_<GUI::Plater, std::unique_ptr<GUI::Plater, py::nodelete>>(host, "Plater")
|
||||
.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_presets_dirty", &GUI::Plater::is_presets_dirty)
|
||||
.def("inside_snapshot_capture", &GUI::Plater::inside_snapshot_capture);
|
||||
|
||||
host.def("plater", ¤t_plater, py::return_value_policy::reference);
|
||||
host.def("model", []() -> Model& {
|
||||
return current_plater()->model();
|
||||
}, py::return_value_policy::reference);
|
||||
host.def("preset_bundle", ¤t_preset_bundle, py::return_value_policy::reference);
|
||||
|
||||
// UI: native dialogs and interactive HTML windows for plugins.
|
||||
PluginHostUi::RegisterBindings(host);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -1,13 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class PluginHostApi
|
||||
{
|
||||
public:
|
||||
static void RegisterBindings(pybind11::module_& module);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -261,6 +261,13 @@ std::shared_ptr<LoadedPluginCapability> PluginLoader::get_plugin_capability_by_n
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::map<std::string, std::string> PluginLoader::get_plugin_settings(const std::string& plugin_key) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
const auto it = m_plugins.find(plugin_key);
|
||||
return it != m_plugins.end() ? it->second.descriptor.settings : std::map<std::string, std::string>{};
|
||||
}
|
||||
|
||||
std::vector<std::shared_ptr<LoadedPluginCapability>> PluginLoader::get_loaded_plugin_capabilities(const std::string& plugin_key) const
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_mutex);
|
||||
@@ -603,7 +610,6 @@ bool PluginLoader::unload_plugin(const std::string& plugin_key, PluginCapability
|
||||
if (!torn_down_types.insert(cap_type).second)
|
||||
continue;
|
||||
switch (cap_type) {
|
||||
case PluginCapabilityType::PostProcessing: break;
|
||||
case PluginCapabilityType::PrinterConnection: NetworkAgentFactory::deregister_python_plugin(plugin_key); break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
@@ -105,6 +105,8 @@ public:
|
||||
std::chrono::milliseconds timeout,
|
||||
std::string& error) const;
|
||||
std::vector<PluginDescriptor> get_all_loaded_plugin_descriptors() const;
|
||||
// the plugin's [tool.orcaslicer.plugin.settings] table (empty if the plugin is unknown).
|
||||
std::map<std::string, std::string> get_plugin_settings(const std::string& plugin_key) const;
|
||||
|
||||
|
||||
// Package descriptor accessor; returns nullptr when the package is not loaded.
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "PythonPluginBridge.hpp"
|
||||
#include "PluginFsUtils.hpp"
|
||||
#include "PluginHooks.hpp"
|
||||
#include "PythonFileUtils.hpp"
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
@@ -128,6 +129,10 @@ bool PluginManager::initialize()
|
||||
// leaves the store empty (see PluginConfig::load), never blocking startup.
|
||||
m_config.load();
|
||||
|
||||
// Install the libslic3r hooks (capability resolver, slicing-pipeline
|
||||
// dispatcher). Uninstalled in shutdown() before the interpreter finalizes.
|
||||
plugin_hooks::install();
|
||||
|
||||
// Persist auto-load / capability state to each plugin's .install_state.json sidecar.
|
||||
// On load: write enabled=true plus current capability flags. On unload: flip enabled=false.
|
||||
// The on-unload callback is skipped during shutdown (run_on_unload_callbacks is gated by
|
||||
@@ -162,6 +167,10 @@ void PluginManager::shutdown()
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": PluginManager shutdown enter";
|
||||
|
||||
// Detach the libslic3r hooks first so nothing dispatches into Python while
|
||||
// (or after) plugins unload. Callers stop background slicing before this.
|
||||
plugin_hooks::uninstall();
|
||||
|
||||
// Signal the loader to reject new plugin loads before we drain.
|
||||
m_loader.set_shutting_down();
|
||||
|
||||
|
||||
@@ -106,10 +106,14 @@ void execute_capabilities_from_refs(const ConfigOptionStrings& capabilities,
|
||||
{
|
||||
PluginManager& plugin_mgr = PluginManager::instance();
|
||||
|
||||
// Log prefix derived from the capability type so each capability family (Printer connection,
|
||||
// Slicing Pipeline, ...) tags its dispatch diagnostics with its own display name.
|
||||
const std::string tag = plugin_capability_type_display_name(type);
|
||||
|
||||
const bool has_any = std::any_of(capabilities.values.begin(), capabilities.values.end(),
|
||||
[](const std::string& s) { return !s.empty(); });
|
||||
if (has_any && !plugin_mgr.get_loader().wait_for_all_plugin_loads(std::chrono::seconds(10))) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Post-process: timed out waiting for plugin loads; unresolved capabilities will be skipped";
|
||||
BOOST_LOG_TRIVIAL(warning) << tag << ": timed out waiting for plugin loads; unresolved capabilities will be skipped";
|
||||
}
|
||||
|
||||
for (const std::string& capability : capabilities.values) {
|
||||
@@ -131,7 +135,7 @@ void execute_capabilities_from_refs(const ConfigOptionStrings& capabilities,
|
||||
}
|
||||
|
||||
if (!ref) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Post-processing: no plugin reference found for capability '" << capability << "'; skipping";
|
||||
BOOST_LOG_TRIVIAL(warning) << tag << ": no plugin reference found for capability '" << capability << "'; skipping";
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -140,19 +144,19 @@ void execute_capabilities_from_refs(const ConfigOptionStrings& capabilities,
|
||||
cap = plugin_mgr.get_loader().get_plugin_capability_by_name(plugin_key, type, cap_name);
|
||||
|
||||
if (!cap) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Post-processing: no loaded capability '" << cap_name
|
||||
BOOST_LOG_TRIVIAL(warning) << tag << ": no loaded capability '" << cap_name
|
||||
<< "' for plugin '" << plugin_key << "'; skipping";
|
||||
continue;
|
||||
}
|
||||
if (!cap->enabled) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Post-processing: capability '" << cap_name
|
||||
BOOST_LOG_TRIVIAL(warning) << tag << ": capability '" << cap_name
|
||||
<< "' for plugin '" << plugin_key << "' is disabled; skipping";
|
||||
continue;
|
||||
}
|
||||
|
||||
auto plugin_capability = std::dynamic_pointer_cast<T>(cap->instance);
|
||||
if (!plugin_capability) {
|
||||
BOOST_LOG_TRIVIAL(warning) << "Post-processing: capability '" << cap_name
|
||||
BOOST_LOG_TRIVIAL(warning) << tag << ": capability '" << cap_name
|
||||
<< "' (plugin_key=" << cap->plugin_key
|
||||
<< ") is not a " << plugin_capability_type_to_string(type) << "; skipping";
|
||||
continue;
|
||||
|
||||
@@ -35,9 +35,9 @@ std::string find_option_for_capability(Preset::Type type, const Preset& preset,
|
||||
if (type != Preset::TYPE_PRINT && type != Preset::TYPE_PRINTER && type != Preset::TYPE_FILAMENT)
|
||||
return {};
|
||||
|
||||
// Plugin-bearing options opt in via ConfigOptionDef::support_plugin, so scan the preset's
|
||||
// definition rather than maintaining a hardcoded per-type field list. A typed preset's config
|
||||
// only contains keys for its own type, so this naturally stays scoped to `type`.
|
||||
// Plugin-bearing options opt in via ConfigOptionDef::is_plugin_backed (a non-empty plugin_type),
|
||||
// so scan the preset's definition rather than maintaining a hardcoded per-type field list. A typed
|
||||
// preset's config only contains keys for its own type, so this naturally stays scoped to `type`.
|
||||
const ConfigDef* def = preset.config.def();
|
||||
if (def == nullptr)
|
||||
return {};
|
||||
@@ -48,7 +48,7 @@ std::string find_option_for_capability(Preset::Type type, const Preset& preset,
|
||||
|
||||
for (const std::string& field : preset.config.keys()) {
|
||||
const ConfigOptionDef* opt_def = def->get(field);
|
||||
if (opt_def == nullptr || !opt_def->support_plugin)
|
||||
if (opt_def == nullptr || !opt_def->is_plugin_backed())
|
||||
continue;
|
||||
|
||||
const ConfigOption* option = preset.config.option(field);
|
||||
|
||||
@@ -128,7 +128,7 @@ bool read_zip_text_file(mz_zip_archive& archive, const char* filename, std::stri
|
||||
}
|
||||
|
||||
// TOML section parsing states.
|
||||
enum class TomlSection { Root, OrcaPlugin, InDepsArray };
|
||||
enum class TomlSection { Root, OrcaPlugin, OrcaPluginSettings, InDepsArray };
|
||||
|
||||
// Strip a quoted string value: "foo" → foo, 'foo' → foo.
|
||||
// Returns the unquoted value or the input unchanged if not quoted.
|
||||
@@ -187,6 +187,7 @@ bool parse_pep723_toml(const std::string& toml_content,
|
||||
std::string& out_description,
|
||||
std::string& out_author,
|
||||
std::string& out_version,
|
||||
std::map<std::string, std::string>& out_settings,
|
||||
std::string& error)
|
||||
{
|
||||
out_deps.clear();
|
||||
@@ -195,6 +196,7 @@ bool parse_pep723_toml(const std::string& toml_content,
|
||||
out_description.clear();
|
||||
out_author.clear();
|
||||
out_version.clear();
|
||||
out_settings.clear();
|
||||
|
||||
TomlSection section = TomlSection::Root;
|
||||
|
||||
@@ -218,6 +220,8 @@ bool parse_pep723_toml(const std::string& toml_content,
|
||||
if (trimmed[0] == '[') {
|
||||
if (trimmed == "[tool.orcaslicer.plugin]") {
|
||||
section = TomlSection::OrcaPlugin;
|
||||
} else if (trimmed == "[tool.orcaslicer.plugin.settings]") {
|
||||
section = TomlSection::OrcaPluginSettings; // per-plugin params table
|
||||
} else {
|
||||
section = TomlSection::Root; // Unknown section — skip.
|
||||
}
|
||||
@@ -270,6 +274,10 @@ bool parse_pep723_toml(const std::string& toml_content,
|
||||
else if (key == "description") out_description = unquote_toml_string(val);
|
||||
else if (key == "author") out_author = unquote_toml_string(val);
|
||||
else if (key == "version") out_version = unquote_toml_string(val);
|
||||
} else if (section == TomlSection::OrcaPluginSettings) {
|
||||
// collect every key as a string; the plugin parses (int/float/...) what it needs.
|
||||
if (!key.empty())
|
||||
out_settings[key] = unquote_toml_string(val);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -673,6 +681,7 @@ bool read_python_plugin_metadata(const boost::filesystem::path& py_path, PluginD
|
||||
pep_desc,
|
||||
pep_author,
|
||||
pep_version,
|
||||
descriptor.settings,
|
||||
pep723_error)) {
|
||||
error = "Failed to parse PEP 723 metadata: " + pep723_error;
|
||||
return false;
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
#include "PythonInterpreter.hpp"
|
||||
#include "PythonJsonUtils.hpp"
|
||||
#include "PluginConfig.hpp"
|
||||
#include "PluginHostApi.hpp"
|
||||
#include "host/PluginHost.hpp"
|
||||
#include "PyPluginPackage.hpp"
|
||||
#include "PyPluginTrampoline.hpp"
|
||||
#include "pluginTypes/gcode/GCodePluginCapability.hpp"
|
||||
#include "pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp"
|
||||
#include "pluginTypes/script/ScriptPluginCapability.hpp"
|
||||
#include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp"
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
@@ -289,7 +289,6 @@ void bind_python_api(pybind11::module_& m)
|
||||
m.doc() = "OrcaSlicer plugin API";
|
||||
|
||||
auto pluginTypes = py::enum_<PluginCapabilityType>(m, "PluginType", "Available plugin capability groups")
|
||||
.value("PostProcessing", PluginCapabilityType::PostProcessing)
|
||||
.value("PrinterConnection", PluginCapabilityType::PrinterConnection)
|
||||
.value("Automation", PluginCapabilityType::Automation)
|
||||
.value("Analysis", PluginCapabilityType::Analysis)
|
||||
@@ -297,6 +296,7 @@ void bind_python_api(pybind11::module_& m)
|
||||
.value("Exporter", PluginCapabilityType::Exporter)
|
||||
.value("Visualization", PluginCapabilityType::Visualization)
|
||||
.value("Script", PluginCapabilityType::Script)
|
||||
.value("SlicingPipeline", PluginCapabilityType::SlicingPipeline)
|
||||
.value("Unknown", PluginCapabilityType::Unknown)
|
||||
.export_values();
|
||||
|
||||
@@ -380,10 +380,10 @@ void bind_python_api(pybind11::module_& m)
|
||||
BOOST_LOG_TRIVIAL(debug) << "Registering embedded Python plugin type bindings";
|
||||
|
||||
// Make sure you register your bindings here
|
||||
GCodePluginCapability::RegisterBindings(m, pluginTypes);
|
||||
PrinterAgentPluginCapability::RegisterBindings(m, pluginTypes);
|
||||
ScriptPluginCapability::RegisterBindings(m, pluginTypes);
|
||||
PluginHostApi::RegisterBindings(m);
|
||||
SlicingPipelinePluginCapability::RegisterBindings(m, pluginTypes);
|
||||
PluginHost::RegisterBindings(m);
|
||||
BOOST_LOG_TRIVIAL(debug) << "Registered ScriptPluginCapability Python bindings";
|
||||
|
||||
m.def(
|
||||
|
||||
@@ -11,12 +11,11 @@
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
enum class PluginCapabilityType { PostProcessing = 0, PrinterConnection, Automation, Analysis, Importer, Exporter, Visualization, Script, Unknown };
|
||||
enum class PluginCapabilityType { PrinterConnection = 0, Automation, Analysis, Importer, Exporter, Visualization, Script, SlicingPipeline, Unknown };
|
||||
|
||||
inline std::string plugin_capability_type_to_string(PluginCapabilityType type)
|
||||
{
|
||||
switch (type) {
|
||||
case PluginCapabilityType::PostProcessing: return "post-processing";
|
||||
case PluginCapabilityType::PrinterConnection: return "printer-connection";
|
||||
case PluginCapabilityType::Automation: return "automation";
|
||||
case PluginCapabilityType::Analysis: return "analysis";
|
||||
@@ -24,6 +23,7 @@ inline std::string plugin_capability_type_to_string(PluginCapabilityType type)
|
||||
case PluginCapabilityType::Exporter: return "exporter";
|
||||
case PluginCapabilityType::Visualization: return "visualization";
|
||||
case PluginCapabilityType::Script: return "script";
|
||||
case PluginCapabilityType::SlicingPipeline: return "slicing-pipeline";
|
||||
default: return "unknown";
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@ inline std::string plugin_capability_type_to_string(PluginCapabilityType type)
|
||||
inline std::string plugin_capability_type_display_name(PluginCapabilityType type)
|
||||
{
|
||||
switch (type) {
|
||||
case PluginCapabilityType::PostProcessing: return "Post-processing";
|
||||
case PluginCapabilityType::PrinterConnection: return "Printer connection";
|
||||
case PluginCapabilityType::Automation: return "Automation";
|
||||
case PluginCapabilityType::Analysis: return "Analysis";
|
||||
@@ -39,6 +38,7 @@ inline std::string plugin_capability_type_display_name(PluginCapabilityType type
|
||||
case PluginCapabilityType::Exporter: return "Exporter";
|
||||
case PluginCapabilityType::Visualization: return "Visualization";
|
||||
case PluginCapabilityType::Script: return "Script";
|
||||
case PluginCapabilityType::SlicingPipeline: return "Slicing Pipeline";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
@@ -52,8 +52,6 @@ inline PluginCapabilityType plugin_capability_type_from_string(std::string_view
|
||||
lowered.push_back(to_lower(ch));
|
||||
}
|
||||
|
||||
if (lowered == "post-processing")
|
||||
return PluginCapabilityType::PostProcessing;
|
||||
if (lowered == "printer-connection")
|
||||
return PluginCapabilityType::PrinterConnection;
|
||||
if (lowered == "automation")
|
||||
@@ -68,6 +66,8 @@ inline PluginCapabilityType plugin_capability_type_from_string(std::string_view
|
||||
return PluginCapabilityType::Visualization;
|
||||
if (lowered == "script")
|
||||
return PluginCapabilityType::Script;
|
||||
if (lowered == "slicing-pipeline")
|
||||
return PluginCapabilityType::SlicingPipeline;
|
||||
return PluginCapabilityType::Unknown;
|
||||
}
|
||||
|
||||
|
||||
26
src/slic3r/plugin/host/PluginHost.cpp
Normal file
26
src/slic3r/plugin/host/PluginHost.cpp
Normal file
@@ -0,0 +1,26 @@
|
||||
#include "PluginHost.hpp"
|
||||
#include "PluginHostBindings.hpp"
|
||||
#include "PluginHostUi.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void PluginHost::RegisterBindings(pybind11::module_& module)
|
||||
{
|
||||
auto host = module.def_submodule("host", "Host application API");
|
||||
|
||||
// Value types first so the docstring signatures of later registrars
|
||||
// resolve to the bound Python names.
|
||||
host_bindings::register_geometry(host);
|
||||
host_bindings::register_mesh(host);
|
||||
host_bindings::register_presets(host);
|
||||
host_bindings::register_model(host);
|
||||
host_bindings::register_app(host);
|
||||
|
||||
// UI: native dialogs and interactive HTML windows for plugins.
|
||||
PluginHostUi::RegisterBindings(host);
|
||||
|
||||
// Slicing print-graph data model (Print, Layer, Surface, ...).
|
||||
host_bindings::register_slicing(host);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
17
src/slic3r/plugin/host/PluginHost.hpp
Normal file
17
src/slic3r/plugin/host/PluginHost.hpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Entry point of the `orca.host` Python API surface. Each domain of the
|
||||
// surface (geometry, mesh, presets, model, app access, ui, slicing graph)
|
||||
// lives in its own translation unit in this directory; RegisterBindings
|
||||
// creates the submodule and runs the per-domain registrars.
|
||||
class PluginHost
|
||||
{
|
||||
public:
|
||||
static void RegisterBindings(pybind11::module_& module);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
60
src/slic3r/plugin/host/PluginHostApp.cpp
Normal file
60
src/slic3r/plugin/host/PluginHostApp.cpp
Normal file
@@ -0,0 +1,60 @@
|
||||
#include "PluginHostBindings.hpp"
|
||||
|
||||
#include <libslic3r/Model.hpp>
|
||||
#include <libslic3r/PresetBundle.hpp>
|
||||
#include <slic3r/GUI/GUI_App.hpp>
|
||||
#include <slic3r/GUI/Plater.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
namespace Slic3r {
|
||||
namespace {
|
||||
|
||||
GUI::Plater* current_plater()
|
||||
{
|
||||
if (wxTheApp == nullptr)
|
||||
throw std::runtime_error("OrcaSlicer application is not initialized");
|
||||
|
||||
GUI::Plater* plater = GUI::wxGetApp().plater();
|
||||
if (plater == nullptr)
|
||||
throw std::runtime_error("Plater is not available");
|
||||
|
||||
return plater;
|
||||
}
|
||||
|
||||
PresetBundle* current_preset_bundle()
|
||||
{
|
||||
if (wxTheApp == nullptr)
|
||||
throw std::runtime_error("OrcaSlicer application is not initialized");
|
||||
|
||||
PresetBundle* preset_bundle = GUI::wxGetApp().preset_bundle;
|
||||
if (preset_bundle == nullptr)
|
||||
throw std::runtime_error("Preset bundle is not available");
|
||||
|
||||
return preset_bundle;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Access to the live GUI application: the Plater and the module-level
|
||||
// plater()/model()/preset_bundle() accessors. Everything here is owned by the
|
||||
// app and only reachable once the GUI is up (the accessors throw before that).
|
||||
void host_bindings::register_app(py::module_& host)
|
||||
{
|
||||
py::class_<GUI::Plater, std::unique_ptr<GUI::Plater, py::nodelete>>(host, "Plater")
|
||||
.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_presets_dirty", &GUI::Plater::is_presets_dirty)
|
||||
.def("inside_snapshot_capture", &GUI::Plater::inside_snapshot_capture);
|
||||
|
||||
host.def("plater", ¤t_plater, py::return_value_policy::reference);
|
||||
host.def("model", []() -> Model& {
|
||||
return current_plater()->model();
|
||||
}, py::return_value_policy::reference);
|
||||
host.def("preset_bundle", ¤t_preset_bundle, py::return_value_policy::reference);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
16
src/slic3r/plugin/host/PluginHostBindings.hpp
Normal file
16
src/slic3r/plugin/host/PluginHostBindings.hpp
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
// Internal to plugin/host/: the per-domain registrars of the `orca.host`
|
||||
// surface, one per translation unit, called by PluginHost::RegisterBindings.
|
||||
namespace Slic3r::host_bindings {
|
||||
|
||||
void register_geometry(pybind11::module_& host); // PluginHostGeometry.cpp
|
||||
void register_mesh(pybind11::module_& host); // PluginHostMesh.cpp
|
||||
void register_presets(pybind11::module_& host); // PluginHostPresets.cpp
|
||||
void register_model(pybind11::module_& host); // PluginHostModel.cpp
|
||||
void register_app(pybind11::module_& host); // PluginHostApp.cpp
|
||||
void register_slicing(pybind11::module_& host); // PluginHostSlicing.cpp
|
||||
|
||||
} // namespace Slic3r::host_bindings
|
||||
216
src/slic3r/plugin/host/PluginHostGeometry.cpp
Normal file
216
src/slic3r/plugin/host/PluginHostGeometry.cpp
Normal file
@@ -0,0 +1,216 @@
|
||||
#include "PluginHostBindings.hpp"
|
||||
#include "slic3r/plugin/PluginBindingUtils.hpp"
|
||||
|
||||
#include <libslic3r/BoundingBox.hpp>
|
||||
#include <libslic3r/ClipperUtils.hpp> // offset/offset_ex/union_ex/diff_ex/intersection_ex
|
||||
#include <libslic3r/ExPolygon.hpp>
|
||||
|
||||
#include <pybind11/stl.h>
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
namespace Slic3r {
|
||||
namespace {
|
||||
// --- Input path: Python geometry -> C++ Polygon/ExPolygon, with validation. ---------------
|
||||
// The mutators take scaled integer coords (the same units the read views hand out). A Python
|
||||
// raise here surfaces as ValueError (pybind translates) so malformed input is rejected up
|
||||
// front rather than silently corrupting the slicing graph.
|
||||
|
||||
// One (N,2) int64 ndarray -> Polygon. Rejects wrong dtype/shape and degenerate (<3 pt) rings.
|
||||
// Float / NaN / inf are rejected implicitly: only a signed-integer, 8-byte (coord_t==int64)
|
||||
// dtype is accepted, and integer arrays cannot hold NaN/inf.
|
||||
Polygon parse_polygon(py::handle h, const char* who)
|
||||
{
|
||||
if (!py::isinstance<py::array>(h))
|
||||
throw py::value_error(std::string(who) + ": each contour/hole must be an (N,2) int64 ndarray");
|
||||
py::array a = py::reinterpret_borrow<py::array>(h);
|
||||
if (a.dtype().kind() != 'i' || a.itemsize() != (py::ssize_t) sizeof(coord_t))
|
||||
throw py::value_error(std::string(who) + ": polygon coordinates must be int64 (scaled coords)");
|
||||
if (a.ndim() != 2 || a.shape(1) != 2)
|
||||
throw py::value_error(std::string(who) + ": each polygon array must have shape (N,2)");
|
||||
if (a.shape(0) < 3)
|
||||
throw py::value_error(std::string(who) + ": a polygon needs at least 3 points");
|
||||
// dtype already validated as int64; forcecast here only guarantees a C-contiguous buffer.
|
||||
auto arr = py::array_t<coord_t, py::array::c_style | py::array::forcecast>::ensure(a);
|
||||
if (!arr)
|
||||
throw py::value_error(std::string(who) + ": could not read polygon as a contiguous int64 array");
|
||||
auto r = arr.unchecked<2>();
|
||||
Polygon poly;
|
||||
poly.points.reserve((size_t) arr.shape(0));
|
||||
for (py::ssize_t i = 0; i < arr.shape(0); ++i)
|
||||
poly.points.emplace_back((coord_t) r(i, 0), (coord_t) r(i, 1));
|
||||
return poly;
|
||||
}
|
||||
|
||||
// Accept a bound orca.host.Polygon (copied) or an (N,2) int64 ndarray. Used by the ExPolygon
|
||||
// binding, whose constructor/contour-setter/set_holes must accept the Polygon it itself hands
|
||||
// out (e.g. `ExPolygon(some_polygon_ref)`) in addition to the ndarray-only parse_polygon() path.
|
||||
Polygon as_polygon(py::handle h, const char* who)
|
||||
{
|
||||
if (py::isinstance<Polygon>(h))
|
||||
return h.cast<Polygon>();
|
||||
return parse_polygon(h, who);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void host_bindings::register_geometry(py::module_& host)
|
||||
{
|
||||
// ------------------------------------------------------------------
|
||||
// Geometry value types of the `orca.host` surface. All use pybind's
|
||||
// default holder, so plugins can construct and own instances. When
|
||||
// obtained from the live slicing graph they are non-owning references
|
||||
// instead — see the lifetime rule in PluginHostSlicing.cpp.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
// Axis-aligned bounding box, returned by value (a copy) so its lifetime is
|
||||
// independent of the model object it was computed from. Coordinates are in mm.
|
||||
py::class_<BoundingBoxf3>(host, "BoundingBox", "Axis-aligned bounding box in millimetres")
|
||||
.def_property_readonly("defined", [](const BoundingBoxf3& bb) { return bb.defined; })
|
||||
.def_property_readonly("min", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.min); })
|
||||
.def_property_readonly("max", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.max); })
|
||||
.def_property_readonly("size", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.size()); })
|
||||
.def_property_readonly("center", [](const BoundingBoxf3& bb) { return vec3_to_tuple(bb.center()); })
|
||||
.def_property_readonly("radius", [](const BoundingBoxf3& bb) { return bb.radius(); });
|
||||
|
||||
// Point: a constructible value type (default holder, so Python-owned instances
|
||||
// are freed). Returned-by-reference from Polygon.points, it aliases the buffer;
|
||||
// x()/y() are Eigen lvalues, so the properties are read/write. p+q / p-q go
|
||||
// through Eigen expression templates, wrapped back into a Point.
|
||||
py::class_<Point>(host, "Point")
|
||||
.def(py::init([](coord_t x, coord_t y) { return Point(x, y); }), py::arg("x"), py::arg("y"))
|
||||
.def_property("x", [](const Point& p) { return p.x(); },
|
||||
[](Point& p, coord_t v) { p.x() = v; })
|
||||
.def_property("y", [](const Point& p) { return p.y(); },
|
||||
[](Point& p, coord_t v) { p.y() = v; })
|
||||
.def("__add__", [](const Point& a, const Point& b) { return Point(a + b); }, py::is_operator())
|
||||
.def("__sub__", [](const Point& a, const Point& b) { return Point(a - b); }, py::is_operator())
|
||||
.def("__mul__", [](const Point& a, double s) { return Point(a.x() * s, a.y() * s); }, py::is_operator())
|
||||
.def("__repr__", [](const Point& p) {
|
||||
return "orca.host.Point(" + std::to_string(p.x()) + ", " + std::to_string(p.y()) + ")";
|
||||
});
|
||||
|
||||
py::class_<Polygon>(host, "Polygon")
|
||||
.def(py::init<>())
|
||||
.def("size", [](const Polygon& p) { return p.points.size(); })
|
||||
.def("is_valid", [](const Polygon& p) { return p.is_valid(); })
|
||||
.def("is_counter_clockwise", [](const Polygon& p) { return p.is_counter_clockwise(); })
|
||||
.def("is_clockwise", [](const Polygon& p) { return p.is_clockwise(); })
|
||||
.def("make_counter_clockwise", [](Polygon& p) { return p.make_counter_clockwise(); },
|
||||
"Reorient to CCW in place. Returns True if it reversed the winding.")
|
||||
.def("make_clockwise", [](Polygon& p) { return p.make_clockwise(); })
|
||||
.def("area", [](const Polygon& p) { return p.area(); })
|
||||
.def("centroid", [](const Polygon& p) { return p.centroid(); })
|
||||
.def("contains", [](const Polygon& p, const Point& pt) { return p.contains(pt); }, py::arg("point"))
|
||||
.def("translate", [](Polygon& p, double x, double y) { p.translate(x, y); }, py::arg("x"), py::arg("y"))
|
||||
.def("rotate", [](Polygon& p, double angle) { p.rotate(angle); }, py::arg("angle"))
|
||||
.def("rotate", [](Polygon& p, double angle, const Point& c) { p.rotate(angle, c); },
|
||||
py::arg("angle"), py::arg("center"))
|
||||
.def("douglas_peucker", [](Polygon& p, double tol) { p.douglas_peucker(tol); }, py::arg("tolerance"))
|
||||
.def("simplify", [](const Polygon& p, double tol) { return p.simplify(tol); }, py::arg("tolerance"),
|
||||
"Return simplified geometry as a list of Polygon (may split into several).")
|
||||
.def("offset", [](const Polygon& p, coord_t delta) { return offset(p, (float) delta); }, py::arg("delta"),
|
||||
"Clipper offset by `delta` scaled units (negative shrinks). Returns [Polygon].")
|
||||
// --- Point-object idiom: references into the buffer (in-place element edit). ---
|
||||
.def_property_readonly("points", [](py::object self) {
|
||||
Polygon& p = self.cast<Polygon&>();
|
||||
py::list out;
|
||||
for (Point& pt : p.points)
|
||||
out.append(py::cast(&pt, py::return_value_policy::reference_internal, self));
|
||||
return out;
|
||||
}, "Vertices as [Point] references into this polygon. Editing a Point mutates the "
|
||||
"buffer in place. Structural changes (count) go through set_points/append, which "
|
||||
"invalidate previously returned Point refs and array views (C++ vector semantics).")
|
||||
.def("append", [](Polygon& p, const Point& pt) { p.points.push_back(pt); }, py::arg("point"),
|
||||
"Append a vertex. Structural change (count): invalidates previously returned "
|
||||
"Point refs and array views into this polygon (C++ vector semantics).")
|
||||
// --- numpy idiom: writable zero-copy (N,2) view (bulk affine edits). ---
|
||||
.def("as_array", [](py::object self) {
|
||||
Polygon& p = self.cast<Polygon&>();
|
||||
return with_numpy([&] {
|
||||
return py::object(make_writable_rows<coord_t, 2>(
|
||||
self, p.points.empty() ? nullptr : p.points.front().data(),
|
||||
(py::ssize_t) p.points.size()));
|
||||
});
|
||||
}, "Vertices as a WRITABLE int64 (N,2) numpy view in scaled coords, aliasing the "
|
||||
"buffer. Count-preserving in-place edits only; valid during execute(ctx). Requires numpy.")
|
||||
.def("set_points", [](Polygon& p, py::handle src) { p = parse_polygon(src, "Polygon.set_points"); },
|
||||
py::arg("points"),
|
||||
"Replace all vertices from an (N,2) int64 ndarray (scaled coords). Count-changing; "
|
||||
"invalidates prior Point refs and array views. Raises ValueError on malformed input.");
|
||||
|
||||
// ExPolygon: default holder (Python-owned instances are freed) so plugins can construct
|
||||
// their own geometry, not just navigate the live slicing graph. contour/holes accessors
|
||||
// still use reference_internal, so refs into a graph-owned ExPolygon stay non-owning views
|
||||
// tied to that owner's lifetime, same as Polygon/Surface.
|
||||
py::class_<ExPolygon>(host, "ExPolygon")
|
||||
.def(py::init([](py::handle contour, py::handle holes) {
|
||||
// Accept bound Polygons or (N,2) ndarrays for both contour and each hole.
|
||||
ExPolygon ex;
|
||||
ex.contour = as_polygon(contour, "ExPolygon.contour");
|
||||
if (!holes.is_none()) {
|
||||
if (!py::isinstance<py::sequence>(holes) || py::isinstance<py::str>(holes))
|
||||
throw py::value_error("ExPolygon: holes must be a list of Polygon or (N,2) ndarrays");
|
||||
for (py::handle h : py::reinterpret_borrow<py::sequence>(holes)) {
|
||||
Polygon hole = as_polygon(h, "ExPolygon.hole");
|
||||
hole.make_clockwise();
|
||||
ex.holes.emplace_back(std::move(hole));
|
||||
}
|
||||
}
|
||||
ex.contour.make_counter_clockwise();
|
||||
return ex;
|
||||
}), py::arg("contour"), py::arg("holes") = py::none(),
|
||||
"Construct from a Polygon/ndarray contour and optional list of hole Polygons/ndarrays. "
|
||||
"Orientation is normalized (contour CCW, holes CW).")
|
||||
.def_property("contour",
|
||||
[](ExPolygon& e) -> Polygon& { return e.contour; },
|
||||
[](ExPolygon& e, py::handle v) { e.contour = as_polygon(v, "ExPolygon.contour"); },
|
||||
py::return_value_policy::reference_internal,
|
||||
"Outer contour (CCW). Read returns a live Polygon ref; assign a Polygon/ndarray to replace it.")
|
||||
.def_property_readonly("holes", [](py::object self) {
|
||||
ExPolygon& e = self.cast<ExPolygon&>();
|
||||
py::list out;
|
||||
for (Polygon& h : e.holes)
|
||||
out.append(py::cast(&h, py::return_value_policy::reference_internal, self));
|
||||
return out;
|
||||
}, "Hole contours (CW) as [Polygon] references (in-place editable). set_holes replaces them.")
|
||||
.def("set_holes", [](ExPolygon& e, py::handle holes) {
|
||||
ExPolygon tmp;
|
||||
if (!py::isinstance<py::sequence>(holes) || py::isinstance<py::str>(holes))
|
||||
throw py::value_error("set_holes: expected a list of Polygon or (N,2) ndarrays");
|
||||
for (py::handle h : py::reinterpret_borrow<py::sequence>(holes)) {
|
||||
Polygon hole = as_polygon(h, "ExPolygon.set_holes");
|
||||
hole.make_clockwise();
|
||||
tmp.holes.emplace_back(std::move(hole));
|
||||
}
|
||||
e.holes = std::move(tmp.holes);
|
||||
}, py::arg("holes"), "Replace all holes. Invalidates prior hole refs (C++ vector semantics).")
|
||||
.def("translate", [](ExPolygon& e, double x, double y) { e.translate(x, y); }, py::arg("x"), py::arg("y"))
|
||||
.def("rotate", [](ExPolygon& e, double a) { e.rotate(a); }, py::arg("angle"))
|
||||
.def("rotate", [](ExPolygon& e, double a, const Point& c) { e.rotate(a, c); },
|
||||
py::arg("angle"), py::arg("center"))
|
||||
.def("scale", [](ExPolygon& e, double f) { e.scale(f); }, py::arg("factor"))
|
||||
.def("douglas_peucker", [](ExPolygon& e, double t) { e.douglas_peucker(t); }, py::arg("tolerance"))
|
||||
.def("area", [](const ExPolygon& e) { return e.area(); })
|
||||
.def("is_valid", [](const ExPolygon& e) { return e.is_valid(); })
|
||||
.def("contains", [](const ExPolygon& e, const Point& p) { return e.contains(p); }, py::arg("point"))
|
||||
.def("num_contours", [](const ExPolygon& e) { return e.num_contours(); })
|
||||
.def("simplify", [](const ExPolygon& e, double t) { return e.simplify(t); }, py::arg("tolerance"),
|
||||
"Return simplified geometry as [ExPolygon].")
|
||||
.def("offset", [](const ExPolygon& e, coord_t delta) { return offset_ex(e, (float) delta); },
|
||||
py::arg("delta"), "Clipper offset by `delta` scaled units (negative shrinks). Returns [ExPolygon].")
|
||||
.def("union_ex", [](const ExPolygon& a, const ExPolygon& b) {
|
||||
return union_ex(ExPolygons{ a, b });
|
||||
}, py::arg("other"), "Union with another ExPolygon. Returns [ExPolygon].")
|
||||
.def("diff_ex", [](const ExPolygon& a, const ExPolygon& b) {
|
||||
return diff_ex(ExPolygons{ a }, ExPolygons{ b });
|
||||
}, py::arg("other"), "This minus `other`. Returns [ExPolygon].")
|
||||
.def("intersection_ex", [](const ExPolygon& a, const ExPolygon& b) {
|
||||
return intersection_ex(ExPolygons{ a }, ExPolygons{ b });
|
||||
}, py::arg("other"), "Intersection with `other`. Returns [ExPolygon].");
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
101
src/slic3r/plugin/host/PluginHostMesh.cpp
Normal file
101
src/slic3r/plugin/host/PluginHostMesh.cpp
Normal file
@@ -0,0 +1,101 @@
|
||||
#include "PluginHostBindings.hpp"
|
||||
#include "PluginHostMesh.hpp"
|
||||
#include "slic3r/plugin/PluginBindingUtils.hpp"
|
||||
|
||||
#include <pybind11/numpy.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
namespace Slic3r {
|
||||
namespace {
|
||||
|
||||
// Zero-copy export of its.vertices / its.indices relies on these Eigen
|
||||
// row-vectors being tightly packed (no padding between the 3 components).
|
||||
static_assert(sizeof(stl_vertex) == 3 * sizeof(float),
|
||||
"stl_vertex must be a packed float[3] for zero-copy numpy export");
|
||||
static_assert(sizeof(stl_triangle_vertex_indices) == 3 * sizeof(std::int32_t),
|
||||
"triangle index must be a packed int32[3] for zero-copy numpy export");
|
||||
|
||||
} // namespace
|
||||
|
||||
void host_bindings::register_mesh(py::module_& host)
|
||||
{
|
||||
// The raw libslic3r TriangleMesh, bound with a shared_ptr holder:
|
||||
// ModelVolume.mesh() hands out the volume's own shared_ptr, so the Python
|
||||
// object pins this snapshot even if the volume's mesh is later replaced on
|
||||
// the main thread. The zero-copy views below use the Python object as their
|
||||
// array base, which keeps the buffer alive for each array's lifetime.
|
||||
//
|
||||
// IMMUTABLE BY RULE: handed-out meshes are copy-on-write snapshots SHARED
|
||||
// across threads (a Print's model snapshot and the live GUI model share the
|
||||
// same instance), reached through a const_pointer_cast that only serves the
|
||||
// holder type. Bind only const/read-only methods here. A future mutable-mesh
|
||||
// API must operate on plugin-owned copies handed back via
|
||||
// ModelVolume::set_mesh — never mutate a mesh obtained from the graph.
|
||||
py::class_<TriangleMesh, std::shared_ptr<TriangleMesh>>(host, "TriangleMesh",
|
||||
"Immutable snapshot of a ModelVolume's mesh in local (untransformed) coordinates, mm.")
|
||||
.def("vertex_count", [](const TriangleMesh& mesh) { return mesh.its.vertices.size(); })
|
||||
.def("triangle_count", [](const TriangleMesh& mesh) { return mesh.its.indices.size(); })
|
||||
.def("facets_count", [](const TriangleMesh& mesh) { return mesh.its.indices.size(); })
|
||||
.def("is_empty", [](const TriangleMesh& mesh) { return mesh.its.indices.empty(); })
|
||||
// Read-only, zero-copy (N, 3) float32 view of vertex positions. Requires numpy.
|
||||
.def("vertices", [](py::object self) {
|
||||
const TriangleMesh& mesh = self.cast<const TriangleMesh&>();
|
||||
return with_numpy([&] {
|
||||
const std::vector<stl_vertex>& vertices = mesh.its.vertices;
|
||||
return py::object(make_readonly_rows<float, 3>(
|
||||
self, vertices.empty() ? nullptr : vertices.front().data(),
|
||||
static_cast<py::ssize_t>(vertices.size())));
|
||||
});
|
||||
}, "Read-only zero-copy (N, 3) float32 ndarray of vertex positions (local mm). Requires numpy.")
|
||||
// Read-only, zero-copy (M, 3) int32 view of triangle vertex indices. Requires numpy.
|
||||
.def("triangles", [](py::object self) {
|
||||
const TriangleMesh& mesh = self.cast<const TriangleMesh&>();
|
||||
return with_numpy([&] {
|
||||
const std::vector<stl_triangle_vertex_indices>& indices = mesh.its.indices;
|
||||
return py::object(make_readonly_rows<std::int32_t, 3>(
|
||||
self, indices.empty() ? nullptr : indices.front().data(),
|
||||
static_cast<py::ssize_t>(indices.size())));
|
||||
});
|
||||
}, "Read-only zero-copy (M, 3) int32 ndarray of triangle vertex indices. Requires numpy.")
|
||||
// One normalized normal per triangle as an (M, 3) float32 copy. Requires numpy.
|
||||
.def("face_normals", [](const TriangleMesh& mesh) {
|
||||
return with_numpy([&] {
|
||||
std::vector<Vec3f> normals = its_face_normals(mesh.its);
|
||||
py::array_t<float> array({ static_cast<py::ssize_t>(normals.size()), py::ssize_t(3) });
|
||||
if (!normals.empty()) {
|
||||
auto view = array.mutable_unchecked<2>();
|
||||
for (size_t i = 0; i < normals.size(); ++i) {
|
||||
view(i, 0) = normals[i].x();
|
||||
view(i, 1) = normals[i].y();
|
||||
view(i, 2) = normals[i].z();
|
||||
}
|
||||
}
|
||||
return py::object(std::move(array));
|
||||
});
|
||||
}, "Per-triangle normalized normals as an (M, 3) float32 ndarray (copy). Requires numpy.")
|
||||
// numpy-free element access, bounds-checked.
|
||||
.def("vertex", [](const TriangleMesh& mesh, size_t index) {
|
||||
const std::vector<stl_vertex>& vertices = mesh.its.vertices;
|
||||
if (index >= vertices.size())
|
||||
throw py::index_error("vertex index out of range");
|
||||
const stl_vertex& vertex = vertices[index];
|
||||
return py::make_tuple(vertex.x(), vertex.y(), vertex.z());
|
||||
})
|
||||
.def("triangle", [](const TriangleMesh& mesh, size_t index) {
|
||||
const std::vector<stl_triangle_vertex_indices>& indices = mesh.its.indices;
|
||||
if (index >= indices.size())
|
||||
throw py::index_error("triangle index out of range");
|
||||
const stl_triangle_vertex_indices& triangle = indices[index];
|
||||
return py::make_tuple(triangle[0], triangle[1], triangle[2]);
|
||||
})
|
||||
.def("volume", [](const TriangleMesh& mesh) { return mesh.stats().volume; })
|
||||
.def("bounding_box", [](const TriangleMesh& mesh) { return bbox_from_stats(mesh.stats()); })
|
||||
.def("is_manifold", [](const TriangleMesh& mesh) { return mesh.stats().manifold(); });
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
18
src/slic3r/plugin/host/PluginHostMesh.hpp
Normal file
18
src/slic3r/plugin/host/PluginHostMesh.hpp
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <libslic3r/BoundingBox.hpp>
|
||||
#include <libslic3r/TriangleMesh.hpp>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Build a BoundingBoxf3 from precomputed (float) triangle-mesh stats min/max.
|
||||
// Shared by the TriangleMesh binding (PluginHostMesh.cpp) and the mesh-derived
|
||||
// ModelVolume accessors (PluginHostModel.cpp).
|
||||
inline BoundingBoxf3 bbox_from_stats(const TriangleMeshStats& stats)
|
||||
{
|
||||
if (stats.number_of_facets == 0)
|
||||
return BoundingBoxf3();
|
||||
return BoundingBoxf3(stats.min.cast<double>(), stats.max.cast<double>());
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
208
src/slic3r/plugin/host/PluginHostModel.cpp
Normal file
208
src/slic3r/plugin/host/PluginHostModel.cpp
Normal file
@@ -0,0 +1,208 @@
|
||||
#include "PluginHostBindings.hpp"
|
||||
#include "PluginHostMesh.hpp"
|
||||
#include "slic3r/plugin/PluginBindingUtils.hpp"
|
||||
|
||||
#include <libslic3r/Model.hpp>
|
||||
|
||||
#include <pybind11/stl.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// The scene/document graph: Model -> ModelObject -> ModelInstance/ModelVolume.
|
||||
// Everything is bound py::nodelete — non-owning references into a graph owned
|
||||
// by the app (the live Plater model) or by a Print's model snapshot.
|
||||
void host_bindings::register_model(py::module_& host)
|
||||
{
|
||||
py::enum_<ModelVolumeType>(host, "ModelVolumeType")
|
||||
.value("Invalid", ModelVolumeType::INVALID)
|
||||
.value("ModelPart", ModelVolumeType::MODEL_PART)
|
||||
.value("NegativeVolume", ModelVolumeType::NEGATIVE_VOLUME)
|
||||
.value("ParameterModifier", ModelVolumeType::PARAMETER_MODIFIER)
|
||||
.value("SupportBlocker", ModelVolumeType::SUPPORT_BLOCKER)
|
||||
.value("SupportEnforcer", ModelVolumeType::SUPPORT_ENFORCER);
|
||||
|
||||
py::class_<ModelVolume, std::unique_ptr<ModelVolume, py::nodelete>>(host, "ModelVolume")
|
||||
.def("id", [](const ModelVolume& volume) { return volume.id().id; })
|
||||
.def_readonly("name", &ModelVolume::name)
|
||||
.def("type", &ModelVolume::type)
|
||||
.def("is_model_part", &ModelVolume::is_model_part)
|
||||
.def("is_modifier", &ModelVolume::is_modifier)
|
||||
.def("is_negative_volume", &ModelVolume::is_negative_volume)
|
||||
.def("is_support_enforcer", &ModelVolume::is_support_enforcer)
|
||||
.def("is_support_blocker", &ModelVolume::is_support_blocker)
|
||||
.def("is_support_modifier", &ModelVolume::is_support_modifier)
|
||||
// Extruder ID is 1-based for FFF, -1 for SLA or support volumes.
|
||||
.def("extruder_id", &ModelVolume::extruder_id)
|
||||
.def("offset", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_offset()); })
|
||||
.def("rotation", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_rotation()); })
|
||||
.def("scaling_factor", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_scaling_factor()); })
|
||||
.def("mirror", [](const ModelVolume& volume) { return vec3_to_tuple(volume.get_mirror()); })
|
||||
// 4x4 float64 affine matrix mapping this volume into its parent object frame. Requires numpy.
|
||||
.def("matrix", [](const ModelVolume& volume) { return mat4_to_numpy(volume.get_matrix()); },
|
||||
"Volume-to-object 4x4 float64 affine matrix (copy). Requires numpy.")
|
||||
.def("facets_count", [](const ModelVolume& volume) { return volume.mesh().facets_count(); })
|
||||
// Raw (untransformed) mesh volume in mm^3; -1 if it was never computed.
|
||||
.def("volume", [](const ModelVolume& volume) { return volume.mesh().stats().volume; })
|
||||
// Bounding box of the raw (untransformed) mesh, in the volume's local frame.
|
||||
.def("bounding_box", [](const ModelVolume& volume) { return bbox_from_stats(volume.mesh().stats()); })
|
||||
.def("is_manifold", [](const ModelVolume& volume) { return volume.mesh().stats().manifold(); })
|
||||
// Full mesh geometry (vertices/triangles) as an immutable snapshot.
|
||||
.def("mesh", [](const ModelVolume& volume) {
|
||||
// The volume stores its mesh as shared_ptr<const TriangleMesh> (a
|
||||
// copy-on-write snapshot); the const_pointer_cast only serves the
|
||||
// binding's shared_ptr holder — the TriangleMesh binding exposes
|
||||
// read-only methods (see the immutability rule in PluginHostMesh.cpp).
|
||||
return std::const_pointer_cast<TriangleMesh>(volume.get_mesh_shared_ptr());
|
||||
}, "Return the volume's TriangleMesh (local coordinates) for vertex/triangle access.")
|
||||
.def("mesh_errors_count", [](const ModelVolume& volume) { return volume.get_repaired_errors_count(); })
|
||||
.def("is_fdm_support_painted", &ModelVolume::is_fdm_support_painted)
|
||||
.def("is_seam_painted", &ModelVolume::is_seam_painted)
|
||||
.def("is_mm_painted", &ModelVolume::is_mm_painted)
|
||||
.def("is_fuzzy_skin_painted", &ModelVolume::is_fuzzy_skin_painted)
|
||||
.def("config_keys", [](const ModelVolume& volume) { return volume.config.keys(); })
|
||||
.def("config_value", [](const ModelVolume& volume, const std::string& key) {
|
||||
return config_value_or_none(volume.config.get(), key);
|
||||
});
|
||||
|
||||
py::class_<ModelInstance, std::unique_ptr<ModelInstance, py::nodelete>>(host, "ModelInstance")
|
||||
.def("id", [](const ModelInstance& instance) { return instance.id().id; })
|
||||
.def_readonly("printable", &ModelInstance::printable)
|
||||
// True only if the object is printable, this instance is printable and it
|
||||
// currently sits fully inside the print volume (set during slicing).
|
||||
.def("is_printable", &ModelInstance::is_printable)
|
||||
.def("offset", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_offset()); })
|
||||
.def("rotation", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_rotation()); })
|
||||
.def("scaling_factor", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_scaling_factor()); })
|
||||
.def("mirror", [](const ModelInstance& instance) { return vec3_to_tuple(instance.get_mirror()); })
|
||||
// 4x4 float64 affine matrix mapping the object into world space. Requires numpy.
|
||||
// World vertices = instance.matrix() @ volume.matrix() applied to mesh vertices.
|
||||
.def("matrix", [](const ModelInstance& instance) { return mat4_to_numpy(instance.get_matrix()); },
|
||||
"Object-to-world 4x4 float64 affine matrix (copy). Requires numpy.")
|
||||
.def("is_left_handed", &ModelInstance::is_left_handed)
|
||||
// Assemble-view placement. Each instance carries a second transform used only by
|
||||
// the Assemble view, set from stored 3mf assemble data or derived from the regular
|
||||
// transform. Until then (is_assemble_initialized() false) it is identity.
|
||||
.def("is_assemble_initialized", [](ModelInstance& instance) { return instance.is_assemble_initialized(); })
|
||||
.def("assemble_offset", [](const ModelInstance& instance) {
|
||||
return vec3_to_tuple(instance.get_assemble_transformation().get_offset());
|
||||
})
|
||||
.def("assemble_rotation", [](const ModelInstance& instance) {
|
||||
return vec3_to_tuple(instance.get_assemble_transformation().get_rotation());
|
||||
})
|
||||
// 4x4 float64 affine matrix placing the object in the Assemble view. Requires numpy.
|
||||
.def("assemble_matrix", [](const ModelInstance& instance) {
|
||||
return mat4_to_numpy(instance.get_assemble_transformation().get_matrix());
|
||||
}, "Assemble-view 4x4 float64 affine matrix (copy). Requires numpy.")
|
||||
// Offset from the instance origin to its position within the source assembly,
|
||||
// recorded at import time (e.g. from a STEP assembly).
|
||||
.def("offset_to_assembly", [](const ModelInstance& instance) {
|
||||
return vec3_to_tuple(instance.get_offset_to_assembly());
|
||||
})
|
||||
// World-space bounding box of this instance.
|
||||
.def("bounding_box", [](ModelInstance& instance) {
|
||||
const ModelObject* object = instance.get_object();
|
||||
if (object == nullptr)
|
||||
return BoundingBoxf3();
|
||||
return object->instance_bounding_box(instance);
|
||||
});
|
||||
|
||||
py::class_<ModelObject, std::unique_ptr<ModelObject, py::nodelete>>(host, "ModelObject")
|
||||
.def("id", [](const ModelObject& object) { return object.id().id; })
|
||||
.def_readonly("name", &ModelObject::name)
|
||||
.def_readonly("module_name", &ModelObject::module_name)
|
||||
.def_readonly("input_file", &ModelObject::input_file)
|
||||
// Import-time flag only: the GUI's printable toggle writes the per-instance
|
||||
// ModelInstance::printable and never updates this field, so derive an
|
||||
// object's effective state from its instances.
|
||||
.def_readonly("printable", &ModelObject::printable)
|
||||
.def("instance_count", [](const ModelObject& object) {
|
||||
return object.instances.size();
|
||||
})
|
||||
.def("volume_count", [](const ModelObject& object) {
|
||||
return object.volumes.size();
|
||||
})
|
||||
.def("instances", [](ModelObject& object) {
|
||||
py::list instances;
|
||||
for (ModelInstance* instance : object.instances)
|
||||
instances.append(py::cast(instance, py::return_value_policy::reference));
|
||||
return instances;
|
||||
})
|
||||
.def("instance", [](ModelObject& object, size_t index) -> ModelInstance* {
|
||||
if (index >= object.instances.size())
|
||||
throw py::index_error("instance index out of range");
|
||||
return object.instances[index];
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("volumes", [](ModelObject& object) {
|
||||
py::list volumes;
|
||||
for (ModelVolume* volume : object.volumes)
|
||||
volumes.append(py::cast(volume, py::return_value_policy::reference));
|
||||
return volumes;
|
||||
})
|
||||
.def("volume", [](ModelObject& object, size_t index) -> ModelVolume* {
|
||||
if (index >= object.volumes.size())
|
||||
throw py::index_error("volume index out of range");
|
||||
return object.volumes[index];
|
||||
}, py::return_value_policy::reference_internal)
|
||||
// World-space bounding box over all instances of this object.
|
||||
.def("bounding_box", [](const ModelObject& object) { return object.bounding_box_exact(); })
|
||||
// Bounding box of the object's raw (untransformed) part meshes — its intrinsic size.
|
||||
.def("raw_mesh_bounding_box", [](const ModelObject& object) { return object.raw_mesh_bounding_box(); })
|
||||
.def("min_z", &ModelObject::min_z)
|
||||
.def("max_z", &ModelObject::max_z)
|
||||
.def("facets_count", [](const ModelObject& object) { return object.facets_count(); })
|
||||
.def("parts_count", [](const ModelObject& object) { return object.parts_count(); })
|
||||
.def("materials_count", [](const ModelObject& object) { return object.materials_count(); })
|
||||
.def("mesh_errors_count", [](const ModelObject& object) { return object.get_repaired_errors_count(); })
|
||||
.def("is_multiparts", &ModelObject::is_multiparts)
|
||||
.def("is_cut", &ModelObject::is_cut)
|
||||
.def("has_custom_layering", &ModelObject::has_custom_layering)
|
||||
.def("is_fdm_support_painted", &ModelObject::is_fdm_support_painted)
|
||||
.def("is_seam_painted", &ModelObject::is_seam_painted)
|
||||
.def("is_mm_painted", &ModelObject::is_mm_painted)
|
||||
.def("is_fuzzy_skin_painted", &ModelObject::is_fuzzy_skin_painted)
|
||||
.def("config_keys", [](const ModelObject& object) {
|
||||
return object.config.keys();
|
||||
})
|
||||
.def("config_value", [](const ModelObject& object, const std::string& key) {
|
||||
return config_value_or_none(object.config.get(), key);
|
||||
});
|
||||
|
||||
py::class_<Model, std::unique_ptr<Model, py::nodelete>>(host, "Model")
|
||||
.def("id", [](const Model& model) { return model.id().id; })
|
||||
.def("object_count", [](const Model& model) {
|
||||
return model.objects.size();
|
||||
})
|
||||
.def("object", [](Model& model, size_t index) -> ModelObject* {
|
||||
if (index >= model.objects.size())
|
||||
throw py::index_error("model object index out of range");
|
||||
return model.objects[index];
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("objects", [](Model& model) {
|
||||
py::list objects;
|
||||
for (ModelObject* object : model.objects)
|
||||
objects.append(py::cast(object, py::return_value_policy::reference));
|
||||
return objects;
|
||||
})
|
||||
// World-space bounding box of the whole model. bounding_box() is exact;
|
||||
// bounding_box_approx() is faster and cached.
|
||||
.def("bounding_box", [](const Model& model) { return model.bounding_box_exact(); })
|
||||
.def("bounding_box_approx", [](const Model& model) { return model.bounding_box_approx(); })
|
||||
.def("max_z", &Model::max_z)
|
||||
.def("material_count", [](const Model& model) { return model.materials.size(); })
|
||||
.def("is_fdm_support_painted", &Model::is_fdm_support_painted)
|
||||
.def("is_seam_painted", &Model::is_seam_painted)
|
||||
.def("is_mm_painted", &Model::is_mm_painted)
|
||||
.def("is_fuzzy_skin_painted", &Model::is_fuzzy_skin_painted)
|
||||
.def("current_plate_index", [](const Model& model) { return model.curr_plate_index; })
|
||||
.def("designer", [](const Model& model) {
|
||||
return model.design_info ? model.design_info->Designer : std::string();
|
||||
})
|
||||
.def("design_id", [](const Model& model) { return model.stl_design_id; });
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
138
src/slic3r/plugin/host/PluginHostPresets.cpp
Normal file
138
src/slic3r/plugin/host/PluginHostPresets.cpp
Normal file
@@ -0,0 +1,138 @@
|
||||
#include "PluginHostBindings.hpp"
|
||||
#include "slic3r/plugin/PluginBindingUtils.hpp"
|
||||
|
||||
#include <libslic3r/Preset.hpp>
|
||||
#include <libslic3r/PresetBundle.hpp>
|
||||
|
||||
#include <pybind11/stl.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
namespace Slic3r {
|
||||
namespace {
|
||||
|
||||
py::list current_filament_presets(PresetBundle& bundle)
|
||||
{
|
||||
py::list presets;
|
||||
for (const std::string& preset_name : bundle.filament_presets) {
|
||||
Preset* preset = bundle.filaments.find_preset(preset_name);
|
||||
if (preset == nullptr)
|
||||
presets.append(py::none());
|
||||
else
|
||||
presets.append(py::cast(preset, py::return_value_policy::reference));
|
||||
}
|
||||
return presets;
|
||||
}
|
||||
|
||||
PresetCollection& printer_presets(PresetBundle& bundle)
|
||||
{
|
||||
return static_cast<PresetCollection&>(bundle.printers);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void host_bindings::register_presets(py::module_& host)
|
||||
{
|
||||
py::enum_<Preset::Type>(host, "PresetType")
|
||||
.value("Invalid", Preset::TYPE_INVALID)
|
||||
.value("Print", Preset::TYPE_PRINT)
|
||||
.value("SlaPrint", Preset::TYPE_SLA_PRINT)
|
||||
.value("Filament", Preset::TYPE_FILAMENT)
|
||||
.value("SlaMaterial", Preset::TYPE_SLA_MATERIAL)
|
||||
.value("Printer", Preset::TYPE_PRINTER)
|
||||
.value("PhysicalPrinter", Preset::TYPE_PHYSICAL_PRINTER)
|
||||
.value("Plate", Preset::TYPE_PLATE)
|
||||
.value("Model", Preset::TYPE_MODEL);
|
||||
|
||||
py::class_<Preset, std::unique_ptr<Preset, py::nodelete>>(host, "Preset")
|
||||
.def_readonly("type", &Preset::type)
|
||||
.def_readonly("name", &Preset::name)
|
||||
.def_readonly("alias", &Preset::alias)
|
||||
.def_readonly("file", &Preset::file)
|
||||
.def_readonly("is_default", &Preset::is_default)
|
||||
.def_readonly("is_external", &Preset::is_external)
|
||||
.def_readonly("is_system", &Preset::is_system)
|
||||
.def_readonly("is_visible", &Preset::is_visible)
|
||||
.def_readonly("is_dirty", &Preset::is_dirty)
|
||||
.def_readonly("is_compatible", &Preset::is_compatible)
|
||||
.def_readonly("is_project_embedded", &Preset::is_project_embedded)
|
||||
.def_readonly("bundle_id", &Preset::bundle_id)
|
||||
.def("is_user", &Preset::is_user)
|
||||
.def("is_from_bundle", &Preset::is_from_bundle)
|
||||
.def("label", &Preset::label, py::arg("no_alias") = false)
|
||||
.def("config_keys", [](const Preset& preset) { return preset.config.keys(); })
|
||||
.def("config_value", [](const Preset& preset, const std::string& key) {
|
||||
return config_value_or_none(preset.config, key);
|
||||
});
|
||||
|
||||
py::class_<PresetCollection, std::unique_ptr<PresetCollection, py::nodelete>>(host, "PresetCollection")
|
||||
.def("size", &PresetCollection::size)
|
||||
.def("get_selected_preset", [](PresetCollection& collection) -> Preset& {
|
||||
return collection.get_selected_preset();
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("selected_preset", [](PresetCollection& collection) -> Preset& {
|
||||
return collection.get_selected_preset();
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("get_selected_preset_name", &PresetCollection::get_selected_preset_name)
|
||||
.def("selected_preset_name", &PresetCollection::get_selected_preset_name)
|
||||
.def("get_edited_preset", [](PresetCollection& collection) -> Preset& {
|
||||
return collection.get_edited_preset();
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("edited_preset", [](PresetCollection& collection) -> Preset& {
|
||||
return collection.get_edited_preset();
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("preset", [](PresetCollection& collection, size_t index) -> Preset& {
|
||||
if (index >= collection.size())
|
||||
throw py::index_error("preset index out of range");
|
||||
return collection.preset(index);
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("find_preset", [](PresetCollection& collection, const std::string& name) -> Preset* {
|
||||
return collection.find_preset(name);
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("preset_names", [](const PresetCollection& collection) {
|
||||
std::vector<std::string> names;
|
||||
names.reserve(collection.get_presets().size());
|
||||
for (const Preset& preset : collection.get_presets())
|
||||
names.push_back(preset.name);
|
||||
return names;
|
||||
});
|
||||
|
||||
py::class_<PresetBundle, std::unique_ptr<PresetBundle, py::nodelete>>(host, "PresetBundle")
|
||||
.def_property_readonly("prints", [](PresetBundle& bundle) -> PresetCollection& {
|
||||
return bundle.prints;
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def_property_readonly("printers", &printer_presets, py::return_value_policy::reference_internal)
|
||||
.def_property_readonly("filaments", [](PresetBundle& bundle) -> PresetCollection& {
|
||||
return bundle.filaments;
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def_property_readonly("sla_prints", [](PresetBundle& bundle) -> PresetCollection& {
|
||||
return bundle.sla_prints;
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def_property_readonly("sla_materials", [](PresetBundle& bundle) -> PresetCollection& {
|
||||
return bundle.sla_materials;
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("current_process_preset", [](PresetBundle& bundle) -> Preset& {
|
||||
return bundle.prints.get_edited_preset();
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("current_print_preset", [](PresetBundle& bundle) -> Preset& {
|
||||
return bundle.prints.get_edited_preset();
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("current_printer_preset", [](PresetBundle& bundle) -> Preset& {
|
||||
return bundle.printers.get_edited_preset();
|
||||
}, py::return_value_policy::reference_internal)
|
||||
.def("current_filament_preset_names", [](PresetBundle& bundle) {
|
||||
return bundle.filament_presets;
|
||||
})
|
||||
.def("current_filament_presets", ¤t_filament_presets)
|
||||
.def("full_config_keys", [](const PresetBundle& bundle) {
|
||||
return bundle.full_config().keys();
|
||||
})
|
||||
.def("full_config_value", [](const PresetBundle& bundle, const std::string& key) {
|
||||
return config_value_or_none(bundle.full_config(), key);
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
352
src/slic3r/plugin/host/PluginHostSlicing.cpp
Normal file
352
src/slic3r/plugin/host/PluginHostSlicing.cpp
Normal file
@@ -0,0 +1,352 @@
|
||||
#include "PluginHostBindings.hpp"
|
||||
#include "slic3r/plugin/PluginBindingUtils.hpp"
|
||||
|
||||
#include "libslic3r/BoundingBox.hpp"
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "libslic3r/Surface.hpp"
|
||||
#include "libslic3r/SurfaceCollection.hpp"
|
||||
#include "libslic3r/ExtrusionEntity.hpp"
|
||||
#include "libslic3r/ExtrusionEntityCollection.hpp"
|
||||
#include "libslic3r/Layer.hpp" // LayerRegion, Layer, SupportLayer
|
||||
#include "libslic3r/Print.hpp" // PrintRegion, PrintObject, Print
|
||||
|
||||
#include <pybind11/stl.h>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
namespace Slic3r {
|
||||
namespace {
|
||||
// Flatten an extrusion graph into a list of leaf ExtrusionPath* while walking the
|
||||
// ORIGINAL Print-owned tree (never a temporary copy): the returned pointers stay
|
||||
// valid for the execute(ctx) lifetime pinned by `owner`, so points() can hand out
|
||||
// zero-copy views into path->polyline.points.
|
||||
//
|
||||
// This is deliberately NOT ExtrusionEntityCollection::flatten(): flatten() only
|
||||
// unwraps nested collections (is_collection() is true solely for collections) and
|
||||
// returns them by value, so it would (a) dangle if we viewed into the copy and
|
||||
// (b) leave ExtrusionLoop/ExtrusionMultiPath intact — dropping every perimeter
|
||||
// loop, since dynamic_cast<ExtrusionPath*> fails on a loop. We descend into
|
||||
// loops/multipaths here to reach their contained paths.
|
||||
static void collect_extrusion_paths(const ExtrusionEntity* ee, std::vector<const ExtrusionPath*>& out)
|
||||
{
|
||||
if (ee == nullptr)
|
||||
return;
|
||||
if (const auto* coll = dynamic_cast<const ExtrusionEntityCollection*>(ee)) {
|
||||
for (const ExtrusionEntity* child : coll->entities)
|
||||
collect_extrusion_paths(child, out);
|
||||
} else if (const auto* loop = dynamic_cast<const ExtrusionLoop*>(ee)) {
|
||||
for (const ExtrusionPath& p : loop->paths)
|
||||
out.push_back(&p);
|
||||
} else if (const auto* mp = dynamic_cast<const ExtrusionMultiPath*>(ee)) {
|
||||
for (const ExtrusionPath& p : mp->paths)
|
||||
out.push_back(&p);
|
||||
} else if (const auto* path = dynamic_cast<const ExtrusionPath*>(ee)) {
|
||||
// Catches ExtrusionPath and its subclasses (Sloped/Contoured/Oriented) last,
|
||||
// after the composite types above have been ruled out.
|
||||
out.push_back(path);
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild a layer's per-island bbox cache from lslices — the same inline pattern
|
||||
// every C++ call site uses (PrintObjectSlice.cpp, Print.cpp, TreeSupport.cpp); no
|
||||
// libslic3r helper exists to reuse.
|
||||
static void refresh_lslices_bboxes(Layer& l)
|
||||
{
|
||||
l.lslices_bboxes.clear();
|
||||
l.lslices_bboxes.reserve(l.lslices.size());
|
||||
for (const ExPolygon& island : l.lslices)
|
||||
l.lslices_bboxes.emplace_back(get_extents(island));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void host_bindings::register_slicing(py::module_& host)
|
||||
{
|
||||
// ------------------------------------------------------------------
|
||||
// Slicing print-graph data model — raw bindings of the classes the C++
|
||||
// pipeline itself uses, same nodelete/reference style as the Model and
|
||||
// Preset graphs in PluginHostModel.cpp / PluginHostPresets.cpp.
|
||||
//
|
||||
// LIFETIME (C++ semantics, the one rule of this API): every object handed
|
||||
// out below is a non-owning reference into the live slicing graph owned by
|
||||
// the Print. References — and every numpy view they hand out — are valid
|
||||
// only while the plugin hook (execute(ctx)) runs, and a container-replacing
|
||||
// mutator (SurfaceCollection.set / append / clear, Polygon.set_points / append,
|
||||
// ExPolygon.set_holes) invalidates previously obtained references into that
|
||||
// container, exactly as std::vector operations invalidate C++ iterators. Do
|
||||
// not stash references or arrays across execute() calls; copy what you need.
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
py::enum_<SurfaceType>(host, "SurfaceType")
|
||||
.value("stTop", stTop)
|
||||
.value("stBottom", stBottom)
|
||||
.value("stBottomBridge", stBottomBridge)
|
||||
.value("stInternalAfterExternalBridge", stInternalAfterExternalBridge)
|
||||
.value("stInternal", stInternal)
|
||||
.value("stInternalSolid", stInternalSolid)
|
||||
.value("stInternalBridge", stInternalBridge)
|
||||
.value("stSecondInternalBridge", stSecondInternalBridge)
|
||||
.value("stInternalVoid", stInternalVoid)
|
||||
.value("stPerimeter", stPerimeter)
|
||||
.value("stCount", stCount)
|
||||
.export_values();
|
||||
|
||||
// Surface: default holder (Python-owned instances are freed), so plugins can construct
|
||||
// their own Surface(surface_type, expolygon) — not just navigate the live slicing graph.
|
||||
// expolygon is a reference_internal property, same idiom as the Polygon/ExPolygon
|
||||
// accessors in PluginHostGeometry.cpp.
|
||||
py::class_<Surface>(host, "Surface")
|
||||
.def(py::init([](SurfaceType t, const ExPolygon& e) { return Surface(t, e); }),
|
||||
py::arg("surface_type"), py::arg("expolygon"))
|
||||
.def(py::init([](SurfaceType t) { return Surface(t); }), py::arg("surface_type"))
|
||||
.def_readwrite("surface_type", &Surface::surface_type,
|
||||
"This surface's SurfaceType. Assigning reclassifies it in place (geometry unchanged).")
|
||||
.def_readwrite("thickness", &Surface::thickness)
|
||||
.def_readwrite("bridge_angle", &Surface::bridge_angle)
|
||||
.def_readwrite("extra_perimeters", &Surface::extra_perimeters)
|
||||
.def_property("expolygon",
|
||||
[](Surface& s) -> ExPolygon& { return s.expolygon; },
|
||||
[](Surface& s, const ExPolygon& e) { s.expolygon = e; },
|
||||
py::return_value_policy::reference_internal,
|
||||
"This surface's geometry. Read returns a live ExPolygon ref; assign to replace it.")
|
||||
.def("area", [](const Surface& s) { return s.area(); })
|
||||
.def("is_top", [](const Surface& s) { return s.is_top(); })
|
||||
.def("is_bottom", [](const Surface& s) { return s.is_bottom(); })
|
||||
.def("is_bridge", [](const Surface& s) { return s.is_bridge(); })
|
||||
.def("is_internal", [](const Surface& s) { return s.is_internal(); })
|
||||
.def("is_external", [](const Surface& s) { return s.is_external(); })
|
||||
.def("is_solid", [](const Surface& s) { return s.is_solid(); });
|
||||
|
||||
// SurfaceCollection: kept on py::nodelete — it is only ever a reference into the live
|
||||
// slicing graph (LayerRegion::slices/fill_surfaces), never constructed by a plugin.
|
||||
py::class_<SurfaceCollection, std::unique_ptr<SurfaceCollection, py::nodelete>>(host, "SurfaceCollection")
|
||||
.def("size", [](const SurfaceCollection& c) { return c.surfaces.size(); })
|
||||
.def("empty", [](const SurfaceCollection& c) { return c.empty(); })
|
||||
.def("clear", [](SurfaceCollection& c) { c.clear(); })
|
||||
.def("has", [](const SurfaceCollection& c, SurfaceType t) { return c.has(t); }, py::arg("surface_type"))
|
||||
.def("set_type", [](SurfaceCollection& c, SurfaceType t) { c.set_type(t); }, py::arg("surface_type"))
|
||||
.def("set", [](SurfaceCollection& c, const std::vector<ExPolygon>& src, SurfaceType t) { c.set(src, t); },
|
||||
py::arg("expolygons"), py::arg("surface_type"),
|
||||
"Replace all surfaces from a list of ExPolygon, all tagged `surface_type`.")
|
||||
.def("set", [](SurfaceCollection& c, const std::vector<Surface>& src) { c.set(src); },
|
||||
py::arg("surfaces"), "Replace all surfaces from a list of Surface (types preserved per surface).")
|
||||
.def("append", [](SurfaceCollection& c, const std::vector<ExPolygon>& src, SurfaceType t) { c.append(src, t); },
|
||||
py::arg("expolygons"), py::arg("surface_type"))
|
||||
.def("filter_by_type", [](py::object self, SurfaceType t) {
|
||||
SurfaceCollection& c = self.cast<SurfaceCollection&>();
|
||||
py::list out;
|
||||
// SurfaceCollection::filter_by_type returns SurfacesPtr, which is
|
||||
// std::vector<const Surface*> (see Surface.hpp), so iterate by const
|
||||
// pointer (py::cast accepts `const itype*` directly, see cast.h cast(const itype*)).
|
||||
for (const Surface* s : c.filter_by_type(t))
|
||||
out.append(py::cast(s, py::return_value_policy::reference_internal, self));
|
||||
return out;
|
||||
}, py::arg("surface_type"), "Surfaces of a given type as [Surface] refs. Invalidated by "
|
||||
"set()/append()/clear() on this collection (C++ vector semantics), same as .surfaces.")
|
||||
.def_property_readonly("surfaces", [](py::object self) {
|
||||
SurfaceCollection& c = self.cast<SurfaceCollection&>();
|
||||
py::list out;
|
||||
for (Surface& s : c.surfaces)
|
||||
out.append(py::cast(&s, py::return_value_policy::reference_internal, self));
|
||||
return out;
|
||||
}, "Surfaces as [Surface] references into the live collection. Invalidated by "
|
||||
"set()/append()/clear() on this collection (C++ vector semantics).");
|
||||
|
||||
// --- Extrusion tree (read-only). Registered polymorphically: when a returned
|
||||
// ExtrusionEntity*'s dynamic type IS one of the classes registered below, pybind
|
||||
// hands the plugin that concrete type, so plugins walk the same tree shape C++ does.
|
||||
// When the dynamic type is NOT registered (e.g. ExtrusionLoopSloped, produced with
|
||||
// scarf seams), pybind falls back to the STATIC type at the cast site -- so such a
|
||||
// `.entities` child surfaces as a bare ExtrusionEntity (only .role is available).
|
||||
// flatten_paths() (a dynamic_cast walk) still yields proper ExtrusionPath leaves and
|
||||
// is the robust way to extract toolpaths.
|
||||
py::class_<ExtrusionEntity, std::unique_ptr<ExtrusionEntity, py::nodelete>>(host, "ExtrusionEntity")
|
||||
.def_property_readonly("role", [](const ExtrusionEntity& e) {
|
||||
return ExtrusionEntity::role_to_string(e.role());
|
||||
}, "Extrusion role as a human-readable string (e.g. \"Outer wall\", \"Sparse infill\").");
|
||||
|
||||
py::class_<ExtrusionPath, ExtrusionEntity, std::unique_ptr<ExtrusionPath, py::nodelete>>(host, "ExtrusionPath")
|
||||
.def("points", [](py::object self) {
|
||||
const ExtrusionPath& p = self.cast<const ExtrusionPath&>();
|
||||
const Points3& pts = p.polyline.points;
|
||||
return with_numpy([&] {
|
||||
return py::object(make_readonly_rows<coord_t, 3>(
|
||||
self, pts.empty() ? nullptr : pts.front().data(), (py::ssize_t) pts.size()));
|
||||
});
|
||||
}, "Path vertices as a read-only int64 (N,3) numpy view in scaled coords "
|
||||
"(the polyline is natively 3D on this branch). Requires numpy.")
|
||||
.def_readonly("width", &ExtrusionPath::width)
|
||||
.def_readonly("height", &ExtrusionPath::height)
|
||||
.def_readonly("mm3_per_mm", &ExtrusionPath::mm3_per_mm);
|
||||
|
||||
py::class_<ExtrusionLoop, ExtrusionEntity, std::unique_ptr<ExtrusionLoop, py::nodelete>>(host, "ExtrusionLoop")
|
||||
.def_property_readonly("paths", [](py::object self) {
|
||||
ExtrusionLoop& l = self.cast<ExtrusionLoop&>();
|
||||
py::list out;
|
||||
for (ExtrusionPath& p : l.paths)
|
||||
out.append(py::cast(&p, py::return_value_policy::reference_internal, self));
|
||||
return out;
|
||||
}, "The loop's constituent paths as [ExtrusionPath].");
|
||||
|
||||
py::class_<ExtrusionMultiPath, ExtrusionEntity, std::unique_ptr<ExtrusionMultiPath, py::nodelete>>(host, "ExtrusionMultiPath")
|
||||
.def_property_readonly("paths", [](py::object self) {
|
||||
ExtrusionMultiPath& m = self.cast<ExtrusionMultiPath&>();
|
||||
py::list out;
|
||||
for (ExtrusionPath& p : m.paths)
|
||||
out.append(py::cast(&p, py::return_value_policy::reference_internal, self));
|
||||
return out;
|
||||
}, "The multipath's constituent paths as [ExtrusionPath].");
|
||||
|
||||
py::class_<ExtrusionEntityCollection, ExtrusionEntity,
|
||||
std::unique_ptr<ExtrusionEntityCollection, py::nodelete>>(host, "ExtrusionEntityCollection")
|
||||
.def("size", [](const ExtrusionEntityCollection& c) { return c.entities.size(); })
|
||||
.def_property_readonly("entities", [](py::object self) {
|
||||
ExtrusionEntityCollection& c = self.cast<ExtrusionEntityCollection&>();
|
||||
py::list out;
|
||||
for (ExtrusionEntity* e : c.entities)
|
||||
out.append(py::cast(e, py::return_value_policy::reference_internal, self));
|
||||
return out;
|
||||
}, "Child entities. Each is handed to you as its concrete type only when that type "
|
||||
"is registered; a child whose concrete type is unregistered (e.g. a scarf-seam "
|
||||
"ExtrusionLoopSloped) surfaces as a bare ExtrusionEntity exposing only .role. Use "
|
||||
"flatten_paths() to robustly reach every ExtrusionPath leaf.")
|
||||
.def("flatten_paths", [](py::object self) {
|
||||
const ExtrusionEntityCollection& c = self.cast<const ExtrusionEntityCollection&>();
|
||||
std::vector<const ExtrusionPath*> paths;
|
||||
collect_extrusion_paths(&c, paths);
|
||||
py::list out;
|
||||
for (const ExtrusionPath* p : paths)
|
||||
out.append(py::cast(const_cast<ExtrusionPath*>(p),
|
||||
py::return_value_policy::reference_internal, self));
|
||||
return out;
|
||||
}, "Every leaf ExtrusionPath under this tree (collections recursed into, "
|
||||
"loops/multipaths decomposed).");
|
||||
|
||||
py::class_<PrintRegion, std::unique_ptr<PrintRegion, py::nodelete>>(host, "PrintRegion")
|
||||
.def("config_keys", [](const PrintRegion& r) { return r.config().keys(); })
|
||||
.def("config_value", [](const PrintRegion& r, const std::string& key) {
|
||||
return config_value_or_none(r.config(), key);
|
||||
}, py::arg("key"),
|
||||
"Serialized value of this region's resolved config option, or None if absent.");
|
||||
|
||||
auto layer_region = py::class_<LayerRegion, std::unique_ptr<LayerRegion, py::nodelete>>(host, "LayerRegion");
|
||||
layer_region
|
||||
.def_readonly("slices", &LayerRegion::slices,
|
||||
"Sliced, typed surfaces (SurfaceCollection). Edit in place, or replace with "
|
||||
"slices.set(expolygons, surface_type). At Step.posSlice this is the primary mutation "
|
||||
"target; the split slice loop runs make_perimeters() afterward so edits cascade downstream.")
|
||||
.def_readonly("fill_surfaces", &LayerRegion::fill_surfaces,
|
||||
"Surfaces prepared for infill (SurfaceCollection). Edit in place or via fill_surfaces.set(...).")
|
||||
.def_readonly("perimeters", &LayerRegion::perimeters,
|
||||
"Perimeter toolpaths (ExtrusionEntityCollection, read-only).")
|
||||
.def_readonly("fills", &LayerRegion::fills,
|
||||
"Infill toolpaths (ExtrusionEntityCollection, read-only).")
|
||||
.def("layer", [](LayerRegion& r) -> py::object {
|
||||
Layer* l = r.layer();
|
||||
if (l == nullptr)
|
||||
return py::none();
|
||||
return py::cast(l, py::return_value_policy::reference);
|
||||
}, "Owning Layer, or None.")
|
||||
.def("region", [](LayerRegion& r) -> const PrintRegion& { return r.region(); },
|
||||
py::return_value_policy::reference,
|
||||
"This region's PrintRegion (resolved per-region settings).")
|
||||
.def("config_value", [](const LayerRegion& r, const std::string& key) {
|
||||
return config_value_or_none(r.region().config(), key);
|
||||
}, py::arg("key"),
|
||||
"Serialized value of this region's resolved config option, or None if absent.");
|
||||
|
||||
auto layer = py::class_<Layer, std::unique_ptr<Layer, py::nodelete>>(host, "Layer");
|
||||
layer
|
||||
.def_readonly("print_z", &Layer::print_z)
|
||||
.def_readonly("slice_z", &Layer::slice_z)
|
||||
.def_readonly("height", &Layer::height)
|
||||
.def_property_readonly("upper_layer", [](Layer& l) -> py::object {
|
||||
if (l.upper_layer == nullptr) return py::none();
|
||||
return py::cast(l.upper_layer, py::return_value_policy::reference);
|
||||
}, "The layer above, or None (graph navigation, like C++).")
|
||||
.def_property_readonly("lower_layer", [](Layer& l) -> py::object {
|
||||
if (l.lower_layer == nullptr) return py::none();
|
||||
return py::cast(l.lower_layer, py::return_value_policy::reference);
|
||||
}, "The layer below, or None.")
|
||||
.def("regions", [](py::object self) {
|
||||
Layer& l = self.cast<Layer&>();
|
||||
py::list out;
|
||||
for (LayerRegion* r : l.regions())
|
||||
out.append(py::cast(r, py::return_value_policy::reference_internal, self));
|
||||
return out;
|
||||
}, "Per-region data as [LayerRegion].")
|
||||
.def("make_slices", [](Layer& l) {
|
||||
l.make_slices();
|
||||
refresh_lslices_bboxes(l);
|
||||
}, "Re-derive lslices (merged islands) from the region slices and refresh the bbox "
|
||||
"cache — the C++ invariant-maintenance call after in-place slice edits.")
|
||||
.def("lslices", [](py::object self) {
|
||||
Layer& l = self.cast<Layer&>();
|
||||
py::list out;
|
||||
for (ExPolygon& e : l.lslices)
|
||||
out.append(py::cast(&e, py::return_value_policy::reference_internal, self));
|
||||
return out;
|
||||
}, "Merged per-layer islands as [ExPolygon] refs (in-place editable). Derived from the "
|
||||
"region slices; call make_slices() to re-derive after edits. Invalidated by make_slices().");
|
||||
|
||||
py::class_<PrintObject, std::unique_ptr<PrintObject, py::nodelete>>(host, "PrintObject")
|
||||
.def("id", [](const PrintObject& o) { return o.id().id; },
|
||||
"Stable numeric object id (ObjectBase::id()).")
|
||||
.def("layers", [](py::object self) {
|
||||
PrintObject& o = self.cast<PrintObject&>();
|
||||
py::list out;
|
||||
for (Layer* l : o.layers())
|
||||
out.append(py::cast(l, py::return_value_policy::reference_internal, self));
|
||||
return out;
|
||||
}, "Object layers, bottom-up, as [Layer].")
|
||||
.def("support_layers", [](py::object self) {
|
||||
PrintObject& o = self.cast<PrintObject&>();
|
||||
py::list out;
|
||||
for (SupportLayer* sl : o.support_layers())
|
||||
out.append(py::cast(static_cast<Layer*>(sl),
|
||||
py::return_value_policy::reference_internal, self));
|
||||
return out;
|
||||
}, "Support layers as [Layer] (support-specific fields are not exposed).")
|
||||
.def("model_object", [](PrintObject& o) -> py::object {
|
||||
// The Print's model SNAPSHOT (worker-thread stable), reusing the
|
||||
// orca.host.ModelObject bindings — mesh access for slicing plugins.
|
||||
// o is non-const here, so model_object() already returns a non-const ModelObject*.
|
||||
return py::cast(o.model_object(), py::return_value_policy::reference);
|
||||
}, "The source orca.host.ModelObject from the Print's own model snapshot.")
|
||||
.def("bounding_box", [](const PrintObject& o) {
|
||||
const BoundingBox bb = o.bounding_box();
|
||||
return py::make_tuple(bb.min.x(), bb.min.y(), bb.max.x(), bb.max.y());
|
||||
}, "Object XY bounding box in scaled coords as (min_x, min_y, max_x, max_y). The "
|
||||
"sliced polygons live in this same frame, so its midpoint is the footprint center.")
|
||||
.def("trafo", [](const PrintObject& o) { return mat4_to_numpy(o.trafo()); },
|
||||
"Object-to-print 4x4 float64 affine matrix (copy). Requires numpy.")
|
||||
.def("config_keys", [](const PrintObject& o) { return o.config().keys(); })
|
||||
.def("config_value", [](const PrintObject& o, const std::string& key) {
|
||||
return config_value_or_none(o.config(), key);
|
||||
}, py::arg("key"),
|
||||
"Serialized value of a resolved per-object config option, or None if absent.");
|
||||
|
||||
py::class_<Print, std::unique_ptr<Print, py::nodelete>>(host, "Print")
|
||||
.def("objects", [](py::object self) {
|
||||
Print& p = self.cast<Print&>();
|
||||
py::list out;
|
||||
for (PrintObject* o : p.objects())
|
||||
out.append(py::cast(o, py::return_value_policy::reference_internal, self));
|
||||
return out;
|
||||
}, "The print's objects as [PrintObject].")
|
||||
.def("model", [](Print& p) -> Model& { return const_cast<Model&>(p.model()); },
|
||||
py::return_value_policy::reference_internal,
|
||||
"The Print's own Model snapshot (worker-thread stable). Inside slicing "
|
||||
"hooks use THIS — never orca.host.model(), which is the live GUI model "
|
||||
"owned by another thread.")
|
||||
.def("config_keys", [](const Print& p) { return p.full_print_config().keys(); })
|
||||
.def("config_value", [](const Print& p, const std::string& key) {
|
||||
return config_value_or_none(p.full_print_config(), key);
|
||||
}, py::arg("key"),
|
||||
"Serialized value of the resolved (full) print config for this slice, or None.")
|
||||
.def("canceled", [](const Print& p) { return p.canceled(); },
|
||||
"True once cancellation was requested (prefer ctx.cancelled()).");
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -1,8 +1,8 @@
|
||||
#include "PluginHostUi.hpp"
|
||||
|
||||
#include "PluginAuditManager.hpp"
|
||||
#include "PythonInterpreter.hpp" // PythonGILState
|
||||
#include "PythonJsonUtils.hpp" // json_to_py / py_to_json
|
||||
#include "slic3r/plugin/PluginAuditManager.hpp"
|
||||
#include "slic3r/plugin/PythonInterpreter.hpp" // PythonGILState
|
||||
#include "slic3r/plugin/PythonJsonUtils.hpp" // json_to_py / py_to_json
|
||||
|
||||
#include <slic3r/GUI/GUI_App.hpp>
|
||||
#include <slic3r/GUI/MainFrame.hpp>
|
||||
@@ -1,30 +0,0 @@
|
||||
#include "GCodePluginCapability.hpp"
|
||||
|
||||
#include "GCodePluginCapabilityTrampoline.hpp"
|
||||
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/stl.h>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
void GCodePluginCapability::RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes)
|
||||
{
|
||||
(void) pluginTypes;
|
||||
|
||||
auto gcode = module.def_submodule("gcode", "G-code API");
|
||||
|
||||
py::class_<GCodePluginContext, PluginContext>(gcode, "GCodePluginContext", "Context shared with G-code plugins")
|
||||
.def(py::init<>())
|
||||
.def_readwrite("gcode_path", &GCodePluginContext::gcode_path)
|
||||
.def_readwrite("host", &GCodePluginContext::host)
|
||||
.def_readwrite("output_name", &GCodePluginContext::output_name);
|
||||
|
||||
py::class_<GCodePluginCapability, PluginCapabilityInterface, PyGCodePluginCapabilityTrampoline, std::shared_ptr<GCodePluginCapability>>(gcode, "GCodePluginCapabilityBase")
|
||||
.def(py::init<>())
|
||||
.def("get_type", &GCodePluginCapability::get_type)
|
||||
.def("execute", &GCodePluginCapability::execute);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -1,27 +0,0 @@
|
||||
#ifndef slic3r_GCodePluginCapability_hpp_
|
||||
#define slic3r_GCodePluginCapability_hpp_
|
||||
|
||||
#include "../../PythonPluginInterface.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
struct GCodePluginContext : public PluginContext {
|
||||
std::string gcode_path;
|
||||
std::string host;
|
||||
std::string output_name;
|
||||
};
|
||||
|
||||
class GCodePluginCapability : public PluginCapabilityInterface
|
||||
{
|
||||
public:
|
||||
PluginCapabilityType get_type() const override { return PluginCapabilityType::PostProcessing; }
|
||||
|
||||
virtual ExecutionResult execute(const GCodePluginContext& ctx) = 0;
|
||||
|
||||
static void RegisterBindings(pybind11::module_ &module,
|
||||
pybind11::enum_<PluginCapabilityType> &pluginTypes);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif /* slic3r_GCodePluginCapability_hpp_ */
|
||||
@@ -1,35 +0,0 @@
|
||||
#ifndef slic3r_GCodePluginCapabilityTrampoline_hpp_
|
||||
#define slic3r_GCodePluginCapabilityTrampoline_hpp_
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include "../../PyPluginTrampoline.hpp"
|
||||
#include "../../PluginAuditManager.hpp"
|
||||
#include "GCodePluginCapability.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
class PyGCodePluginCapabilityTrampoline : public PyPluginCommonTrampoline<GCodePluginCapability>
|
||||
{
|
||||
public:
|
||||
using PyPluginCommonTrampoline<GCodePluginCapability>::PyPluginCommonTrampoline;
|
||||
|
||||
ExecutionResult execute(const GCodePluginContext& ctx) override
|
||||
{
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
||||
[&] {
|
||||
// G-code post-processing plugins may also write into the folder holding the
|
||||
// current temp G-code file, in addition to the globally-allowed data_dir().
|
||||
// The setup callback runs AFTER the context is constructed so the scoped root
|
||||
// is not cleared by ScopedPluginAuditContext's constructor.
|
||||
|
||||
if (!ctx.gcode_path.empty())
|
||||
::Slic3r::PluginAuditManager::instance().add_scoped_allowed_root(
|
||||
std::filesystem::path(ctx.gcode_path).parent_path());
|
||||
},
|
||||
PYBIND11_OVERRIDE_PURE, ExecutionResult, GCodePluginCapability, execute, ctx);
|
||||
}
|
||||
};
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,99 @@
|
||||
#include "SlicingPipelinePluginCapability.hpp"
|
||||
#include "SlicingPipelinePluginCapabilityTrampoline.hpp"
|
||||
#include "slic3r/plugin/PluginBindingUtils.hpp" // config_value_or_none
|
||||
#include "libslic3r/libslic3r.h" // unscale<>, live SCALING_FACTOR
|
||||
#include <pybind11/stl.h> // std::map<std::string,std::string> -> dict for ctx.params
|
||||
|
||||
namespace py = pybind11;
|
||||
namespace Slic3r {
|
||||
|
||||
bool SlicingPipelineContext::cancelled() const { return print && print->canceled(); }
|
||||
|
||||
void SlicingPipelinePluginCapability::RegisterBindings(py::module_& module, py::enum_<PluginCapabilityType>& pluginTypes) {
|
||||
(void) pluginTypes; // unused: this capability defines its own Step enum (below) rather than extending the shared PluginCapabilityType enum.
|
||||
auto slicing = module.def_submodule("slicing", "Slicing pipeline API (research/experimental).");
|
||||
|
||||
py::enum_<SlicingPipelineStepPlugin>(slicing, "Step")
|
||||
.value("posSlice", SlicingPipelineStepPlugin::posSlice)
|
||||
.value("posPerimeters", SlicingPipelineStepPlugin::posPerimeters)
|
||||
.value("posEstimateCurledExtrusions", SlicingPipelineStepPlugin::posEstimateCurledExtrusions)
|
||||
.value("posPrepareInfill", SlicingPipelineStepPlugin::posPrepareInfill) // after prepare_infill, before make_fills: editing fill_surfaces here CASCADES
|
||||
.value("posInfill", SlicingPipelineStepPlugin::posInfill) // after make_fills: editing fill_surfaces here does NOT regenerate the fills
|
||||
.value("posIroning", SlicingPipelineStepPlugin::posIroning)
|
||||
.value("posContouring", SlicingPipelineStepPlugin::posContouring)
|
||||
.value("posSupportMaterial", SlicingPipelineStepPlugin::posSupportMaterial)
|
||||
.value("posDetectOverhangsForLift", SlicingPipelineStepPlugin::posDetectOverhangsForLift)
|
||||
.value("posSimplifyPath", SlicingPipelineStepPlugin::posSimplifyPath) // covers all simplify sub-steps
|
||||
.value("psWipeTower", SlicingPipelineStepPlugin::psWipeTower)
|
||||
.value("psSkirtBrim", SlicingPipelineStepPlugin::psSkirtBrim)
|
||||
// Post-process seam: fires in the GUI export path AFTER the classic post_process scripts, on the
|
||||
// exported G-code file. Unlike every step above it is NOT fired by Print::process(): ctx.print and
|
||||
// ctx.object are None; instead ctx.gcode_path / ctx.host / ctx.output_name are set and the plugin
|
||||
// edits the file at ctx.gcode_path IN PLACE. May fire more than once per slice (file export and/or
|
||||
// upload each fire once, on separate working copies) and its output is not reflected in the G-code
|
||||
// preview (the viewer maps the pre-post-process file). ctx.config_value()/ctx.params still work.
|
||||
.value("psGCodePostProcess", SlicingPipelineStepPlugin::psGCodePostProcess)
|
||||
.export_values();
|
||||
|
||||
// The read-graph data model (Surface / ExPolygon / the extrusion tree / LayerRegion /
|
||||
// Layer / PrintObject / Print) and the 2D-geometry mutators live in orca.host, registered
|
||||
// by PluginHostSlicing.cpp. orca.slicing is workflow-only: Step, unscale, the context, and
|
||||
// the capability base. See PluginHostSlicing.cpp for the mandatory reference-lifetime rule.
|
||||
|
||||
// Scaled integer coordinate -> millimeters. Reads the live SCALING_FACTOR at call
|
||||
// time (1e-6 normal, 1e-5 for beds > 2147mm), so it is never cached.
|
||||
slicing.def("unscale", [](coord_t v) { return unscale<double>(v); }, py::arg("coord"),
|
||||
"Convert a scaled integer coordinate to millimeters (reads the live SCALING_FACTOR).");
|
||||
|
||||
py::class_<SlicingPipelineContext>(slicing, "SlicingPipelineContext")
|
||||
.def_readonly("orca_version", &SlicingPipelineContext::orca_version)
|
||||
.def_readonly("step", &SlicingPipelineContext::step)
|
||||
.def_readonly("params", &SlicingPipelineContext::params,
|
||||
"read-only dict of this plugin's [tool.orcaslicer.plugin.settings] values "
|
||||
"(string->string). Parse the values you need, e.g. float(ctx.params['rate']).")
|
||||
.def_readonly("gcode_path", &SlicingPipelineContext::gcode_path,
|
||||
"Path to the working G-code file, set ONLY at Step.psGCodePostProcess. Edit it in "
|
||||
"place; empty at every other step.")
|
||||
.def_readonly("host", &SlicingPipelineContext::host,
|
||||
"Target host at Step.psGCodePostProcess (\"File\", \"OctoPrint\", ...); empty otherwise.")
|
||||
.def_readonly("output_name", &SlicingPipelineContext::output_name,
|
||||
"Final output G-code name at Step.psGCodePostProcess (mirrors SLIC3R_PP_OUTPUT_NAME); "
|
||||
"empty otherwise.")
|
||||
.def_property_readonly("print", [](const SlicingPipelineContext& ctx) -> py::object {
|
||||
if (ctx.print == nullptr)
|
||||
return py::none();
|
||||
return py::cast(ctx.print, py::return_value_policy::reference);
|
||||
}, "The orca.host.Print being sliced — the raw slicing graph, exactly what the "
|
||||
"C++ pipeline walks. Valid only during the execute(ctx) call. For mesh access "
|
||||
"use ctx.print.model() (the Print's snapshot), never orca.host.model().")
|
||||
.def_property_readonly("object", [](const SlicingPipelineContext& ctx) -> py::object {
|
||||
if (ctx.object == nullptr)
|
||||
return py::none();
|
||||
// The hook signature hands objects out as const; they are genuinely mutable
|
||||
// (owned by the Print), so the const_cast is safe — done once here at the
|
||||
// graph entry point so Python steps receive a mutable PrintObject.
|
||||
return py::cast(const_cast<PrintObject*>(ctx.object), py::return_value_policy::reference);
|
||||
}, "orca.host.PrintObject for object-scoped steps, or None for print-wide steps. "
|
||||
"Valid only during the execute(ctx) call.")
|
||||
.def("config_value", [](const SlicingPipelineContext& ctx, const std::string& key) -> py::object {
|
||||
// In-pipeline steps read the live Print's full config; at psGCodePostProcess (print == null)
|
||||
// fall back to the config the export path handed in.
|
||||
if (ctx.print != nullptr)
|
||||
return config_value_or_none(ctx.print->full_print_config(), key);
|
||||
if (ctx.full_config != nullptr)
|
||||
return config_value_or_none(*ctx.full_config, key);
|
||||
return py::none();
|
||||
}, py::arg("key"),
|
||||
"serialized value of a resolved (full) print config option for this slice, or "
|
||||
"None if absent. Shorthand for ctx.print.config_value(key).")
|
||||
.def("cancelled", &SlicingPipelineContext::cancelled);
|
||||
|
||||
py::class_<SlicingPipelinePluginCapability, PluginCapabilityInterface,
|
||||
PySlicingPipelinePluginCapabilityTrampoline,
|
||||
std::shared_ptr<SlicingPipelinePluginCapability>>(slicing, "SlicingPipelineCapabilityBase")
|
||||
.def(py::init<>())
|
||||
.def("get_type", &SlicingPipelinePluginCapability::get_type)
|
||||
.def("execute", &SlicingPipelinePluginCapability::execute);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
#include "slic3r/plugin/PythonPluginInterface.hpp"
|
||||
#include "libslic3r/Print.hpp" // SlicingPipelineStepPlugin, Print, PrintObject
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Workflow context handed to SlicingPipeline plugins. ctx.print / ctx.object
|
||||
// are RAW references into the live slicing graph — the same objects the C++
|
||||
// pipeline mutates. The data-model bindings and the mandatory lifetime rule
|
||||
// (valid only during execute(ctx); mutators invalidate references into replaced
|
||||
// containers, like std::vector iterators) live in
|
||||
// src/slic3r/plugin/host/PluginHostSlicing.cpp.
|
||||
struct SlicingPipelineContext {
|
||||
std::string orca_version;
|
||||
SlicingPipelineStepPlugin step { SlicingPipelineStepPlugin::posSlice };
|
||||
Print* print { nullptr }; // present for in-pipeline steps; null at psGCodePostProcess
|
||||
const PrintObject* object { nullptr }; // null for print-wide steps and psGCodePostProcess
|
||||
// read-only per-plugin settings, populated by the dispatcher from the
|
||||
// plugin's [tool.orcaslicer.plugin.settings] PEP-723 table. Exposed as
|
||||
// ctx.params (dict of string->string).
|
||||
std::map<std::string, std::string> params;
|
||||
// Populated ONLY at Step.psGCodePostProcess (the GUI G-code export/post-process seam,
|
||||
// PostProcessor.cpp). gcode_path is the working G-code file on disk that the plugin edits
|
||||
// in place; host is the target ("File", "OctoPrint", ...); output_name mirrors
|
||||
// SLIC3R_PP_OUTPUT_NAME. Empty at every other step.
|
||||
std::string gcode_path;
|
||||
std::string host;
|
||||
std::string output_name;
|
||||
// C++-only config fallback for psGCodePostProcess (no live Print graph there): config_value()
|
||||
// reads it when `print` is null. Not exposed to Python directly. Never dereferenced elsewhere.
|
||||
const DynamicPrintConfig* full_config { nullptr };
|
||||
bool cancelled() const; // -> print->canceled() (false when print is null)
|
||||
};
|
||||
|
||||
class SlicingPipelinePluginCapability : public PluginCapabilityInterface {
|
||||
public:
|
||||
PluginCapabilityType get_type() const override { return PluginCapabilityType::SlicingPipeline; }
|
||||
virtual ExecutionResult execute(SlicingPipelineContext& ctx) = 0;
|
||||
static void RegisterBindings(pybind11::module_& module, pybind11::enum_<PluginCapabilityType>& pluginTypes);
|
||||
};
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
#include "SlicingPipelinePluginCapability.hpp"
|
||||
#include "slic3r/plugin/PyPluginTrampoline.hpp"
|
||||
#include "slic3r/plugin/PluginAuditManager.hpp"
|
||||
#include <filesystem>
|
||||
|
||||
namespace Slic3r {
|
||||
class PySlicingPipelinePluginCapabilityTrampoline : public PyPluginCommonTrampoline<SlicingPipelinePluginCapability> {
|
||||
public:
|
||||
using PyPluginCommonTrampoline<SlicingPipelinePluginCapability>::PyPluginCommonTrampoline;
|
||||
ExecutionResult execute(SlicingPipelineContext& ctx) override {
|
||||
ORCA_PY_OVERRIDE_AUDITED(
|
||||
::Slic3r::PluginAuditManager::AuditMode::Loading,
|
||||
[&]{
|
||||
// At Step.psGCodePostProcess the plugin edits the exported G-code file, which lives
|
||||
// outside data_dir() (a temp/output folder), so writing to it would otherwise be
|
||||
// blocked by the audit sandbox. Grant that folder as a scoped allowed root. The setup
|
||||
// callback runs AFTER the audit context is constructed, so the scoped root is not
|
||||
// cleared by its constructor. Empty at every other step, so no extra access is
|
||||
// granted to the geometry hooks.
|
||||
if (!ctx.gcode_path.empty())
|
||||
::Slic3r::PluginAuditManager::instance().add_scoped_allowed_root(
|
||||
std::filesystem::path(ctx.gcode_path).parent_path());
|
||||
},
|
||||
PYBIND11_OVERRIDE_PURE,
|
||||
ExecutionResult, SlicingPipelinePluginCapability, execute, ctx);
|
||||
}
|
||||
};
|
||||
} // namespace Slic3r
|
||||
Reference in New Issue
Block a user