From 24cf1c6bcc707d01912caf4096a5291a94aa9565 Mon Sep 17 00:00:00 2001 From: HanifKoh <76276251+HanifKoh@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:29:26 +0800 Subject: [PATCH 1/5] Fix Crash Selecting a User Printer Whose Parent Preset Is Not Loaded (#16579) A user preset can inherit a system preset from a vendor that is not installed. get_preset_base() returns null for it, and get_selected_preset_base() dereferenced that unconditionally, so picking the printer crashed while building the shared profiles notification URL. Fall back to the selected preset itself. The create printer and create filament dialogs had the same null dereference on such presets. --- src/libslic3r/Preset.hpp | 9 ++++++++- src/slic3r/GUI/CreatePresetsDialog.cpp | 4 ++-- tests/libslic3r/test_preset_bundle_loading.cpp | 18 ++++++++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp index b6dc3b37bb..8d5372c5e4 100644 --- a/src/libslic3r/Preset.hpp +++ b/src/libslic3r/Preset.hpp @@ -756,7 +756,14 @@ public: Preset& get_edited_preset() { return m_edited_preset; } const Preset& get_edited_preset() const { return m_edited_preset; } - const Preset& get_selected_preset_base() const { return *get_preset_base(m_presets[m_idx_selected]); } + // The root of the selected preset's inherits chain, or the selected preset itself when + // the chain names a preset that is not loaded (e.g. a vendor that is not installed). + const Preset& get_selected_preset_base() const + { + const Preset &selected = m_presets[m_idx_selected]; + const Preset *base = get_preset_base(selected); + return base ? *base : selected; + } // Return the last saved preset. // const Preset& get_saved_preset() const { return m_saved_preset; } diff --git a/src/slic3r/GUI/CreatePresetsDialog.cpp b/src/slic3r/GUI/CreatePresetsDialog.cpp index 0d5f688948..92925227ac 100644 --- a/src/slic3r/GUI/CreatePresetsDialog.cpp +++ b/src/slic3r/GUI/CreatePresetsDialog.cpp @@ -3151,7 +3151,7 @@ void CreatePrinterPresetDialog::set_current_visible_printer() m_printer_name_to_preset.clear(); for (const Preset &printer_preset : printer_presets) { if (!printer_preset.is_visible) continue; - if (preset_bundle->printers.get_preset_base(printer_preset)->name != printer_preset.name) continue; + if (preset_bundle->printers.get_preset_base(printer_preset) != &printer_preset) continue; if (auto printer_model = dynamic_cast(const_cast(printer_preset).config.option("printer_model", false))) { if (m_printer_name_to_preset.find(printer_model->value) == m_printer_name_to_preset.end()) { printer_choice.push_back(from_u8(printer_model->value)); @@ -4962,7 +4962,7 @@ void CreatePresetForPrinterDialog::get_visible_printer_and_compatible_filament_p if (filament_preset.is_default || !filament_preset.is_compatible || filament_preset.is_project_embedded) continue; ConfigOptionStrings *filament_types; const Preset * filament_preset_base = m_preset_bundle->filaments.get_preset_base(filament_preset); - if (filament_preset_base == &filament_preset) { + if (filament_preset_base == &filament_preset || filament_preset_base == nullptr) { filament_types = dynamic_cast(const_cast(&filament_preset)->config.option("filament_type")); } else { filament_types = dynamic_cast(const_cast(filament_preset_base)->config.option("filament_type")); diff --git a/tests/libslic3r/test_preset_bundle_loading.cpp b/tests/libslic3r/test_preset_bundle_loading.cpp index 7ea1452042..1b4927f08c 100644 --- a/tests/libslic3r/test_preset_bundle_loading.cpp +++ b/tests/libslic3r/test_preset_bundle_loading.cpp @@ -1219,6 +1219,24 @@ TEST_CASE("find_preset2 auto-matches removed Generic vendor profiles to the libr CHECK(bundle.filaments.find_preset2("BrandX Generic PETG", /*auto_match=*/true) == nullptr); } +TEST_CASE("The selected preset is its own base when its parent is not loaded", "[Preset]") +{ + PresetCollection printers(Preset::TYPE_PRINTER, Preset::printer_options(), + static_cast(FullPrintConfig::defaults())); + add_inmemory_preset(printers, "Printer"); + add_inmemory_preset(printers, "User Printer", "Printer"); + add_inmemory_preset(printers, "Orphan Printer", "Uninstalled Vendor Printer"); + + printers.select_preset_by_name("User Printer", true); + REQUIRE(printers.get_selected_preset_name() == "User Printer"); + CHECK(printers.get_selected_preset_base().name == "Printer"); + + printers.select_preset_by_name("Orphan Printer", true); + REQUIRE(printers.get_selected_preset_name() == "Orphan Printer"); + REQUIRE(printers.get_preset_base(printers.get_selected_preset()) == nullptr); + CHECK(printers.get_selected_preset_base().name == "Orphan Printer"); +} + TEST_CASE("Renamed parent is normalized into a loaded preset's inherits", "[Preset][Rename]") { ScopedTemporaryDir temp_dir; From a315a96c5ce1a09673d387860f4d401b5c575043 Mon Sep 17 00:00:00 2001 From: HanifKoh <76276251+HanifKoh@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:30:36 +0800 Subject: [PATCH 2/5] Show "No Available Printers" Instead of an Empty Printer Dropdown (#16611) * Show "No Available Printers" Instead of an Empty Printer Dropdown A ComboBox with no items opened a one-row-high popup that DropDown never paints, leaving a blank strip with stale pixels under the box. Don't open the list when it is empty, and label the printer box in the print and send-to-printer dialogs when no printer is available. * Add "No Available Printers" to the Translation Template --- localization/i18n/OrcaSlicer.pot | 3 +++ src/slic3r/GUI/SelectMachine.cpp | 2 +- src/slic3r/GUI/SendToPrinter.cpp | 2 +- src/slic3r/GUI/Widgets/ComboBox.cpp | 6 +++--- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/localization/i18n/OrcaSlicer.pot b/localization/i18n/OrcaSlicer.pot index c679c1829f..1fbe90b60c 100644 --- a/localization/i18n/OrcaSlicer.pot +++ b/localization/i18n/OrcaSlicer.pot @@ -10657,6 +10657,9 @@ msgstr "" msgid "Send Failed" msgstr "" +msgid "No available printers" +msgstr "" + msgid "The name length exceeds the limit." msgstr "" diff --git a/src/slic3r/GUI/SelectMachine.cpp b/src/slic3r/GUI/SelectMachine.cpp index c6e1ea312e..3f5bf126a2 100644 --- a/src/slic3r/GUI/SelectMachine.cpp +++ b/src/slic3r/GUI/SelectMachine.cpp @@ -4087,7 +4087,7 @@ void SelectMachineDialog::update_user_printer() { m_printer_last_select = ""; update_select_layout(nullptr); - m_printer_box->GetPrinterComboBox()->SetTextLabel(""); + m_printer_box->GetPrinterComboBox()->SetValue(_L("No available printers")); } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "for send task, current printer id = " << m_printer_last_select << std::endl; diff --git a/src/slic3r/GUI/SendToPrinter.cpp b/src/slic3r/GUI/SendToPrinter.cpp index a2a705b8a2..031c5847f4 100644 --- a/src/slic3r/GUI/SendToPrinter.cpp +++ b/src/slic3r/GUI/SendToPrinter.cpp @@ -1229,7 +1229,7 @@ void SendToPrinterDialog::update_user_printer() } else { m_printer_last_select = ""; - m_comboBox_printer->SetTextLabel(""); + m_comboBox_printer->SetValue(_L("No available printers")); } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "for send task, current printer id = " << m_printer_last_select << std::endl; diff --git a/src/slic3r/GUI/Widgets/ComboBox.cpp b/src/slic3r/GUI/Widgets/ComboBox.cpp index 55e261d04d..d58c677205 100644 --- a/src/slic3r/GUI/Widgets/ComboBox.cpp +++ b/src/slic3r/GUI/Widgets/ComboBox.cpp @@ -287,7 +287,7 @@ void ComboBox::DoDeleteOneItem(unsigned int pos) void ComboBox::ForceDropdownOpen() { - if (!IsEnabled()) + if (!IsEnabled() || items.empty()) return; if (!drop_down) { @@ -398,7 +398,7 @@ void ComboBox::mouseDown(wxMouseEvent &event) SetFocus(); if (drop_down) { drop.Hide(); - } else if (drop.HasDismissLongTime()) { + } else if (!items.empty() && drop.HasDismissLongTime()) { // DropDown paints nothing for an empty list drop.autoPosition(); drop_down = true; drop.Popup(&drop); @@ -426,7 +426,7 @@ void ComboBox::keyDown(wxKeyEvent& event) case WXK_SPACE: if (drop_down) { drop.DismissAndNotify(); - } else if (drop.HasDismissLongTime()) { + } else if (!items.empty() && drop.HasDismissLongTime()) { drop.autoPosition(); drop_down = true; drop.Popup(); From d3699dd9f6b292b59a7b01d735a2b53d462c87d7 Mon Sep 17 00:00:00 2001 From: HanifKoh <76276251+HanifKoh@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:48:53 +0800 Subject: [PATCH 3/5] Keep Python Bytecode out of the Bundled Python Home (#16550) * Keep Python Bytecode out of the Bundled Python Home The bundled stdlib ships .pyc files that are stale against their sources: they are compiled when the Python deps are installed, and the copy into the app does not keep the sources' mtimes. CPython then recompiles every module it imports and writes the result next to the source. A drag-installed macOS app rewrites files inside its own bundle on first launch, so its signature no longer verifies, and read-only installs (AppImage, Flatpak, Program Files) recompile the same modules on every launch. Point pycache_prefix at /python/pycache so the interpreter reads and writes bytecode there. The bundle is never written, the cache survives between launches, and the data directory is already a plugin read/write root. * Drop Cached Bytecode Through the Interpreter in the Module Release Test --- src/slic3r/plugin/PythonInterpreter.cpp | 13 +++++++++++++ tests/slic3rutils/test_plugin_lifecycle.cpp | 18 ++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/slic3r/plugin/PythonInterpreter.cpp b/src/slic3r/plugin/PythonInterpreter.cpp index 4a08431117..89dc91723c 100644 --- a/src/slic3r/plugin/PythonInterpreter.cpp +++ b/src/slic3r/plugin/PythonInterpreter.cpp @@ -584,6 +584,19 @@ bool PythonInterpreter::initialize() } BOOST_LOG_TRIVIAL(debug) << "Program name set successfully"; + // Keep bytecode out of the Python home. The shipped .pyc files are stale against their + // sources, so a writable install would rewrite them inside the app (breaking a signed + // macOS bundle's seal) and a read-only one would recompile them on every launch. + const std::string pycache_prefix = data_dir() + "/python/pycache"; +#ifdef _WIN32 + status = PyConfig_SetString(&config, &config.pycache_prefix, boost::nowide::widen(pycache_prefix).c_str()); +#else + status = PyConfig_SetBytesString(&config, &config.pycache_prefix, pycache_prefix.c_str()); +#endif + if (PyStatus_Exception(status)) + BOOST_LOG_TRIVIAL(warning) << "Failed to set the Python bytecode cache to " << pycache_prefix << ": " + << (status.err_msg ? status.err_msg : "unknown error"); + // Use pybind11's scoped_interpreter which properly initializes // pybind11 internals for multi-threaded use — raw Py_InitializeFromConfig // does not set up thread state tracking that pybind11 requires. diff --git a/tests/slic3rutils/test_plugin_lifecycle.cpp b/tests/slic3rutils/test_plugin_lifecycle.cpp index 58a4f1a079..498935c2f6 100644 --- a/tests/slic3rutils/test_plugin_lifecycle.cpp +++ b/tests/slic3rutils/test_plugin_lifecycle.cpp @@ -615,8 +615,22 @@ TEST_CASE("Python module release removes package submodules and owned sys.path", // Ensure the next import executes the new submodule rather than reusing a stale package child. write_package("'new'"); - boost::system::error_code ec; - fs::remove_all(package_root / "__pycache__", ec); + { + // The rewrite can keep the old mtime and size, so drop the bytecode wherever the + // interpreter caches it. + PythonGILState gil; + REQUIRE(gil); + PyObjectPtr util(PyImport_ImportModule("importlib.util")); + REQUIRE(util); + for (const char* file : {"__init__.py", "sub.py"}) { + PyObjectPtr source(PyUnicode_DecodeFSDefault((package_root / file).string().c_str())); + REQUIRE(source); + PyObjectPtr cached(PyObject_CallMethod(util.get(), "cache_from_source", "O", source.get())); + REQUIRE(cached); + boost::system::error_code ec; + fs::remove(fs::path(PyUnicode_AsUTF8(cached.get())), ec); + } + } paths.clear(); modules.clear(); From ac022b56ad043889a141391ab17c1b9947229c05 Mon Sep 17 00:00:00 2001 From: HanifKoh <76276251+HanifKoh@users.noreply.github.com> Date: Mon, 19 Oct 2026 16:06:41 +0800 Subject: [PATCH 4/5] Wait for the Close Frame in the Moonraker Status Stream Test (#16635) The fake printer counts the agent's close frame in its read handler, but Beast answers the close before that read completes, so stop() can return and the test can read the counter before it is incremented. On a loaded runner the check saw 0. Wait for the counter like the other fake-printer counters do, then keep the exact-count check. --- tests/slic3rutils/test_moonraker_printer_agent.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/slic3rutils/test_moonraker_printer_agent.cpp b/tests/slic3rutils/test_moonraker_printer_agent.cpp index 32dca2ffd3..d3559cea1f 100644 --- a/tests/slic3rutils/test_moonraker_printer_agent.cpp +++ b/tests/slic3rutils/test_moonraker_printer_agent.cpp @@ -150,6 +150,13 @@ public: return m_close_frames; } + // The printer answers a close frame before its own read completes, so the agent's stop can return first. + bool wait_for_close_frames(int count) + { + std::unique_lock lock(m_mutex); + return m_changed.wait_for(lock, 10s, [&] { return m_close_frames >= count; }); + } + // Hangs up every connection that is waiting. void release() { @@ -277,6 +284,7 @@ private: if (ec == websocket::error::closed) { std::lock_guard lock(m_mutex); ++m_close_frames; + m_changed.notify_all(); } if (ec) reading = false; @@ -449,6 +457,7 @@ TEST_CASE("Stopping the status stream sends the printer a close frame", "[Moonra REQUIRE(f.printer.wait_for_ready_connections(1)); CHECK(f.stops_within(3s)); + CHECK(f.printer.wait_for_close_frames(1)); CHECK(f.printer.close_frames() == 1); } From 4c2ae6d2022bdba6bc587b74a8a2cf443a206348 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Mon, 19 Oct 2026 16:08:59 +0800 Subject: [PATCH 5/5] Keep Auto Drop off for projects saved by 2.4.x (#16633) 2.4.0 to 2.4.2 wrote auto_drop on each build item, and 2.4.x objects carry no auto_drop attribute in model_settings.config. The importer only read the object attribute and treated a missing one as on, so floating objects were dropped to the bed on load and the flag was rewritten as on on the next save. Record object ids with an item auto_drop="0" and use that as the fallback when the object has no attribute. The attribute's presence is now tracked so the fallback applies only when it is absent. Any item set to off turns auto drop off for the object, since the flag is shared by all instances. --- src/libslic3r/Format/bbs_3mf.cpp | 16 +++++++++++++--- tests/libslic3r/test_3mf.cpp | 22 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index aca428f171..df17940ece 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -988,7 +988,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) MetadataList metadata; VolumeMetadataList volumes; - bool auto_drop { true }; + std::optional auto_drop; }; struct CutObjectInfo @@ -1208,6 +1208,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) //IdToGeometryMap m_orig_geometries; // backup & restore CurrentConfig m_curr_config; IdToMetadataMap m_objects_metadata; + std::set m_items_auto_drop_off; // object ids with a build item auto_drop="0" (2.4.x layout) IdToCutObjectInfoMap m_cut_object_infos; IdToLayerHeightsProfileMap m_layer_heights_profiles; IdToLayerConfigRangesMap m_layer_config_ranges; @@ -1463,6 +1464,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) m_objects.clear(); m_instances.clear(); m_objects_metadata.clear(); + m_items_auto_drop_off.clear(); m_curr_metadata_name.clear(); m_curr_characters.clear(); @@ -1502,6 +1504,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) m_curr_config.object_id = -1; m_curr_config.volume_id = -1; m_objects_metadata.clear(); + m_items_auto_drop_off.clear(); m_layer_heights_profiles.clear(); m_layer_config_ranges.clear(); m_brim_ear_points.clear(); @@ -2257,11 +2260,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) ObjectMetadata::VolumeMetadataList volumes; ObjectMetadata::VolumeMetadataList* volumes_ptr = nullptr; + // 2.4.x stored auto_drop on build items, shared by all instances of an object; any "off" wins + const bool legacy_auto_drop = !m_items_auto_drop_off.count(object.first.second); + IdToMetadataMap::iterator obj_metadata = m_objects_metadata.find(object.first.second); if (obj_metadata != m_objects_metadata.end()) { // config data has been found, this model was saved using slic3r pe - model_object->auto_drop = obj_metadata->second.auto_drop; + model_object->auto_drop = obj_metadata->second.auto_drop.value_or(legacy_auto_drop); // apply object's name and config data for (const Metadata& metadata : obj_metadata->second.metadata) { @@ -2279,6 +2285,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) } else { // config data not found, this model was not saved using slic3r pe + model_object->auto_drop = legacy_auto_drop; // add the entire geometry as the single volume to generate //volumes.emplace_back(0, (int)obj_geometry->second.triangles.size() - 1); @@ -4121,6 +4128,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) std::string path = bbs_get_attribute_value_string(attributes, num_attributes, PPATH_ATTR); Transform3d transform = bbs_get_transform_from_3mf_specs_string(bbs_get_attribute_value_string(attributes, num_attributes, TRANSFORM_ATTR)); int printable = bbs_get_attribute_value_bool(attributes, num_attributes, PRINTABLE_ATTR); + if (const char* auto_drop = bbs_get_attribute_value_charptr(attributes, num_attributes, AUTO_DROP_ATTR); auto_drop != nullptr && !::atoi(auto_drop)) + m_items_auto_drop_off.insert(object_id); return !m_load_model || _create_object_instance(path, object_id, transform, printable, 1); } @@ -4489,7 +4498,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) // int instances_count_id = bbs_get_attribute_value_int(attributes, num_attributes, INSTANCESCOUNT_ATTR); ObjectMetadata metadata; - metadata.auto_drop = bbs_get_attribute_value_bool(attributes, num_attributes, AUTO_DROP_ATTR); + if (bbs_get_attribute_value_charptr(attributes, num_attributes, AUTO_DROP_ATTR) != nullptr) + metadata.auto_drop = bbs_get_attribute_value_bool(attributes, num_attributes, AUTO_DROP_ATTR); m_objects_metadata.insert({ object_id, std::move(metadata) }); m_curr_config.object_id = object_id; return true; diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index f7b5049ebe..7287b9d5d3 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -505,6 +505,28 @@ TEST_CASE("A project whose components reference themselves fails to load", "[3mf REQUIRE_FALSE(loaded); } +TEST_CASE("A project in the 2.4.x layout keeps auto drop off from its build item", "[3mf][Regression]") +{ + // 2.4.x wrote auto_drop on the build item and left it off the object, so the object is re-laid out here. + ScopedTemporaryFile temp(".3mf"); + { + Model model; + ModelObject* object = model.add_object(); + object->add_volume(make_cube(10., 10., 10.)); + object->add_instance(); + object->auto_drop = false; + store_project(model, temp.string(), SaveStrategy::Zip64 | SaveStrategy::Silence); + } + REQUIRE(replace_in_3mf_entry(temp.string(), "model_settings.config", " auto_drop=\"0\"", "")); + REQUIRE(replace_in_3mf_entry(temp.string(), "3dmodel.model", "printable=\"1\"/>", "printable=\"1\" auto_drop=\"0\"/>")); + + ScopedTemporaryDir backup_dir("orca_auto_drop_dst"); + Model model; + REQUIRE(load_project(temp.string(), model, backup_dir)); + REQUIRE(model.objects.size() == 1); + CHECK_FALSE(model.objects.front()->auto_drop); +} + TEST_CASE("An object loads up to the component reference budget and fails past it", "[3mf][Regression]") { // The importer queues at most 100000 component references per object. Every reference besides the