From e028476d2ac6ee3c775817e6ce4ba3284a06123e Mon Sep 17 00:00:00 2001 From: packerlschupfer <83344883+packerlschupfer@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:46:35 +0200 Subject: [PATCH] 3mf: record the no-light thumbnail in its own status flag (#16307) When a CLI export rendered the no-light thumbnail but no plate thumbnail, the no-light loop set the plate thumbnail's status flag. That skipped the fallback that copies the plate thumbnail from disk, so re-exporting a 3mf that has plate_N.png but no plate_no_light_N.png left plate_N.png out of the package. The loop now sets no_light_thumbnail_status instead. --- src/libslic3r/Format/bbs_3mf.cpp | 2 +- tests/libslic3r/test_3mf.cpp | 64 ++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index 86e306d4ac..54e55fd0a0 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -6542,7 +6542,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) } BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format(",add no light thumbnail %1%'s data into 3mf") % (index + 1); - thumbnail_status[index] = true; + no_light_thumbnail_status[index] = true; } } // Adds the file Metadata/top_i.png and Metadata/pick_i.png diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp index 2d5656b733..3d59c08d24 100644 --- a/tests/libslic3r/test_3mf.cpp +++ b/tests/libslic3r/test_3mf.cpp @@ -2454,3 +2454,67 @@ TEST_CASE("has_restore_data leaves a spent backup collectable", "[3mf]") { CHECK(origin.empty()); } } + +// The no-light pass must not mark the plate thumbnail as written. +SCENARIO("a rendered no-light thumbnail does not suppress the plate thumbnail", "[3mf]") +{ + GIVEN("a plate whose no-light thumbnail is rendered and whose plate thumbnail is only on disk") { + Model model; + const std::string src_file = std::string(TEST_DATA_DIR) + "/test_3mf/Prusa.stl"; + REQUIRE(load_stl(src_file.c_str(), &model)); + model.add_default_instances(); + + ScopedTemporaryDir backup_dir("orca_no_light"); + model.set_backup_path(backup_dir.string()); + + // Only the bytes matter: the exporter copies this file into the archive verbatim. + ScopedTemporaryFile png(".png"); + const std::string png_path = png.string(); + const std::string png_body = "\x89PNG\r\n\x1a\n" "plate-1-thumbnail-from-disk"; + { + boost::nowide::ofstream out(png_path.c_str(), std::ios::binary); + out << png_body; + } + REQUIRE(boost::filesystem::exists(png_path)); + + PlateData plate; + plate.plate_index = 0; + plate.thumbnail_file = png_path; + PlateDataPtrs plates{ &plate }; + + ThumbnailData plate_thumbnail; // nothing was rendered for the plate itself + ThumbnailData no_light_thumbnail; + no_light_thumbnail.set(16, 16); + no_light_thumbnail.pixels.assign(16 * 16 * 4, static_cast(0xff)); + REQUIRE_FALSE(plate_thumbnail.is_valid()); + REQUIRE(no_light_thumbnail.is_valid()); + + WHEN("the project is written") { + ScopedTemporaryFile temp(".3mf"); + const std::string test_file = temp.string(); + + DynamicPrintConfig cfg; + StoreParams sp; + sp.path = test_file.c_str(); + sp.model = &model; + sp.config = &cfg; + sp.plate_data_list = plates; + sp.thumbnail_data = { &plate_thumbnail }; + sp.no_light_thumbnail_data = { &no_light_thumbnail }; + sp.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence; + REQUIRE(store_bbs_3mf(sp)); + + THEN("the plate thumbnail is still copied in from disk") { + std::string got; + REQUIRE(read_cad_recipe_entry(test_file, got, "Metadata/plate_1.png")); + REQUIRE(got == png_body); + } + + THEN("the rendered no-light thumbnail is written as well") { + std::string got; + REQUIRE(read_cad_recipe_entry(test_file, got, "Metadata/plate_no_light_1.png")); + REQUIRE_FALSE(got.empty()); + } + } + } +}