Merge branch 'main' into fix/texture-picker-lazy-png-header

This commit is contained in:
Lam Wei Lun
2026-10-16 15:41:04 +08:00
18 changed files with 417 additions and 133 deletions
+2 -17
View File
@@ -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}
)
@@ -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
+27 -18
View File
@@ -432,27 +432,36 @@ static std::vector<Vec2d> 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;
}
+1
View File
@@ -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;
+14 -1
View File
@@ -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
+3
View File
@@ -370,6 +370,7 @@ public:
std::vector<int> & 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<coordf_t> &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<unsigned int, std::map<LayerTools::MixedSubLayerGroup::VolumeKey, std::vector<size_t>>> m_gradient_volume_layers;
// Map: extruder_id -> print_z (z coordinate) of the last layer it is used on
std::map<unsigned int, coordf_t> m_last_layer_per_extruder;
const PrintObject* m_print_object_ptr = nullptr;
Print* m_print;
bool m_sorted = false;
+1
View File
@@ -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
+15 -91
View File
@@ -65,6 +65,7 @@
#include <string_view>
#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<std::pair<Polygon, float>> height_polygons;
polygons.reserve(instances_count);
height_polygons.reserve(instances_count);
std::vector<struct height_info> convex_and_bounding_boxes;
struct height_info
{
double instance_height;
BoundingBox bounding_box;
Polygon hull_polygon;
};
std::vector<SequentialClearanceInstance> convex_and_bounding_boxes;
convex_and_bounding_boxes.reserve(instances_count);
std::map<ObjectID, int> 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);
+3
View File
@@ -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();
+1
View File
@@ -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;
+3 -1
View File
@@ -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<unsigned int>(i)) != text)
if (section.variant_switch->GetOptionText(static_cast<unsigned int>(i)) != text) {
section.variant_switch->SetOptionText(static_cast<unsigned int>(i), text);
section.page->Layout();
}
}
any = any || on;
}
@@ -0,0 +1,63 @@
#pragma once
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/ObjectID.hpp"
#include "libslic3r/Polygon.hpp"
#include <algorithm>
#include <cstddef>
#include <map>
#include <utility>
#include <vector>
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<SequentialClearanceInstance>::iterator first, std::vector<SequentialClearanceInstance>::iterator last,
const std::map<ObjectID, int>& 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<std::pair<Polygon, float>> sequential_clearance_height_polygons(
const std::vector<SequentialClearanceInstance>& instances, double printable_height, double height_to_lid, double height_to_rod)
{
std::vector<std::pair<Polygon, float>> 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
+1 -1
View File
@@ -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
+7
View File
@@ -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)
@@ -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
+109
View File
@@ -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);
}
+1
View File
@@ -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
)
@@ -0,0 +1,162 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "catch2/matchers/catch_matchers.hpp"
#include "fff_print/test_helpers.hpp"
#include "slic3r/GUI/SequentialPrintClearance.hpp"
#include <libslic3r/libslic3r.h>
#include <libslic3r/Model.hpp>
#include <libslic3r/TriangleMesh.hpp>
#include <libslic3r/PrintConfig.hpp>
#include <libslic3r/Print.hpp>
#include <libslic3r/Polygon.hpp>
#include <libslic3r/ObjectID.hpp>
#include <cstddef>
#include <map>
#include <vector>
#include <utility>
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<SequentialClearanceInstance> 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<SequentialClearanceInstance> 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<SequentialClearanceInstance> 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<SequentialClearanceInstance> 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<std::pair<Polygon, float>> validation_warnings;
Print::sequential_print_clearance_valid(print, &polygons, &validation_warnings);
std::map<ObjectID, int> 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<SequentialClearanceInstance> 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<SequentialClearanceInstance> instances{instance(20, 0, 50.), instance(0, 0, 50.)};
instances[0].instance_id = ObjectID(1);
instances[1].instance_id = ObjectID(2);
std::map<ObjectID, int> 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<SequentialClearanceInstance> 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<ObjectID, int> 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));
}