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/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/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/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(); 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/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 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; 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); } 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();