Compare commits

...

12 Commits

Author SHA1 Message Date
Tobias Gloth
b292a06b3f std::set_union expects its inputs to be sorted. (#13928) 2026-07-21 16:14:14 -03:00
Kris Austin
cc1fe80073 fix: out-of-memory on Windows crashes with no dialog and a misleading stack (#14807) 2026-07-21 15:43:55 -03:00
Kris Austin
b86501cd3c fix: guard Bambu Lab dev-mode dialog against repeat spam (#14774) 2026-07-21 15:40:40 -03:00
packerlschupfer
ae0193899d calib: default-init Calib_Params scalar fields (#14413) 2026-07-21 15:28:36 -03:00
Kris Austin
5d9b641093 fix: guard filament_printable read against a short per-filament array (#14695) 2026-07-21 15:24:15 -03:00
Kiss Lorand
8390d55127 Fix crash when opening Object Settings with mixed nullable values (#14864) 2026-07-21 15:17:22 -03:00
Ian Bassi
23f8ad88d1 Troubleshoot Center Wiki redirection (#14887) 2026-07-21 15:16:07 -03:00
Rodrigo Faselli
21f830bf24 Ensure spiral lift inside print area (avoid collision) (#13634)
* Ensure spiral lift positive quadrant

* check for printable area

* clamp area

* simpler version

* Adjust printable area bounds with safety safety margin

Added safety margin to printable area bounds calculations.

* increase safety margin

* Refactor safety margin calculations in GCodeWriter

* New Logic

Co-Authored-By: Ian Bassi <12130714+ianalexis@users.noreply.github.com>

---------

Co-authored-by: Ian Bassi <12130714+ianalexis@users.noreply.github.com>
2026-07-21 14:23:41 -03:00
Kris Austin
dfb93e0332 fix: PA pattern calibration over-retracts with absolute E distances (#14473) 2026-07-21 12:42:09 -03:00
Ian Chua
355a21626e fix: deep_diff typo (#14879)
# Description

Typo introduced a regression failure. Fixes failing tests.

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-07-21 18:22:34 +08:00
Ian Chua
8de1ff37bd fix: deep_diff typo 2026-07-21 14:52:02 +08:00
Ian Chua
8b93cc5df3 prune stale plugin capability overrides (#14862)
* prune stale plugin capability overrides

* fix: configure button still shows (n) modified configs after removing

* fix: add slicing_pipeline_plugin to deep_diff
2026-07-21 12:55:44 +08:00
16 changed files with 343 additions and 39 deletions

View File

@@ -24,6 +24,8 @@
#include <iostream>
#include <math.h>
#include <csignal>
#include <atomic>
#include <new>
#if defined(__linux__) || defined(__LINUX__)
#include <condition_variable>
@@ -7608,6 +7610,9 @@ LONG WINAPI VectoredExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo)
}*/
#if defined(_MSC_VER) || defined(__MINGW32__)
// Guards against a failed allocation inside the dump re-entering the new-handler.
static std::atomic<bool> g_dump_in_progress{false};
extern "C" {
__declspec(dllexport) int __stdcall orcaslicer_main(int argc, wchar_t **argv)
{
@@ -7626,10 +7631,22 @@ extern "C" {
//AddVectoredExceptionHandler(1, CBaseException::UnhandledExceptionFilter);
SET_DEFULTER_HANDLER();
#endif
// Dump before unwinding, while the stack still names what asked for the memory. Throwing
// std::bad_alloc is standard-permitted here and is what reaches generic_exception_handle().
std::set_new_handler([]() {
int *a = nullptr;
*a = 0;
});
if (!g_dump_in_progress.exchange(true)) {
try {
// A null EXCEPTION_POINTERS walks the calling thread as it stands.
CBaseException base(GetCurrentProcess(), GetCurrentProcessId(), NULL, nullptr);
base.ShowCallstack();
} catch (...) {
// A failed dump must not displace the std::bad_alloc owed to the caller.
}
// ObjParser recovers from std::bad_alloc, so let a later one dump again.
g_dump_in_progress = false;
}
throw std::bad_alloc();
});
// Call the UTF8 main.
return CLI().run(argc, argv_ptrs.data());
}

View File

@@ -2,6 +2,8 @@
#include "CustomGCode.hpp"
#include "I18N.hpp"
#include "PrintConfig.hpp"
#include "ClipperUtils.hpp"
#include "Line.hpp"
#include <algorithm>
#include <iomanip>
#include <iostream>
@@ -99,9 +101,87 @@ void GCodeWriter::apply_print_config(const PrintConfig &print_config)
m_max_jerk_z = LIMITS(machine_max_jerk_z);
m_max_jerk_e = LIMITS(machine_max_jerk_e);
m_resolution = print_config.resolution.value;
#undef LIMITS
#undef LIMITS_UINT
// Orca: capture the printable area(s) so a spiral lift can be skipped when its
// circle would leave the boundary and collide with the print limits. Full polygons
// are stored (not a bounding box) so the check stays correct for non-rectangular
// beds, and per-extruder areas are kept so printers with different boundaries per
// extruder use the right limit for whichever extruder is active.
auto to_scaled_polygon = [](const Pointfs &pts) {
Polygon poly;
poly.points.reserve(pts.size());
for (const Vec2d &p : pts)
poly.points.emplace_back(coord_t(scale_(p.x())), coord_t(scale_(p.y())));
poly.make_counter_clockwise();
return poly;
};
m_bed_printable_area.points.clear();
m_extruder_printable_areas.clear();
if (print_config.printable_area.values.size() >= 3)
m_bed_printable_area = to_scaled_polygon(print_config.printable_area.values);
const std::vector<Pointfs> &extruder_areas = print_config.extruder_printable_area.values;
if (!extruder_areas.empty()) {
m_extruder_printable_areas.resize(extruder_areas.size());
for (size_t i = 0; i < extruder_areas.size(); ++i) {
if (extruder_areas[i].size() < 3) {
// No dedicated area for this extruder: it can reach the whole bed.
m_extruder_printable_areas[i] = m_bed_printable_area;
continue;
}
Polygon extruder_poly = to_scaled_polygon(extruder_areas[i]);
if (m_bed_printable_area.points.size() < 3) {
m_extruder_printable_areas[i] = std::move(extruder_poly);
continue;
}
// The reachable area is the extruder area clipped to the bed. Bed shapes are
// convex in practice, so keep the largest resulting contour.
Polygons clipped = intersection(extruder_poly, m_bed_printable_area);
const Polygon *largest = nullptr;
double best_area = 0.;
for (const Polygon &p : clipped) {
double a = std::abs(p.area());
if (a > best_area) { best_area = a; largest = &p; }
}
m_extruder_printable_areas[i] = largest ? *largest : std::move(extruder_poly);
}
}
}
const Polygon *GCodeWriter::active_printable_area() const
{
if (const Extruder *e = this->filament()) {
size_t id = e->extruder_id();
if (id < m_extruder_printable_areas.size() && m_extruder_printable_areas[id].points.size() >= 3)
return &m_extruder_printable_areas[id];
}
if (m_bed_printable_area.points.size() >= 3)
return &m_bed_printable_area;
return nullptr;
}
bool GCodeWriter::spiral_lift_fits_printable_area(const Vec2d &center, double radius) const
{
const Polygon *area = this->active_printable_area();
if (area == nullptr)
return true; // Boundary unknown: don't restrict (preserve previous behavior).
const Point c = Point::new_scale(center.x(), center.y());
const double r_scaled = scale_(radius);
const double r2 = r_scaled * r_scaled;
// The spiral traces a full circle of `radius` around `center`, so the center must lie
// inside the printable area and every edge must be at least `radius` away from it.
if (!area->contains(c))
return false;
const Points &pts = area->points;
for (size_t i = 0, n = pts.size(); i < n; ++i)
if (Line::distance_to_squared(c, pts[i], pts[(i + 1) % n]) < r2)
return false;
return true;
}
void GCodeWriter::set_extruders(std::vector<unsigned int> extruder_ids)
@@ -731,14 +811,19 @@ std::string GCodeWriter::eager_lift(const LiftType type) {
}
// BBS: spiral lift only safe with known position
// TODO: check the arc will move within bed area
if (type == LiftType::SpiralLift && this->is_current_position_clear()) {
double radius = target_lift / (2 * PI * atan(filament()->travel_slope()));
// static spiral alignment when no move in x,y plane.
// spiral centra is a radius distance to the right (y=0)
// spiral centra is a radius distance to the right (y=0)
Vec2d ij_offset = { radius, 0 };
if (target_lift > 0) {
// Orca: keep the spiral inside the active extruder's printable area, otherwise
// fall back to a normal lift to avoid colliding with the print boundary. m_pos
// includes the plate offset, so remove it to match the printable area coordinates.
const Vec2d spiral_center = { m_pos.x() - m_x_offset + ij_offset.x(), m_pos.y() - m_y_offset + ij_offset.y() };
if (target_lift > 0 && this->spiral_lift_fits_printable_area(spiral_center, radius)) {
lift_move = this->_spiral_travel_to_z(m_pos(2) + target_lift, ij_offset, "spiral lift Z");
} else if (target_lift > 0) {
lift_move = _travel_to_z(m_pos(2) + target_lift, "normal lift Z");
}
}
//BBS: if position is unknown use normal lift
@@ -793,7 +878,15 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
double radius = delta(2) / (2 * PI * atan(this->filament()->travel_slope()));
Vec2d ij_offset = radius * delta_no_z.normalized();
ij_offset = { -ij_offset(1), ij_offset(0) };
slop_move = this->_spiral_travel_to_z(target(2), ij_offset, "spiral lift Z");
// Orca: only perform the spiral lift if its full circle stays inside the
// printable area of the active extruder, otherwise fall back to a normal
// lift to avoid colliding with the print boundary. `source` is already in
// bed coordinates (plate offset removed), matching the printable area.
const Vec2d spiral_center = { source.x() + ij_offset.x(), source.y() + ij_offset.y() };
if (this->spiral_lift_fits_printable_area(spiral_center, radius))
slop_move = this->_spiral_travel_to_z(target(2), ij_offset, "spiral lift Z");
else
slop_move = _travel_to_z(target.z(), "normal lift Z");
}
//BBS: SlopeLift
else if (m_to_lift_type == LiftType::SlopeLift &&

View File

@@ -6,6 +6,7 @@
#include <charconv>
#include "Extruder.hpp"
#include "Point.hpp"
#include "Polygon.hpp"
#include "PrintConfig.hpp"
#include "GCode/CoolingBuffer.hpp"
@@ -181,6 +182,14 @@ public:
// Orca: slicing resolution in mm
double m_resolution = 0.01;
// Orca: printable area polygons (scaled, bed coordinates) used to keep spiral lifts
// from colliding with the print boundary. m_extruder_printable_areas holds the
// per-extruder reachable area (intersected with the bed) when a printer defines
// different boundaries per extruder; m_bed_printable_area is the global fallback.
// Storing full polygons (rather than a bounding box) keeps the check correct for
// non-rectangular beds such as delta/circular printers.
Polygon m_bed_printable_area;
std::vector<Polygon> m_extruder_printable_areas;
std::string m_gcode_label_objects_start;
std::string m_gcode_label_objects_end;
@@ -197,6 +206,10 @@ public:
std::string _travel_to_z(double z, const std::string &comment);
std::string _spiral_travel_to_z(double z, const Vec2d &ij_offset, const std::string &comment);
// Orca: printable area of the active extruder (per-extruder when configured, otherwise the bed). Null when unknown.
const Polygon *active_printable_area() const;
// Orca: true if a full spiral-lift circle (center in bed coordinates, mm) fits inside the active printable area.
bool spiral_lift_fits_printable_area(const Vec2d &center, double radius) const;
std::string _retract(double length, double restart_extra, const std::string &comment);
std::string set_acceleration_internal(Acceleration type, unsigned int acceleration);

View File

@@ -3504,7 +3504,7 @@ inline t_config_option_keys deep_diff(const ConfigBase &config_this, const Confi
if (this_opt != nullptr && other_opt != nullptr && *this_opt != *other_opt)
{
//BBS: add bed_exclude_area
if (opt_key == "printable_area" || opt_key == "bed_exclude_area" || opt_key == "compatible_prints" || opt_key == "compatible_printers" || opt_key == "thumbnails" || opt_key == "wrapping_exclude_area") {
if (opt_key == "printable_area" || opt_key == "bed_exclude_area" || opt_key == "compatible_prints" || opt_key == "compatible_printers" || opt_key == "thumbnails" || opt_key == "wrapping_exclude_area" || opt_key == "slicing_pipeline_plugin") {
// Scalar variable, or a vector variable, which is independent from number of extruders,
// thus the vector is presented to the user as a single input.
diff.emplace_back(opt_key);

View File

@@ -3605,7 +3605,8 @@ std::vector<std::set<int>> Print::get_physical_unprintable_filaments(const std::
return physical_unprintables;
auto get_unprintable_extruder_id = [&](unsigned int filament_idx) -> int {
int status = m_config.filament_printable.values[filament_idx];
// filament_printable may be shorter than the filament count; get_at() clamps.
int status = m_config.filament_printable.get_at(filament_idx);
for (int i = 0; i < extruder_num; ++i) {
if (!(status >> i & 1)) {
return i;

View File

@@ -771,6 +771,7 @@ CustomGCode::Info CalibPressureAdvancePattern::generate_custom_gcodes(const Dyna
}
}
gcode << m_writer.reset_e();
gcode << m_writer.set_pressure_advance(m_params.start);
gcode << "; end pressure advance pattern for layer\n";

View File

@@ -35,10 +35,10 @@ struct Calib_Params
{
Calib_Params() : mode(CalibMode::Calib_None){};
int extruder_id = 0;
double start, end, step;
bool print_numbers;
double freqStartX, freqEndX, freqStartY, freqEndY;
int test_model;
double start = 0.0, end = 1.0, step = 0.1;
bool print_numbers = false;
double freqStartX = 0.0, freqEndX = 1.0, freqStartY = 0.0, freqEndY = 1.0;
int test_model = 0;
std::string shaper_type;
std::vector<double> accelerations;
std::vector<double> speeds;

View File

@@ -558,6 +558,7 @@ namespace Slic3r
}
else
{
Slic3r::GUI::wxGetApp().reset_unsigned_plugin_warning();
if (m_agent)
{
if (it->second->connection_type() != "lan" || it->second->connection_type().empty())

View File

@@ -1043,14 +1043,14 @@ void TextCtrl::propagate_value()
void TextCtrl::set_value(const boost::any& value, bool change_event/* = false*/) {
m_disable_change_event = !change_event;
if (m_opt.nullable) {
const bool m_is_na_val = value.empty() || (boost::any_cast<wxString>(value) == _(L("N/A")));
if (!m_is_na_val)
m_last_meaningful_value = value;
text_ctrl()->SetValue(boost::any_cast<wxString>(value)); // BBS
}
else
text_ctrl()->SetValue(value.empty() ? "" : boost::any_cast<wxString>(value)); // BBS // BBS: null value
text_ctrl()->SetValue(value.empty() ? wxString() : boost::any_cast<wxString>(value));
m_disable_change_event = false;
if (!change_event) {
@@ -1187,18 +1187,24 @@ void CheckBox::set_value(const boost::any& value, bool change_event)
m_disable_change_event = !change_event;
if (m_opt.nullable) {
const bool is_value_unsigned_char = value.type() == typeid(unsigned char);
bool bool_value = false;
m_is_na_val = value.empty() || (is_value_unsigned_char &&
boost::any_cast<unsigned char>(value) == ConfigOptionBoolsNullable::nil_value());
if (!m_is_na_val)
m_last_meaningful_value = is_value_unsigned_char ? value : static_cast<unsigned char>(boost::any_cast<bool>(value));
const auto bool_value = is_value_unsigned_char ?
boost::any_cast<unsigned char>(value) != 0 :
boost::any_cast<bool>(value);
dynamic_cast<::CheckBox*>(window)->SetValue(m_is_na_val ? false : bool_value); // BBS
if (!m_is_na_val) {
bool_value = is_value_unsigned_char ?
boost::any_cast<unsigned char>(value) != 0 :
boost::any_cast<bool>(value);
m_last_meaningful_value = is_value_unsigned_char ? value : static_cast<unsigned char>(bool_value);
}
dynamic_cast<::CheckBox*>(window)->SetValue(bool_value);
}
else if (!value.empty()) // BBS: null value
else if (!value.empty()){ // BBS: null value
dynamic_cast<::CheckBox*>(window)->SetValue(boost::any_cast<bool>(value)); // BBS
}
dynamic_cast<::CheckBox*>(window)->SetHalfChecked(value.empty());
m_disable_change_event = false;
}

View File

@@ -6127,18 +6127,23 @@ bool GUI_App::process_network_msg(std::string dev_id, std::string msg)
}
else if (msg == "unsigned_studio") {
BOOST_LOG_TRIVIAL(info) << "process_network_msg, unsigned_studio";
MessageDialog
msg_dlg(nullptr,
_L("To use OrcaSlicer with Bambu Lab printers, you need to enable LAN mode and Developer mode on your printer.\n\n"
"Please go to your printer's settings and:\n"
"1. Turn on LAN mode\n"
"2. Enable Developer mode\n\n"
"Developer mode allows the printer to work exclusively through local network access, "
"enabling full functionality with OrcaSlicer."),
_L("Network Plug-in Restriction"), wxAPPLY | wxOK);
m_show_error_msgdlg = true;
msg_dlg.ShowModal();
m_show_error_msgdlg = false;
// Plugin re-emits this on every subscribe retry; latch it so it shows
// once per connection episode.
if (!m_show_error_msgdlg && !m_unsigned_plugin_warning_shown) {
m_unsigned_plugin_warning_shown = true;
MessageDialog
msg_dlg(nullptr,
_L("To use OrcaSlicer with Bambu Lab printers, you need to enable LAN mode and Developer mode on your printer.\n\n"
"Please go to your printer's settings and:\n"
"1. Turn on LAN mode\n"
"2. Enable Developer mode\n\n"
"Developer mode allows the printer to work exclusively through local network access, "
"enabling full functionality with OrcaSlicer."),
_L("Network Plug-in Restriction"), wxAPPLY | wxOK);
m_show_error_msgdlg = true;
msg_dlg.ShowModal();
m_show_error_msgdlg = false;
}
return true;
}
}

View File

@@ -343,6 +343,7 @@ private:
public:
//try again when subscription fails
void on_start_subscribe_again(std::string dev_id);
void reset_unsigned_plugin_warning() { m_unsigned_plugin_warning_shown = false; }
std::string get_local_models_path();
bool OnInit() override;
int OnExit() override;

View File

@@ -34,6 +34,7 @@
#include "GUI_App.hpp"
#include "GUI_ObjectList.hpp"
#include "slic3r/Utils/PresetUpdater.hpp"
#include "slic3r/plugin/PluginConfig.hpp"
#include "Plater.hpp"
#include "MainFrame.hpp"
#include "format.hpp"
@@ -1795,9 +1796,19 @@ void Tab::on_value_change(const std::string& opt_key, const boost::any& value)
// Keep this preset's "plugins" manifest in sync when a plugin picker changes, so full_config() and
// save_to_json() always find resolved "name;uuid;capability" references and rebuild it nowhere else.
// Also drop any plugin_config_overrides entries for a capability the change just stopped
// referencing (e.g. a plugin removed from slicing_pipeline_plugin), so a saved preset never
// carries configuration for a capability it no longer names. The Configure button is a separate
// field holding its own cached copy of that value, so it needs to be told explicitly, or it
// keeps showing the stale count until something else happens to refresh it.
if (const ConfigOptionDef* opt_def = m_config->def()->get(opt_key);
opt_def && opt_def->is_plugin_backed())
opt_def && opt_def->is_plugin_backed()) {
m_config->update_plugin_manifest();
if (prune_stale_plugin_overrides(*m_config)) {
if (Field* overrides_field = get_field(PLUGIN_OVERRIDES_OPTION_KEY))
overrides_field->set_value(boost::any(m_config->opt_string(PLUGIN_OVERRIDES_OPTION_KEY)), false);
}
}
if (opt_key == "gcode_flavor" && m_type == Preset::TYPE_PRINTER) {
if (auto printer_tab = dynamic_cast<TabPrinter*>(this))
@@ -3288,7 +3299,19 @@ static std::vector<std::string> intersect(std::vector<std::string> const& l, std
static std::vector<std::string> concat(std::vector<std::string> const& l, std::vector<std::string> const& r)
{
std::vector<std::string> t;
std::set_union(l.begin(), l.end(), r.begin(), r.end(), std::back_inserter(t));
bool l_is_sorted = std::is_sorted(l.begin(), l.end());
bool r_is_sorted = std::is_sorted(r.begin(), r.end());
if (l_is_sorted && r_is_sorted) {
std::set_union(l.begin(), l.end(), r.begin(), r.end(), std::back_inserter(t));
return t;
}
std::vector<std::string> l_sorted = l;
std::vector<std::string> r_sorted = r;
std::sort(l_sorted.begin(), l_sorted.end());
std::sort(r_sorted.begin(), r_sorted.end());
std::set_union(l_sorted.begin(), l_sorted.end(), r_sorted.begin(), r_sorted.end(), std::back_inserter(t));
return t;
}

View File

@@ -186,7 +186,7 @@ TroubleshootDialog::TroubleshootDialog()
Fit();
});
auto link_wiki = new HyperLink(this, _L("Wiki Guide"));
auto link_wiki = new HyperLink(this, _L("Wiki Guide"), "https://www.orcaslicer.com/wiki/troubleshoot_center");
// RIGHT SIZER //////////////////////

View File

@@ -5,6 +5,7 @@
#include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp>
#include <libslic3r/Config.hpp>
#include <libslic3r/PresetBundle.hpp>
#include <libslic3r/PrintConfig.hpp>
#include <slic3r/GUI/GUI.hpp>
@@ -164,6 +165,20 @@ bool CapabilityConfigDocument::erase(const PluginCapabilityId& id)
return erased;
}
bool CapabilityConfigDocument::prune_unreferenced(const std::set<std::pair<PluginCapabilityType, std::string>>& referenced)
{
bool changed = false;
for (auto it = m_entries.begin(); it != m_entries.end();) {
if (referenced.count({it->first.type, it->first.name}) != 0) {
++it;
} else {
it = m_entries.erase(it);
changed = true;
}
}
return changed;
}
bool CapabilityConfigDocument::empty() const
{
return m_entries.empty() && m_opaque_entries.empty();
@@ -342,6 +357,50 @@ std::string serialize_plugin_overrides(const CapabilityConfigDocument& document)
return document.empty() ? std::string() : document.serialize_entries().dump();
}
bool prune_stale_plugin_overrides(DynamicConfig& config)
{
const auto* overrides_opt = dynamic_cast<const ConfigOptionString*>(config.option(PLUGIN_OVERRIDES_OPTION_KEY));
if (overrides_opt == nullptr || overrides_opt->value.empty())
return false;
CapabilityConfigDocument overrides;
std::string error;
if (!parse_plugin_overrides(overrides_opt->value, overrides, error)) {
// Malformed text is not ours to fix up here: leave it untouched rather than risk
// discarding data the user might still be able to recover.
BOOST_LOG_TRIVIAL(error) << "prune_stale_plugin_overrides: " << error;
return false;
}
// Capability names currently referenced by a plugin-backed option's value(s) — e.g.
// slicing_pipeline_plugin's ConfigOptionStrings entries name SlicingPipeline capabilities
// directly, the same raw values save_plugin_collection() resolves into the "plugins" manifest.
std::set<std::pair<PluginCapabilityType, std::string>> referenced;
const ConfigDef* def = config.def();
for (const std::string& opt_key : config.keys()) {
const ConfigOptionDef* opt_def = def != nullptr ? def->get(opt_key) : nullptr;
if (opt_def == nullptr || !opt_def->is_plugin_backed())
continue;
const ConfigOption* opt = config.option(opt_key);
const PluginCapabilityType type = plugin_capability_type_from_string(opt_def->plugin_type);
if (const auto* string_opt = dynamic_cast<const ConfigOptionString*>(opt)) {
if (!string_opt->value.empty())
referenced.emplace(type, string_opt->value);
} else if (const auto* vector_opt = dynamic_cast<const ConfigOptionVectorBase*>(opt)) {
for (const std::string& value : vector_opt->vserialize())
if (!value.empty())
referenced.emplace(type, value);
}
}
if (!overrides.prune_unreferenced(referenced))
return false;
config.set_key_value(PLUGIN_OVERRIDES_OPTION_KEY, new ConfigOptionString(serialize_plugin_overrides(overrides)));
return true;
}
EffectiveCapabilityConfig PresetPluginConfigService::get_effective_config(const CapabilityConfigDocument& overrides,
const PluginCapabilityId& id) const
{

View File

@@ -8,7 +8,9 @@
#include <map>
#include <mutex>
#include <optional>
#include <set>
#include <string>
#include <utility>
#include <vector>
#define PLUGIN_CONFIG_DIR "config.json"
@@ -16,6 +18,7 @@
namespace Slic3r {
class Preset;
class DynamicConfig;
struct CapabilityConfigEntry
{
PluginCapabilityId id;
@@ -35,6 +38,9 @@ public:
bool contains(const PluginCapabilityId& id) const;
bool upsert(CapabilityConfigEntry entry);
bool erase(const PluginCapabilityId& id);
// Drops every entry whose (type, name) is not in `referenced`, e.g. capabilities a preset's
// plugin-backed options no longer name. Returns true if anything was removed.
bool prune_unreferenced(const std::set<std::pair<PluginCapabilityType, std::string>>& referenced);
bool empty() const;
nlohmann::json serialize_entries() const;
nlohmann::json root_json() const;
@@ -50,6 +56,14 @@ std::string plugin_overrides_of(const Preset& preset);
bool parse_plugin_overrides(const std::string& raw, CapabilityConfigDocument& document, std::string& error);
std::string serialize_plugin_overrides(const CapabilityConfigDocument& document);
// Drops plugin_config_overrides entries for capabilities no longer named by any plugin-backed
// option's current value in `config` (e.g. slicing_pipeline_plugin cleared or switched to a
// different capability), and writes the result back if anything changed. Called wherever a
// plugin-backed option's value changes, so a saved preset never carries configuration for a
// capability it no longer references. Returns true if `config` was modified, so a caller holding a
// GUI field over PLUGIN_OVERRIDES_OPTION_KEY knows it must refresh that field's displayed value.
bool prune_stale_plugin_overrides(DynamicConfig& config);
struct EffectiveCapabilityConfig
{
PluginCapabilityId id;

View File

@@ -1,5 +1,9 @@
#include <catch2/catch_all.hpp>
#include <algorithm>
#include <sstream>
#include <string>
#include "libslic3r/calib.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/TriangleMesh.hpp"
@@ -38,3 +42,69 @@ TEST_CASE("Zero calibration line width resolves to a positive default", "[Calib]
REQUIRE(pattern.line_width() > 0.);
REQUIRE(pattern.line_width_first_layer() > 0.);
}
namespace {
struct EndState { double final_e; double max_e; };
EndState simulate_absolute_e(const std::string &gcode)
{
double final_e = 0.;
double max_e = 0.;
std::istringstream lines(gcode);
std::string line;
while (std::getline(lines, line)) {
std::istringstream words(line);
std::string op;
if (!(words >> op))
continue;
if (op != "G1" && op != "G0" && op != "G92")
continue;
std::string word;
while (words >> word) {
if (word.size() >= 2 && word[0] == 'E') {
final_e = std::stod(word.substr(1));
max_e = std::max(max_e, final_e);
break;
}
}
}
return {final_e, max_e};
}
} // namespace
TEST_CASE("PA pattern resets the extruder after the final layer in absolute E mode", "[Calib][Regression]")
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{"use_relative_e_distances", "0"},
{"line_width", "0.45"},
{"initial_layer_line_width", "0.45"},
});
Model model;
model.add_object("cube", "", make_cube(20, 20, 20))->add_instance();
Calib_Params params;
params.mode = CalibMode::Calib_PA_Pattern;
params.start = 0.;
params.end = 0.08;
params.step = 0.002;
CalibPressureAdvancePattern pattern(params, config, /* is_bbl_machine */ false, *model.objects.front(), Vec3d(0, 0, 0));
const CustomGCode::Info info = pattern.generate_custom_gcodes(config, /* is_bbl_machine */ false, *model.objects.front(),
Vec3d(0, 0, 0));
std::string gcode;
for (const CustomGCode::Item &item : info.gcodes)
gcode += item.extra;
const EndState state = simulate_absolute_e(gcode);
REQUIRE(state.max_e > 1.);
REQUIRE_THAT(state.final_e, Catch::Matchers::WithinAbs(0., 1e-9));
}