From f6cf18fb96d28c36ab1a539af3279f91d4b6a21b Mon Sep 17 00:00:00 2001 From: mosfet80 <10235105+mosfet80@users.noreply.github.com> Date: Thu, 15 Oct 2026 19:47:21 +0200 Subject: [PATCH 1/5] Update libjpeg-turbo to 3.2.0 (#16098) Also removes the Linux-only JPEG_VERSION block, so Linux builds libjpeg with the default 6.2 ABI like Windows and macOS. --- deps/JPEG/JPEG.cmake | 19 ++----------------- scripts/flatpak/com.orcaslicer.OrcaSlicer.yml | 6 +++--- 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/deps/JPEG/JPEG.cmake b/deps/JPEG/JPEG.cmake index 82b0ca5565..b3e5b06acb 100644 --- a/deps/JPEG/JPEG.cmake +++ b/deps/JPEG/JPEG.cmake @@ -1,22 +1,7 @@ -if(CMAKE_SYSTEM_NAME STREQUAL "Linux") - if (JPEG_VERSION STREQUAL "6") - message("Using Jpeg Lib 62") - set(jpeg_flag "") - elseif (JPEG_VERSION STREQUAL "7") - message("Using Jpeg Lib 70") - set(jpeg_flag "-DWITH_JPEG7=ON") - else () - message("Using Jpeg Lib 80") - set(jpeg_flag "-DWITH_JPEG8=ON") - endif () -endif() - orcaslicer_add_cmake_project(JPEG - URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.0.1.zip - URL_HASH SHA256=d6d99e693366bc03897677650e8b2dfa76b5d6c54e2c9e70c03f0af821b0a52f - DEPENDS ${ZLIB_PKG} + URL https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.2.0.zip + URL_HASH SHA256=1b4c2cb6cba1ca5e53b9e85eeeab02154cab4b70894c96e97a9c747a26d19d63 CMAKE_ARGS -DENABLE_SHARED=OFF -DENABLE_STATIC=ON - ${jpeg_flag} ) diff --git a/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml b/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml index a8cbf1d868..cca417f735 100644 --- a/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml +++ b/scripts/flatpak/com.orcaslicer.OrcaSlicer.yml @@ -336,10 +336,10 @@ modules: sha256: 3d22d46c566b1761a0e15ea397589b3a5f36ac09b7c785382e6470156c04247f dest: external-packages/PNG - # libjpeg-turbo 3.0.1 + # libjpeg-turbo 3.2.0 - type: file - url: https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.0.1.zip - sha256: d6d99e693366bc03897677650e8b2dfa76b5d6c54e2c9e70c03f0af821b0a52f + url: https://github.com/libjpeg-turbo/libjpeg-turbo/archive/refs/tags/3.2.0.zip + sha256: 1b4c2cb6cba1ca5e53b9e85eeeab02154cab4b70894c96e97a9c747a26d19d63 dest: external-packages/JPEG # Freetype 2.12.1 From b00237807dc76bec2c967d5330497c934e95e2ce Mon Sep 17 00:00:00 2001 From: Matias Alejandro Yocca Date: Thu, 15 Oct 2026 18:13:28 -0300 Subject: [PATCH 2/5] Fix crash when dragging objects in sequential printing mode (#16029) The clearance preview shown while dragging sorted instances with a comparator that is not a strict weak ordering, which aborts with libc++ debug hardening. The preview now uses object-list order, with each object's copies in their last validated print order, which is the order print validation uses. The height check is now in SequentialPrintClearance.hpp, with unit tests. --- src/slic3r/CMakeLists.txt | 1 + src/slic3r/GUI/GLCanvas3D.cpp | 106 ++---------- src/slic3r/GUI/SequentialPrintClearance.hpp | 63 +++++++ tests/slic3rutils/CMakeLists.txt | 1 + .../slic3rutils/test_sequential_clearance.cpp | 162 ++++++++++++++++++ 5 files changed, 242 insertions(+), 91 deletions(-) create mode 100644 src/slic3r/GUI/SequentialPrintClearance.hpp create mode 100644 tests/slic3rutils/test_sequential_clearance.cpp diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 05650d2275..367aadaa27 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -222,6 +222,7 @@ set(SLIC3R_GUI_SOURCES GUI/Gizmos/GLGizmoUtils.hpp GUI/GLCanvas3D.cpp GUI/GLCanvas3D.hpp + GUI/SequentialPrintClearance.hpp GUI/GLModel.cpp GUI/GLModel.hpp GUI/GLSelectionRectangle.cpp diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index bf87241fb6..95944a8da5 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -65,6 +65,7 @@ #include #include "slic3r/GUI/Selection.hpp" #include "GLCanvas3D.hpp" +#include "SequentialPrintClearance.hpp" #ifdef SLIC3R_CAD #include "slic3r/GUI/CAD/DesignSketchTool.hpp" // Design tab: interactive 2D sketch tool #endif @@ -6397,17 +6398,16 @@ void GLCanvas3D::update_sequential_clearance() //BBS: add the height logic PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_curr_plate(); Polygons polygons; - std::vector> height_polygons; polygons.reserve(instances_count); - height_polygons.reserve(instances_count); - std::vector convex_and_bounding_boxes; - struct height_info - { - double instance_height; - BoundingBox bounding_box; - Polygon hull_polygon; - }; + std::vector convex_and_bounding_boxes; + convex_and_bounding_boxes.reserve(instances_count); + std::map print_order; + for (const PrintObject* print_object : fff_print()->objects()) + for (const PrintInstance& instance : print_object->instances()) + print_order.emplace(instance.model_instance->id(), instance.model_instance->arrange_order); + for (size_t i = 0; i < instance_transforms.size(); ++i) { + const size_t first_instance = convex_and_bounding_boxes.size(); const auto& instances = instance_transforms[i]; double rotation_z0 = instances.front()->get_rotation().z(); int index = 0; @@ -6427,96 +6427,20 @@ void GLCanvas3D::update_sequential_clearance() Polygon convex_hull(std::move(inst_pts)); BoundingBox bouding_box = convex_hull.bounding_box(); BoundingBox plate_bb = plate->get_bounding_box_crd(); + const ObjectID instance_id = m_model->objects[i]->instances[index]->id(); double instance_height = m_model->objects[i]->get_instance_max_z(index++); //skip the object for not current plate if (!plate_bb.overlap(bouding_box)) continue; - convex_and_bounding_boxes.push_back({instance_height, bouding_box, convex_hull}); + convex_and_bounding_boxes.push_back({instance_height, bouding_box, convex_hull, instance_id}); polygons.emplace_back(std::move(convex_hull)); } + sort_sequential_clearance_instances(convex_and_bounding_boxes.begin() + first_instance, convex_and_bounding_boxes.end(), print_order); } - //sort the print instance - std::sort(convex_and_bounding_boxes.begin(), convex_and_bounding_boxes.end(), - [](auto &l, auto &r) { - auto ly1 = l.bounding_box.min.y(); - auto ly2 = l.bounding_box.max.y(); - auto ry1 = r.bounding_box.min.y(); - auto ry2 = r.bounding_box.max.y(); - auto inter_min = std::max(ly1, ry1); - auto inter_max = std::min(ly2, ry2); - auto lx = l.bounding_box.min.x(); - auto rx = r.bounding_box.min.x(); - if (inter_max - inter_min > 0) - return (lx < rx) || ((lx == rx)&&(ly1 < ry1)); - else - return (ly1 < ry1); - }); - - /*bool has_interlaced_objects = false; - for (int k = 0; k < bounding_box_count; k++) - { - Polygon& convex = convex_and_bounding_boxes[k].hull_polygon; - BoundingBox& bbox = convex_and_bounding_boxes[k].bounding_box; - auto iy1 = bbox.min.y(); - auto iy2 = bbox.max.y(); - - for (int i = k+1; i < bounding_box_count; i++) - { - Polygon& next_convex = convex_and_bounding_boxes[i].hull_polygon; - BoundingBox& next_bbox = convex_and_bounding_boxes[i].bounding_box; - auto py1 = next_bbox.min.y(); - auto py2 = next_bbox.max.y(); - auto inter_min = std::max(iy1, py1); // min y of intersection - auto inter_max = std::min(iy2, py2); // max y of intersection. length=max_y-min_y>0 means intersection exists - if (inter_max - inter_min > 0) { - has_interlaced_objects = true; - break; - } - } - if (has_interlaced_objects) - break; - }*/ - - int bounding_box_count = convex_and_bounding_boxes.size(); - double printable_height = fff_print()->config().printable_height; - double hc1 = fff_print()->config().extruder_clearance_height_to_lid; - double hc2 = fff_print()->config().extruder_clearance_height_to_rod; - for (int k = 0; k < bounding_box_count; k++) - { - Polygon& convex = convex_and_bounding_boxes[k].hull_polygon; - BoundingBox& bbox = convex_and_bounding_boxes[k].bounding_box; - auto iy1 = bbox.min.y(); - auto iy2 = bbox.max.y(); - double height = (k == (bounding_box_count - 1))?printable_height:hc1; - - /*if (has_interlaced_objects) { - if ((k < (bounding_box_count - 1)) && (convex_and_bounding_boxes[k].instance_height > hc2)) { - height_polygons.emplace_back(std::make_pair(convex, hc2)); - } - } - else { - if ((k < (bounding_box_count - 1)) && (convex_and_bounding_boxes[k].instance_height > hc1)) { - height_polygons.emplace_back(std::make_pair(convex, hc1)); - } - }*/ - - for (int i = k+1; i < bounding_box_count; i++) - { - Polygon& next_convex = convex_and_bounding_boxes[i].hull_polygon; - BoundingBox& next_bbox = convex_and_bounding_boxes[i].bounding_box; - auto py1 = next_bbox.min.y(); - auto py2 = next_bbox.max.y(); - auto inter_min = std::max(iy1, py1); // min y of intersection - auto inter_max = std::min(iy2, py2); // max y of intersection. length=max_y-min_y>0 means intersection exists - if (inter_max - inter_min > 0) { - height = hc2; - break; - } - } - if (height < convex_and_bounding_boxes[k].instance_height) - height_polygons.emplace_back(std::make_pair(convex, height)); - } + const auto& config = fff_print()->config(); + const auto height_polygons = sequential_clearance_height_polygons(convex_and_bounding_boxes, config.printable_height, + config.extruder_clearance_height_to_lid, config.extruder_clearance_height_to_rod); // sends instances 2d hulls to be rendered set_sequential_print_clearance_visible(true); diff --git a/src/slic3r/GUI/SequentialPrintClearance.hpp b/src/slic3r/GUI/SequentialPrintClearance.hpp new file mode 100644 index 0000000000..0f5d79be9c --- /dev/null +++ b/src/slic3r/GUI/SequentialPrintClearance.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include "libslic3r/BoundingBox.hpp" +#include "libslic3r/ObjectID.hpp" +#include "libslic3r/Polygon.hpp" + +#include +#include +#include +#include +#include + +namespace Slic3r::GUI { + +struct SequentialClearanceInstance +{ + double instance_height; + BoundingBox bounding_box; + Polygon hull_polygon; + ObjectID instance_id; +}; + +// PrintApply may group rotated copies in a different order from ModelObject::instances. +// Reorder one object's instances only when validation has assigned all of them an order. +inline void sort_sequential_clearance_instances( + std::vector::iterator first, std::vector::iterator last, + const std::map& print_order) +{ + if (!std::all_of(first, last, [&print_order](const SequentialClearanceInstance& instance) { + const auto order = print_order.find(instance.instance_id); + return order != print_order.end() && order->second > 0; + })) + return; + + std::stable_sort(first, last, [&print_order](const SequentialClearanceInstance& lhs, const SequentialClearanceInstance& rhs) { + return print_order.at(lhs.instance_id) < print_order.at(rhs.instance_id); + }); +} + +// Input follows object-list order with validated print order within each object. +inline std::vector> sequential_clearance_height_polygons( + const std::vector& instances, double printable_height, double height_to_lid, double height_to_rod) +{ + std::vector> height_polygons; + height_polygons.reserve(instances.size()); + for (size_t k = 0; k < instances.size(); ++k) { + const auto& instance = instances[k]; + double height = k + 1 == instances.size() ? printable_height : height_to_lid; + for (size_t i = k + 1; i < instances.size(); ++i) { + const auto& next_bbox = instances[i].bounding_box; + if (std::min(instance.bounding_box.max.y(), next_bbox.max.y()) > + std::max(instance.bounding_box.min.y(), next_bbox.min.y())) { + height = height_to_rod; + break; + } + } + if (height < instance.instance_height) + height_polygons.emplace_back(instance.hull_polygon, float(height)); + } + return height_polygons; +} + +} // namespace Slic3r::GUI diff --git a/tests/slic3rutils/CMakeLists.txt b/tests/slic3rutils/CMakeLists.txt index ac774899e7..83632601f7 100644 --- a/tests/slic3rutils/CMakeLists.txt +++ b/tests/slic3rutils/CMakeLists.txt @@ -47,6 +47,7 @@ add_executable(${_TEST_NAME}_tests test_file_url.cpp test_imgui_wrapper.cpp test_user_manager.cpp + test_sequential_clearance.cpp ../fff_print/test_helpers.cpp ) diff --git a/tests/slic3rutils/test_sequential_clearance.cpp b/tests/slic3rutils/test_sequential_clearance.cpp new file mode 100644 index 0000000000..8068212a32 --- /dev/null +++ b/tests/slic3rutils/test_sequential_clearance.cpp @@ -0,0 +1,162 @@ +#include +#include +#include + +#include "catch2/matchers/catch_matchers.hpp" +#include "fff_print/test_helpers.hpp" +#include "slic3r/GUI/SequentialPrintClearance.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace Slic3r; +using namespace Slic3r::GUI; +using Catch::Matchers::WithinAbs; + +namespace { + +SequentialClearanceInstance instance(coord_t x, coord_t y, double height) +{ + return {height, BoundingBox(Point(x, y), Point(x + 5, y + 10)), + Polygon{Point(x, y), Point(x + 5, y), Point(x + 5, y + 10), Point(x, y + 10)}}; +} + +} // namespace + +TEST_CASE("Sequential clearance preserves model print order when objects move", "[SequentialClearance][Regression]") +{ + const coord_t last_y = GENERATE(-20, 20); + std::vector instances{instance(20, 0, 50.), instance(0, last_y, 50.)}; + const auto warnings = sequential_clearance_height_polygons(instances, 200., 30., 10.); + + REQUIRE(warnings.size() == 1); + CHECK(warnings.front().first == instance(20, 0, 50.).hull_polygon); + CHECK_THAT(warnings.front().second, WithinAbs(30., 1e-6)); +} + +TEST_CASE("Sequential clearance handles chains of overlapping Y ranges", "[SequentialClearance][Regression]") +{ + // A overlaps B, B overlaps C, but A does not overlap C. Combining X order + // for overlaps with Y order otherwise formed the cycle B < A < C < B. + std::vector instances{ + instance(20, 0, 50.), instance(10, 8, 50.), instance(0, 16, 50.)}; + const auto warnings = sequential_clearance_height_polygons(instances, 200., 30., 10.); + + REQUIRE(warnings.size() == 2); + CHECK(warnings[0].first == instance(20, 0, 50.).hull_polygon); + CHECK(warnings[1].first == instance(10, 8, 50.).hull_polygon); + CHECK_THAT(warnings[0].second, WithinAbs(10., 1e-6)); + CHECK_THAT(warnings[1].second, WithinAbs(10., 1e-6)); +} + +TEST_CASE("Sequential clearance applies rod height only to positive Y overlap", "[SequentialClearance]") +{ + const coord_t next_y = GENERATE(9, 10, 11); + std::vector instances{instance(0, 0, 50.), instance(20, next_y, 50.)}; + const auto warnings = sequential_clearance_height_polygons(instances, 200., 30., 10.); + + REQUIRE(warnings.size() == 1); + CHECK(warnings.front().first == instances.front().hull_polygon); + CHECK_THAT(warnings.front().second, WithinAbs(next_y < 10 ? 10. : 30., 1e-6)); +} + +TEST_CASE("Sequential clearance allows the last object up to printable height", "[SequentialClearance]") +{ + std::vector instances; + CHECK(sequential_clearance_height_polygons(instances, 200., 30., 10.).empty()); + + instances.push_back(instance(0, 0, 200.)); + CHECK(sequential_clearance_height_polygons(instances, 200., 30., 10.).empty()); + + instances.front().instance_height = 201.; + const auto warnings = sequential_clearance_height_polygons(instances, 200., 30., 10.); + REQUIRE(warnings.size() == 1); + CHECK_THAT(warnings.front().second, WithinAbs(200., 1e-6)); +} + +TEST_CASE("Sequential clearance warns the same rotated copy as print validation", "[SequentialClearance][Regression]") +{ + constexpr double rod_height = 10.; + constexpr double lid_height = 30.; + constexpr double printable_height = 200.; + Model model = Test::model("rotated copies", make_cube(10., 10., 50.)); + ModelObject* object = model.objects.front(); + object->instances.front()->set_offset(Vec3d(150., 80., 0.)); + ModelInstance* rotated = object->add_instance(); + rotated->set_offset(Vec3d(50., 80., 0.)); + rotated->set_rotation(Z, PI / 2.); + + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({{"print_sequence", "by object"}, {"extruder_clearance_radius", 20.}, + {"extruder_clearance_height_to_lid", lid_height}, {"extruder_clearance_height_to_rod", rod_height}, + {"printable_height", printable_height}}); + Print print; + print.auto_assign_extruders(object); + print.apply(model, config); + Polygons polygons; + std::vector> validation_warnings; + Print::sequential_print_clearance_valid(print, &polygons, &validation_warnings); + + std::map print_order; + for (const PrintObject* print_object : print.objects()) + for (const PrintInstance& print_instance : print_object->instances()) + print_order.emplace(print_instance.model_instance->id(), print_instance.model_instance->arrange_order); + + std::vector instances; + for (size_t i = 0; i < object->instances.size(); ++i) { + Polygon hull = object->convex_hull_2d(object->instances[i]->get_matrix()); + instances.push_back({object->get_instance_max_z(i), hull.bounding_box(), hull, object->instances[i]->id()}); + } + const Polygon expected_warning = instances[1].hull_polygon; + REQUIRE(validation_warnings.size() == 1); + CHECK(validation_warnings.front().first.bounding_box().contains(instances[1].bounding_box.center())); + CHECK_FALSE(validation_warnings.front().first.bounding_box().contains(instances[0].bounding_box.center())); + + sort_sequential_clearance_instances(instances.begin(), instances.end(), print_order); + const auto warnings = sequential_clearance_height_polygons(instances, printable_height, lid_height, rod_height); + REQUIRE(warnings.size() == 1); + CHECK(warnings.front().first == expected_warning); + CHECK_THAT(warnings.front().second, WithinAbs(rod_height, 1e-6)); +} + +TEST_CASE("Sequential clearance keeps copy order when print order is unavailable", "[SequentialClearance]") +{ + std::vector instances{instance(20, 0, 50.), instance(0, 0, 50.)}; + instances[0].instance_id = ObjectID(1); + instances[1].instance_id = ObjectID(2); + std::map print_order{{ObjectID(1), 2}, {ObjectID(2), 1}}; + + SECTION("No validation order") { print_order.clear(); } + SECTION("New copy missing from print") { print_order.erase(ObjectID(2)); } + SECTION("Copy has not been ordered") { print_order[ObjectID(2)] = 0; } + SECTION("Copy has an invalid order") { print_order[ObjectID(2)] = -1; } + + sort_sequential_clearance_instances(instances.begin(), instances.end(), print_order); + CHECK(instances[0].instance_id == ObjectID(1)); + CHECK(instances[1].instance_id == ObjectID(2)); +} + +TEST_CASE("Sequential clearance keeps object order when sorting copies", "[SequentialClearance]") +{ + std::vector instances(4, instance(0, 0, 50.)); + for (size_t i = 0; i < instances.size(); ++i) + instances[i].instance_id = ObjectID(instances.size() - i); + const std::map print_order{{ObjectID(1), 1}, {ObjectID(2), 2}, {ObjectID(3), 3}, {ObjectID(4), 4}}; + + // The two objects were reordered in the model since the last validation. + sort_sequential_clearance_instances(instances.begin(), instances.begin() + 2, print_order); + sort_sequential_clearance_instances(instances.begin() + 2, instances.end(), print_order); + CHECK(instances[0].instance_id == ObjectID(3)); + CHECK(instances[1].instance_id == ObjectID(4)); + CHECK(instances[2].instance_id == ObjectID(1)); + CHECK(instances[3].instance_id == ObjectID(2)); +} From 109d09e0b8a25490ee0394f55769b1d735c36bba Mon Sep 17 00:00:00 2001 From: Error404JoyNotFound Date: Fri, 16 Oct 2026 03:05:53 +0530 Subject: [PATCH 3/5] Fix multi-tool standby heaters by turning off unused extruders (#15849) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With ooze prevention on, an extruder that has no layers left is set to 0 °C when it is parked, instead of its standby temperature. On IMEX printers where several filaments share one heater, the heater is only turned off once every filament on it is done. --- src/libslic3r/GCode.cpp | 45 +++++--- src/libslic3r/GCode.hpp | 1 + src/libslic3r/GCode/ToolOrdering.cpp | 15 ++- src/libslic3r/GCode/ToolOrdering.hpp | 3 + .../wipe_tower_temperature_trace_main.txt | 2 +- tests/fff_print/test_multifilament.cpp | 109 ++++++++++++++++++ 6 files changed, 155 insertions(+), 20 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index ce2be2db58..508ab5b421 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -432,27 +432,36 @@ static std::vector get_path_of_change_filament(const Print& print) std::string OozePrevention::pre_toolchange(GCode& gcodegen) { - std::string gcode; - unsigned int extruder_id = gcodegen.writer().filament()->id(); - const auto& filament_idle_temp = gcodegen.config().idle_temperature; - if (filament_idle_temp.get_at(extruder_id) == 0) { - // There is no idle temperature defined in filament settings. - // Use the delta value from print config. - if (gcodegen.config().standby_temperature_delta.value != 0) { - // we assume that heating is always slower than cooling, so no need to block - gcode += gcodegen.writer().set_temperature - (this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value, false, extruder_id); - gcode.pop_back(); - gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed - } - } else { - // Use the value from filament settings. That one is absolute, not delta. - gcode += gcodegen.writer().set_temperature(filament_idle_temp.get_at(extruder_id), false, extruder_id); - gcode.pop_back(); - gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed + + // Check if this tool will ever be used again in this print. + bool is_last_use = false; + if (gcodegen.m_print != nullptr && !gcodegen.m_print->tool_ordering().empty()) { + coordf_t current_z = gcodegen.layer() ? gcodegen.layer()->print_z : gcodegen.writer().get_position().z(); + const auto& tool_ordering = gcodegen.m_print->tool_ordering(); + const auto& cfg = gcodegen.config(); + const int heater = imex_physical_heater_for(cfg.is_imex.value, cfg.physical_extruder_map, int(extruder_id)); + is_last_use = tool_ordering.is_last_extrusion_layer(current_z, extruder_id); + for (unsigned int f : tool_ordering.all_extruders()) + if (is_last_use && imex_physical_heater_for(cfg.is_imex.value, cfg.physical_extruder_map, int(f)) == heater) + is_last_use = tool_ordering.is_last_extrusion_layer(current_z, f); } + int temp = 0; + if (!is_last_use) { + const auto& filament_idle_temp = gcodegen.config().idle_temperature; + if (filament_idle_temp.get_at(extruder_id) == 0) { + if (gcodegen.config().standby_temperature_delta.value == 0) + return std::string(); + temp = this->_get_temp(gcodegen) + gcodegen.config().standby_temperature_delta.value; + } else { + temp = filament_idle_temp.get_at(extruder_id); + } + } + + std::string gcode = gcodegen.writer().set_temperature(temp, false, extruder_id); + gcode.pop_back(); + gcode += " ;cooldown\n"; // this is a marker for GCodeProcessor, so it can supress the commands when needed return gcode; } diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 08894574ed..b5898b2305 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -1004,6 +1004,7 @@ protected: size_t num_objects, size_t num_islands); + friend class OozePrevention; friend class Wipe; friend class WipeTowerIntegration; friend class PressureEqualizer; diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index 19f5c686f6..2ea34aef90 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -1276,6 +1276,13 @@ void ToolOrdering::collect_extruder_statistics(bool prime_multi_material) sort_remove_duplicates(m_all_printing_extruders); } + m_last_layer_per_extruder.clear(); + for (const LayerTools& lt : m_layer_tools) { + for (unsigned int ext : lt.extruders) { + m_last_layer_per_extruder[ext] = lt.print_z; + } + } + if (prime_multi_material && ! m_all_printing_extruders.empty()) { // Reorder m_all_printing_extruders in the sequence they will be primed, the last one will be m_first_printing_extruder. // Then set m_first_printing_extruder to the 1st extruder primed. @@ -3664,5 +3671,11 @@ int WipingExtrusions::get_support_interface_extruder_overrides(const PrintObject return -1; } - +bool ToolOrdering::is_last_extrusion_layer(coordf_t print_z, unsigned int extruder_id) const +{ + auto it = m_last_layer_per_extruder.find(extruder_id); + if (it == m_last_layer_per_extruder.end()) + return true; + return print_z >= it->second - EPSILON; +} } // namespace Slic3r diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index 5c11f482cd..b7aca7dd30 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -370,6 +370,7 @@ public: std::vector & initial_filaments); bool has_non_support_filament(const PrintConfig &config); + bool is_last_extrusion_layer(coordf_t print_z, unsigned int extruder_id) const; private: void initialize_layers(std::vector &zs); @@ -413,6 +414,8 @@ private: // which keeps every legacy per-object code path bit-identical (loops over an empty map are // no-ops; downstream emission falls through to the per-object branch). std::map>> m_gradient_volume_layers; + // Map: extruder_id -> print_z (z coordinate) of the last layer it is used on + std::map m_last_layer_per_extruder; const PrintObject* m_print_object_ptr = nullptr; Print* m_print; bool m_sorted = false; diff --git a/tests/data/wipe_tower_temperature_trace_main.txt b/tests/data/wipe_tower_temperature_trace_main.txt index f755008c26..2ff2b4f0a4 100644 --- a/tests/data/wipe_tower_temperature_trace_main.txt +++ b/tests/data/wipe_tower_temperature_trace_main.txt @@ -163,7 +163,7 @@ M109 S240 T0 ; set nozzle temperature and wait for it to be reached ; CP TOOLCHANGE END M104 S240 T1 ; preheat T1 time: 30s lead 30.0s ; CP TOOLCHANGE START -M104 S200 T0 ; set nozzle temperature ;cooldown +M104 S0 T0 ; set nozzle temperature ;cooldown T1 ; change extruder M109 S240 T1 ; set nozzle temperature and wait for it to be reached ; CP TOOLCHANGE END diff --git a/tests/fff_print/test_multifilament.cpp b/tests/fff_print/test_multifilament.cpp index 54a98d2268..b7fccf2890 100644 --- a/tests/fff_print/test_multifilament.cpp +++ b/tests/fff_print/test_multifilament.cpp @@ -1742,3 +1742,112 @@ TEST_CASE("Each filament cools with the fan speeds of its extruder variant", "[M CHECK(gcode.find("; start range high " + std::to_string(range_high) + "\n") != std::string::npos); } } + +TEST_CASE("Shared heater on IMEX printer stays hot until all filaments on it are done", "[MultiFilament]") +{ + DynamicPrintConfig cfg = multifilament_config(7); + cfg.set_deserialize_strict({ + {"ooze_prevention", 1}, + {"preheat_time", 0}, + {"single_extruder_multi_material", 0}, + }); + imex_7x4_printer(cfg); + + const std::string gcode = slice_with_object_overrides({make_cube(20., 20., 10.), make_cube(20., 20., 4.), make_cube(20., 20., 10.)}, + cfg, + {{{"extruder", 1}, {"layer_height", 0.30}}, + {{"extruder", 2}, {"layer_height", 0.15}}, + {{"extruder", 5}}}); + + bool premature_s0_t0 = false; + double current_z = 0.0; + + GCodeReader reader; + reader.parse_buffer(gcode, [&](GCodeReader& /*self*/, const GCodeReader::GCodeLine& line) { + if (line.has_z()) + current_z = line.z(); + + const std::string raw = line.raw(); + if (raw.find("M104 S0 T0") != std::string::npos && raw.find(";cooldown") != std::string::npos) { + if (current_z < 9.0) + premature_s0_t0 = true; + } + }); + + // Filaments 1 and 2 share head 0, and filament 1 prints up to 10mm. + // T0 must not shut down at ~4mm when filament 2 finishes. + CHECK_FALSE(premature_s0_t0); +} + +TEST_CASE("Unused extruder turns off after its final layer on multi-object print", "[MultiFilament]") +{ + const std::string gcode = slice_with_object_overrides( + { make_cube(20., 20., 10.), make_cube(20., 20., 4.) }, + multifilament_config(2, { + { "single_extruder_multi_material", 0 }, + { "ooze_prevention", 1 }, + { "standby_temperature_delta", -40 }, + { "nozzle_temperature", "240,240" }, + }), + { + { { "extruder", 1 }, { "layer_height", 0.20 } }, + { { "extruder", 2 }, { "layer_height", 0.15 } } + }); + + int t1_s0_cooldown_count = 0; + int t0_s0_cooldown_count = 0; + + std::istringstream stream(gcode); + for (std::string line; std::getline(stream, line);) { + if (line.find(";cooldown") != std::string::npos) { + if (line.find("M104 S0 T1") != std::string::npos) + ++t1_s0_cooldown_count; + if (line.find("M104 S0 T0") != std::string::npos) + ++t0_s0_cooldown_count; + } + } + + // T1 (extruder 2, 4mm cube) gets exactly one S0 after its last layer. + // T0 (extruder 1, 10mm cube) never gets S0 mid-print. + CHECK(t1_s0_cooldown_count == 1); + CHECK(t0_s0_cooldown_count == 0); +} + +TEST_CASE("Unused extruder turns off on non-IMEX three-tool print", "[MultiFilament]") +{ + const std::string gcode = slice_with_object_overrides( + { make_cube(20., 20., 4.), make_cube(20., 20., 10.), make_cube(20., 20., 10.) }, + multifilament_config(3, { + { "single_extruder_multi_material", 0 }, + { "ooze_prevention", 1 }, + { "standby_temperature_delta", -40 }, + { "nozzle_temperature", "240,240,240" }, + }), + { + { { "extruder", 1 } }, + { { "extruder", 2 } }, + { { "extruder", 3 } } + }); + + bool premature_s0_t1 = false; + bool premature_s0_t2 = false; + double current_z = 0.0; + + GCodeReader reader; + reader.parse_buffer(gcode, [&](GCodeReader& /*self*/, const GCodeReader::GCodeLine& line) { + if (line.has_z()) + current_z = line.z(); + + const std::string raw = line.raw(); + if (raw.find(";cooldown") != std::string::npos && current_z < 9.0) { + if (raw.find("M104 S0 T1") != std::string::npos) + premature_s0_t1 = true; + if (raw.find("M104 S0 T2") != std::string::npos) + premature_s0_t2 = true; + } + }); + + // T1 and T2 print up to 10mm — neither should get S0 before that. + CHECK_FALSE(premature_s0_t1); + CHECK_FALSE(premature_s0_t2); +} \ No newline at end of file From 8b6c074c69ca81071e06bcd4105fa325d54e6ad8 Mon Sep 17 00:00:00 2001 From: Lam Wei Lun Date: Fri, 16 Oct 2026 15:37:17 +0800 Subject: [PATCH 4/5] Fix publish dialog tab clipping and extruder switch distortion (#16543) * Fix TabCtrl clipping titles and indicator dots Button measures a label plus icon without the icon gap that render draws, so icon tabs were ellipsized and the indicator dot was cut off. Widen the horizontal padding so the measured width covers the gap. * Re-layout publish dialog when extruder switch labels change Setting the indicator bullet on a switch option changed its width without laying out the page, leaving the switch distorted until the next layout. --- src/slic3r/GUI/PublishSettingsDialog.cpp | 4 +++- src/slic3r/GUI/Widgets/TabCtrl.cpp | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/PublishSettingsDialog.cpp b/src/slic3r/GUI/PublishSettingsDialog.cpp index 080182a1b6..4020eedee4 100644 --- a/src/slic3r/GUI/PublishSettingsDialog.cpp +++ b/src/slic3r/GUI/PublishSettingsDialog.cpp @@ -2225,8 +2225,10 @@ void PublishSettingsDialog::refresh_tab_indicators() if (section.variant_switch != nullptr) { const wxString& title = m_categories[section.categories[i]].title; const wxString text = on ? title + wxString(" ") + wxString(wxUniChar(0x2022)) : title; - if (section.variant_switch->GetOptionText(static_cast(i)) != text) + if (section.variant_switch->GetOptionText(static_cast(i)) != text) { section.variant_switch->SetOptionText(static_cast(i), text); + section.page->Layout(); + } } any = any || on; } diff --git a/src/slic3r/GUI/Widgets/TabCtrl.cpp b/src/slic3r/GUI/Widgets/TabCtrl.cpp index 6b970929e5..2135a695e1 100644 --- a/src/slic3r/GUI/Widgets/TabCtrl.cpp +++ b/src/slic3r/GUI/Widgets/TabCtrl.cpp @@ -32,7 +32,7 @@ END_EVENT_TABLE() */ #define TAB_BUTTON_SPACE 2 -#define TAB_BUTTON_PADDING_X 2 +#define TAB_BUTTON_PADDING_X 3 #define TAB_BUTTON_PADDING_Y 2 #define TAB_BUTTON_PADDING TAB_BUTTON_PADDING_X, TAB_BUTTON_PADDING_Y From 29b6bbb8c9f4c45c893ee7bb3e37ff2754729f68 Mon Sep 17 00:00:00 2001 From: HanifKoh <76276251+HanifKoh@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:39:02 +0800 Subject: [PATCH 5/5] Keep Slice and Print Visible When Plugin Page Tabs Overflow (#16541) --- src/slic3r/GUI/MainFrame.cpp | 3 +++ src/slic3r/GUI/Notebook.hpp | 1 + src/slic3r/plugin/host/PluginPages.cpp | 7 +++++++ 3 files changed, 11 insertions(+) diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index a36f322066..66e982ad9f 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1739,6 +1739,9 @@ void MainFrame::fit_tab_labels() for (size_t i = count - 1; i >= 1; --i) { int right = ScreenToClient(m_slice_option_btn->ClientToScreen({})).x; int left = sizer->GetSize().GetWidth(); + // The overflow button sits between the tab buttons and the side tools, so it is not free space. + if (const wxWindow* overflow = ctrl->GetOverflowButton()) + left += overflow->GetSize().GetWidth(); if (right - left - FromDIP(15) > 0) return; ctrl->SetCompact(i, true); m_tabpanel->Refresh(); diff --git a/src/slic3r/GUI/Notebook.hpp b/src/slic3r/GUI/Notebook.hpp index e85f3ad723..b7774c84e0 100644 --- a/src/slic3r/GUI/Notebook.hpp +++ b/src/slic3r/GUI/Notebook.hpp @@ -57,6 +57,7 @@ public: // ORCA: a companion widget shown right after the tab buttons (before any side_tools), e.g. // an overflow indicator. Pass nullptr to remove it; ownership stays with the caller. void SetOverflowButton(wxWindow* button); + wxWindow* GetOverflowButton() const { return m_overflow_button; } private: wxFlexGridSizer* m_buttons_sizer; diff --git a/src/slic3r/plugin/host/PluginPages.cpp b/src/slic3r/plugin/host/PluginPages.cpp index cf4ed6a6a8..93bc8e93cd 100644 --- a/src/slic3r/plugin/host/PluginPages.cpp +++ b/src/slic3r/plugin/host/PluginPages.cpp @@ -4,6 +4,7 @@ #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/Notebook.hpp" #include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/MainFrame.hpp" #include "slic3r/GUI/Widgets/Button.hpp" #include "slic3r/GUI/wxExtensions.hpp" #include "slic3r/plugin/PluginManager.hpp" @@ -382,6 +383,7 @@ void PluginPages::relayout() m_parent->SelectPageByName(id_to_reselect); } + const bool overflow_changed = need_overflow != (m_overflow_button != nullptr); if (need_overflow) { if (m_overflow_button == nullptr) { auto* btn = new Button(m_parent->GetBtnsListCtrl(), wxString(L"\u25BE"), wxString(), wxNO_BORDER); @@ -398,6 +400,11 @@ void PluginPages::relayout() m_overflow_button->Destroy(); m_overflow_button = nullptr; } + + // The tab strip changed width; compact the labels again so the side tools stay visible. + if ((!up_to_date || overflow_changed) && !GUI::wxGetApp().is_closing()) + if (GUI::MainFrame* mainframe = GUI::wxGetApp().mainframe) + mainframe->fit_tab_labels(); } void PluginPages::select_page(const PluginCapabilityId& id)