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