feat(plugin): expose the slicing print-graph as raw orca.host classes + Twistify sample

Adds PluginHostSlicing, which registers the print-graph data model (Print,
PrintObject, Layer, LayerRegion, Surface, ExPolygon, extrusions, ...) into the
orca.host submodule in the same raw-class style as PluginHostApi's Model/Preset
graph, with shared helpers in PluginBindingUtils. SlicingPipelinePluginCapability
is trimmed to the capability surface (the standalone SlicingNumpy helper is folded
away). Adds the Twistify example plugin next to Inset and broadens the binding,
hook, and plugin-install tests.
This commit is contained in:
SoftFever
2026-07-08 00:05:28 +08:00
parent aafcccc83c
commit f81a24abfb
29 changed files with 1718 additions and 962 deletions

View File

@@ -1521,8 +1521,6 @@ void ConfigBase::save_to_json(const std::string &file, const std::string &name,
j[BBL_JSON_KEY_NAME] = name;
j[BBL_JSON_KEY_FROM] = from;
std::vector<std::string> plugin_refs;
//record all the key-values
for (const std::string &opt_key : this->keys())
{
@@ -1548,24 +1546,14 @@ void ConfigBase::save_to_json(const std::string &file, const std::string &name,
json j_array(string_values);
j[opt_key] = j_array;
}
this->save_plugin_collection(opt_key, opt, plugin_refs);
}
// Lazily serialize the top-level "plugins" manifest: the individual plugin-backed options keep
// bare capability names, and the full "name;uuid;capability" references are derived here from
// those options via the registered resolver. Only do this when a resolver is available (GUI);
// without one (CLI/headless) leave whatever the "plugins" option already serialized above, so a
// round-trip never drops the manifest. De-duplicate while preserving order and skip empties.
// Serialize the top-level "plugins" manifest: the individual plugin-backed options keep bare
// capability names; the full "name;uuid;capability" references are derived here (same helper as
// update_plugin_manifest). Only with a resolver (GUI); without one (CLI/headless) leave whatever
// the "plugins" option already serialized above, so a round-trip never drops the manifest.
if (resolve_capability_fn) {
std::vector<std::string> unique_refs;
unique_refs.reserve(plugin_refs.size());
for (std::string& ref : plugin_refs) {
if (ref.empty())
continue;
if (std::find(unique_refs.begin(), unique_refs.end(), ref) == unique_refs.end())
unique_refs.emplace_back(std::move(ref));
}
std::vector<std::string> unique_refs = this->collect_plugin_manifest();
if (unique_refs.empty())
j.erase("plugins");
else
@@ -1610,29 +1598,60 @@ void ConfigBase::save_plugin_collection(const std::string& opt_key, const Config
if (!resolve_capability_fn)
return;
// Resolve a single bare capability value into its full reference and append it, skipping
// unset values and capabilities that could not be resolved (resolver returns "").
const auto append_ref = [&plugin_refs](const std::string& capability_value, const std::string& type) {
// A plugin-backed option declares its capability type via ConfigOptionDef::plugin_type (the same
// metadata PluginResolver::find_option_for_capability scans). Deriving off the def rather than a
// per-key branch keeps this generic across every plugin-backed option.
const ConfigDef* def = this->def();
const ConfigOptionDef* opt_def = def ? def->get(opt_key) : nullptr;
if (opt_def == nullptr || !opt_def->is_plugin_backed())
return;
const std::string& type = opt_def->plugin_type;
// Resolve a single bare capability value into its full reference and append it, skipping unset
// values, capabilities that could not be resolved (resolver returns ""), and duplicates already
// collected (preserving insertion order).
const auto append_ref = [&plugin_refs, &type](const std::string& capability_value) {
if (capability_value.empty())
return;
std::string ref = resolve_capability_fn(capability_value, type);
if (!ref.empty())
if (!ref.empty() && std::find(plugin_refs.begin(), plugin_refs.end(), ref) == plugin_refs.end())
plugin_refs.emplace_back(std::move(ref));
};
if (opt_key == "post_process_plugin") {
const ConfigOptionVectorBase* vec = static_cast<const ConfigOptionVectorBase*>(opt);
for (const std::string& val : vec->vserialize())
append_ref(val, "post-processing");
} else if (opt_key == "printer_agent") {
append_ref((dynamic_cast<const ConfigOptionString *>(opt))->value, "printer-connection");
} else if (opt_key == "slicing_pipeline_plugin") {
if (const auto* vec = dynamic_cast<const ConfigOptionStrings*>(opt)) {
for (const std::string& val : vec->vserialize())
append_ref(val, "slicing-pipeline");
}
}
// Extend for other plugin-backed settings as needed.
// Scalar options carry a single capability name; vector options carry a list. Same scalar/vector
// dispatch as PluginResolver::find_option_for_capability.
if (const auto* string_option = dynamic_cast<const ConfigOptionString*>(opt))
append_ref(string_option->value);
else if (const auto* vector_option = dynamic_cast<const ConfigOptionVectorBase*>(opt))
for (const std::string& val : vector_option->vserialize())
append_ref(val);
}
std::vector<std::string> ConfigBase::collect_plugin_manifest() const
{
std::vector<std::string> refs;
if (!resolve_capability_fn)
return refs;
// Each plugin-backed option (ConfigOptionDef::is_plugin_backed) contributes its resolved
// reference(s) via save_plugin_collection, which appends in order and skips duplicates, so no
// second de-duplication pass is needed here.
for (const std::string& opt_key : this->keys())
if (const ConfigOption* opt = this->option(opt_key))
this->save_plugin_collection(opt_key, opt, refs);
return refs;
}
void ConfigBase::update_plugin_manifest()
{
// Writes the derived manifest back into this config's "plugins" option (save_to_json writes the
// same manifest into a JSON document instead), so an in-memory backend config carries a resolved
// manifest even when the source preset was never serialized (picked-but-unsaved). Without a
// resolver (CLI/headless) leave whatever manifest was loaded from disk untouched.
if (!resolve_capability_fn)
return;
if (auto* manifest = this->option<ConfigOptionStrings>("plugins", true))
manifest->values = this->collect_plugin_manifest();
}
DynamicConfig::DynamicConfig(const ConfigBase& rhs, const t_config_option_keys& keys)

View File

@@ -2444,10 +2444,13 @@ public:
// "serialized" - vector valued option is entered in a single edit field. Values are separated by a semicolon.
// "show_value" - even if enum_values / enum_labels are set, still display the value, not the enum label.
std::string gui_flags;
// Optional plugin type used by GUIType::plugin_picker for filtering plugins.
// Capability type of a plugin-backed option, e.g. "post-processing" / "slicing-pipeline" /
// "printer-connection" (empty for ordinary options). GUIType::plugin_picker filters the plugin
// list by it, and it resolves the option's "plugins" manifest reference; see is_plugin_backed().
std::string plugin_type;
// Indicate whether the option support plugin.
bool support_plugin { false };
// Whether this option holds plugin capability name(s) that feed the "plugins" manifest -- true
// iff it declares a plugin_type. Setting plugin_type is the only step needed to add one.
bool is_plugin_backed() const { return !plugin_type.empty(); }
// Label of the GUI input field.
// In case the GUI input fields are grouped in some views, the label defines a short label of a grouped value,
// while full_label contains a label of a stand-alone field.
@@ -2761,6 +2764,13 @@ public:
//BBS: add json support
void save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const;
// Rebuild the in-memory "plugins" manifest (the "name;uuid;capability" references the plugin
// dispatchers consume) from the plugin-backed options via the registered resolver. save_to_json()
// derives the same manifest, but only when a preset is written to disk; a config assembled in
// memory for the backend (PresetBundle::full_config -> Print::apply) must refresh it here or a
// picked-but-unsaved plugin never resolves at slice/export time. No-op without a resolver.
void update_plugin_manifest();
// Set all the nullable values to nils.
void null_nullables();
@@ -2770,6 +2780,11 @@ private:
// Set a configuration value from a string.
bool set_deserialize_raw(const t_config_option_key& opt_key_src, const std::string& value, ConfigSubstitutionContext& substitutions, bool append);
void save_plugin_collection(const std::string& opt_key, const ConfigOption* opt, std::vector<std::string>& plugin_refs) const;
// Collect the de-duplicated "name;uuid;capability" plugin references derived from this config's
// plugin-backed options via the resolver. Shared by save_to_json (serializes them into the JSON
// manifest) and update_plugin_manifest (writes them back into the "plugins" option). Order is
// preserved and empties are dropped; returns empty without a resolver (CLI/headless).
std::vector<std::string> collect_plugin_manifest() const;
static std::function<std::string(std::string, std::string)> resolve_capability_fn;
};

View File

@@ -1193,6 +1193,7 @@ static std::vector<std::string> s_Preset_print_options{
"min_bead_width",
"post_process",
"post_process_plugin",
"slicing_pipeline_plugin",
"plugins",
"process_change_extrusion_role_gcode",
"min_length_factor",

View File

@@ -2330,6 +2330,17 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
const bool was_done = obj->is_step_done(posSlice);
obj->slice();
hook_after(obj, was_done, posSlice, SlicingPipelineStep::Slice);
// re-snapshot each layer's raw_slices AFTER the Slice hook ran, so the
// plugin's mutation becomes the untyped baseline. Without this, a later
// perimeter-only re-run (make_perimeters -> restore_untyped_slices) reverts
// slices to the PRE-hook geometry while posSlice stays cached (the hook does
// not re-fire), silently un-applying the mutation; raw_slices consumers
// (sharp-tail support, ToolOrdering) also read this backup directly. Gated on
// an active plugin AND a genuine (re)slice, so the inactive path is untouched
// and re-backing-up an unmutated layer is a harmless identical copy.
if (m_pipeline_plugin_active && !was_done && obj->is_step_done(posSlice))
for (Layer *layer : obj->layers())
layer->backup_untyped_slices();
} else {
if (obj->set_started(posSlice)) obj->set_done(posSlice); // shared/duplicate — no hook
}
@@ -2356,6 +2367,14 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
}
for (PrintObject *obj : m_objects) {
if (need_slicing_objects.count(obj) != 0) {
// split prepare_infill (fill-surface prep) from infill (make_fills) so a
// plugin can mutate fill surfaces at the PrepareInfill seam and have make_fills
// consume them (unlike the Infill seam, which fires after the fills are already
// built). infill() re-invokes prepare_infill() as a no-op once posPrepareInfill
// is DONE, so this is a mechanical split mirroring the slice/perimeters loop.
const bool prepare_was_done = obj->is_step_done(posPrepareInfill);
obj->prepare_infill();
hook_after(obj, prepare_was_done, posPrepareInfill, SlicingPipelineStep::PrepareInfill);
const bool was_done = obj->is_step_done(posInfill);
obj->infill();
hook_after(obj, was_done, posInfill, SlicingPipelineStep::Infill);

View File

@@ -883,7 +883,7 @@ enum FilamentCompatibilityType {
};
enum class SlicingPipelineStep {
Slice, Perimeters, EstimateCurledExtrusions, Infill, Ironing, Contouring,
Slice, Perimeters, EstimateCurledExtrusions, PrepareInfill, Infill, Ironing, Contouring,
SupportMaterial, DetectOverhangsForLift, SimplifyPath, WipeTower, SkirtBrim
};

View File

@@ -828,7 +828,10 @@ void PrintConfigDef::init_common_params()
def->tooltip = L("Select the network agent implementation for printer communication.");
def->mode = comAdvanced;
def->cli = ConfigOptionDef::nocli;
def->support_plugin = true;
// Plugin-backed like the pickers, but edited via a dedicated Choice widget rather than a
// plugin_picker field. plugin_type marks it plugin-backed and names its capability type, so its
// "plugins" manifest reference is derived generically (see ConfigOptionDef::is_plugin_backed).
def->plugin_type = "printer-connection";
def->set_default_value(new ConfigOptionString(""));
def = this->add("print_host", coString);
@@ -5118,7 +5121,6 @@ void PrintConfigDef::init_fff_params()
"The plugin will receive the G-code file path and can modify it in place.");
def->gui_type = ConfigOptionDef::GUIType::plugin_picker;
def->plugin_type = "post-processing";
def->support_plugin = true;
def->full_width = true;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionStrings());
@@ -5128,7 +5130,6 @@ void PrintConfigDef::init_fff_params()
def->tooltip = L("Python plugin(s) invoked at each slicing pipeline step to read and modify intermediate slicing data. Research/experimental.");
def->gui_type = ConfigOptionDef::GUIType::plugin_picker;
def->plugin_type = "slicing-pipeline";
def->support_plugin = true;
def->full_width = true;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionStrings());