From 153d35ded0ebc43ceb5544bc2dd70753df9015ee Mon Sep 17 00:00:00 2001 From: Rad Date: Tue, 10 Mar 2026 19:33:36 +0100 Subject: [PATCH] Add Local-Z wipe tower reserve planning and pass ordering Add startup profiling for GUI initialization Drop unusable Bonjour sockets and harden socket cleanup Add /FS to MSVC parallel compile options --- CMakeLists.txt | 2 +- src/libslic3r/GCode.cpp | 442 ++++++++++++++---- src/libslic3r/GCode.hpp | 15 +- src/libslic3r/GCode/ToolOrdering.cpp | 3 +- src/libslic3r/GCode/WipeTower2.cpp | 174 ++++++- src/libslic3r/GCode/WipeTower2.hpp | 9 + src/libslic3r/LocalZOrderOptimizer.hpp | 72 +++ src/libslic3r/Preset.cpp | 2 +- src/libslic3r/PresetBundle.cpp | 105 ++++- src/libslic3r/Print.cpp | 56 +++ src/libslic3r/Print.hpp | 2 + src/libslic3r/PrintConfig.cpp | 10 + src/libslic3r/PrintConfig.hpp | 1 + src/slic3r/GUI/ConfigManipulation.cpp | 5 + src/slic3r/GUI/GUI_App.cpp | 109 ++++- src/slic3r/GUI/Plater.cpp | 2 +- src/slic3r/GUI/Tab.cpp | 1 + src/slic3r/Utils/Bonjour.cpp | 52 +-- src/slic3r/Utils/Bonjour.hpp | 1 + tests/libslic3r/CMakeLists.txt | 1 + .../test_local_z_order_optimizer.cpp | 29 ++ 21 files changed, 961 insertions(+), 132 deletions(-) create mode 100644 src/libslic3r/LocalZOrderOptimizer.hpp create mode 100644 tests/libslic3r/test_local_z_order_optimizer.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 08855a48a1..eda197229f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -191,7 +191,7 @@ endif () if (MSVC) if (SLIC3R_MSVC_COMPILE_PARALLEL AND NOT IS_CLANG_CL) - add_compile_options(/MP) + add_compile_options(/MP /FS) endif () # /bigobj (Increase Number of Sections in .Obj file) # error C3859: virtual memory range for PCH exceeded; please recompile with a command line option of '-Zm90' or greater diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 1d9aff376f..95d3172eca 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -5,6 +5,7 @@ #include "libslic3r.h" #include "I18N.hpp" #include "GCode.hpp" +#include "LocalZOrderOptimizer.hpp" #include "Exception.hpp" #include "ExtrusionEntity.hpp" #include "EdgeGrid.hpp" @@ -12,6 +13,7 @@ #include "GCode/PrintExtents.hpp" #include "GCode/Thumbnails.hpp" #include "GCode/WipeTower.hpp" +#include "GCode/WipeTower2.hpp" #include "ShortestPath.hpp" #include "Print.hpp" #include "Utils.hpp" @@ -428,7 +430,9 @@ static inline Point wipe_tower_point_to_object_point(GCode& gcodegen, const Vec2 std::string WipeTowerIntegration::append_tcr(GCode& gcodegen, const WipeTower::ToolChangeResult& tcr, int new_extruder_id, double z) const { if (new_extruder_id != -1 && new_extruder_id != tcr.new_tool) - throw Slic3r::InvalidArgument("Error: WipeTowerIntegration::append_tcr was asked to do a toolchange it didn't expect."); + throw Slic3r::InvalidArgument(Slic3r::format( + "Error: WipeTowerIntegration::append_tcr unexpected toolchange: layer_idx=%1% tool_change_idx=%2% requested=%3% expected=%4% initial=%5%", + m_layer_idx, m_tool_change_idx, new_extruder_id, tcr.new_tool, tcr.initial_tool)); std::string gcode; @@ -688,7 +692,9 @@ std::string WipeTowerIntegration::append_tcr(GCode& gcodegen, const WipeTower::T std::string WipeTowerIntegration::append_tcr2(GCode& gcodegen, const WipeTower::ToolChangeResult& tcr, int new_extruder_id, double z) const { if (new_extruder_id != -1 && new_extruder_id != tcr.new_tool) - throw Slic3r::InvalidArgument("Error: WipeTowerIntegration::append_tcr was asked to do a toolchange it didn't expect."); + throw Slic3r::InvalidArgument(Slic3r::format( + "Error: WipeTowerIntegration::append_tcr unexpected toolchange: layer_idx=%1% tool_change_idx=%2% requested=%3% expected=%4% initial=%5%", + m_layer_idx, m_tool_change_idx, new_extruder_id, tcr.new_tool, tcr.initial_tool)); std::string gcode; @@ -922,25 +928,204 @@ std::string WipeTowerIntegration::prime(GCode& gcodegen) return gcode; } -std::string WipeTowerIntegration::tool_change(GCode& gcodegen, int extruder_id, bool finish_layer, bool local_z_unplanned) +std::string WipeTowerIntegration::tool_change(GCode& gcodegen, int extruder_id, bool finish_layer, bool local_z_unplanned, + double local_z_nominal_layer_z) { std::string gcode; auto emit_local_z_unplanned_toolchange = [&]() -> std::string { if (extruder_id < 0 || !gcodegen.writer().need_toolchange(extruder_id)) return ""; + if (m_layer_idx < 0 || size_t(m_layer_idx) >= m_local_z_reserve_boxes.size() || + size_t(m_layer_idx) >= m_local_z_reserve_slot_idx.size()) { + BOOST_LOG_TRIVIAL(debug) << "Local-Z unplanned toolchange using direct extruder switch" + << " layer_idx=" << m_layer_idx + << " extruder_id=" << extruder_id + << " tool_change_idx=" << m_tool_change_idx; + return gcodegen.set_extruder(unsigned(extruder_id), + gcodegen.writer().get_position().z() - gcodegen.config().z_offset.value); + } - // Local-Z phase-b may introduce extra intra-layer toolchanges that were not part - // of the preplanned wipe tower sequence. Replaying a full wipe-tower template for - // each of those extra switches overprints the same tower layer at micro-step Zs - // and turns the tower into mush. Keep these extra switches local-Z-only by doing - // a direct toolchange here and leave the normal wipe tower plan untouched. - BOOST_LOG_TRIVIAL(debug) << "Local-Z unplanned toolchange using direct extruder switch" + size_t &slot_idx = m_local_z_reserve_slot_idx[size_t(m_layer_idx)]; + const auto &layer_slots = m_local_z_reserve_boxes[size_t(m_layer_idx)]; + if (slot_idx >= layer_slots.size() || layer_slots.empty()) { + BOOST_LOG_TRIVIAL(warning) << "Local-Z toolchange reserve exhausted" + << " layer_idx=" << m_layer_idx + << " extruder_id=" << extruder_id + << " reserved_slots=" << layer_slots.size() + << " consumed_slots=" << slot_idx; + return gcodegen.set_extruder(unsigned(extruder_id), + gcodegen.writer().get_position().z() - gcodegen.config().z_offset.value); + } + + const WipeTower::box_coordinates &slot = layer_slots[slot_idx++]; + const double current_z = gcodegen.writer().get_position().z(); + const double tower_z = local_z_nominal_layer_z >= 0. ? local_z_nominal_layer_z : current_z; + const double toolchange_print_z = tower_z - gcodegen.config().z_offset.value; + + if (m_layer_idx >= (int)m_tool_changes.size() || m_tool_changes[m_layer_idx].empty()) { + BOOST_LOG_TRIVIAL(debug) << "Local-Z reserve has no matching wipe-tower layer metadata, using direct extruder switch" + << " layer_idx=" << m_layer_idx + << " extruder_id=" << extruder_id; + return gcodegen.set_extruder(unsigned(extruder_id), toolchange_print_z); + } + + const float layer_height = m_tool_changes[m_layer_idx].front().layer_height; + if (gcodegen.m_curr_print != nullptr && gcodegen.writer().extruder() != nullptr) { + const Print& print = *gcodegen.m_curr_print; + const PrintConfig& print_config = print.config(); + const size_t current_tool = gcodegen.writer().extruder()->id(); + std::vector> wipe_volumes = WipeTower2::extract_wipe_volumes(print_config); + + WipeTower2 local_z_wipe_tower(print_config, + print.default_region_config(), + print.get_plate_index(), + print.get_plate_origin(), + wipe_volumes, + current_tool); + for (size_t extruder_idx = 0; extruder_idx < print_config.nozzle_diameter.size(); ++extruder_idx) + local_z_wipe_tower.set_extruder(extruder_idx, print_config); + + local_z_wipe_tower.set_current_tool(current_tool); + local_z_wipe_tower.set_layer(float(toolchange_print_z), layer_height, 0, m_layer_idx == 0, false); + + WipeTower::ToolChangeResult local_z_tcr = + local_z_wipe_tower.local_z_tool_change(size_t(extruder_id), slot, float(print_config.prime_volume)); + BOOST_LOG_TRIVIAL(debug) << "Local-Z toolchange emitted via wipe tower mini-toolchange" + << " layer_idx=" << m_layer_idx + << " extruder_id=" << extruder_id + << " reserve_slot=" << (slot_idx - 1); + return gcodegen.is_BBL_Printer() ? append_tcr(gcodegen, local_z_tcr, extruder_id, tower_z) : + append_tcr2(gcodegen, local_z_tcr, extruder_id, tower_z); + } + + const float nozzle_diameter = float(gcodegen.config().nozzle_diameter.get_at(size_t(extruder_id))); + const float line_width = nozzle_diameter * 1.25f; + const float extra_flow = float(gcodegen.config().wipe_tower_extra_flow.value) / 100.f; + const float extra_spacing = float(gcodegen.config().wipe_tower_extra_spacing.value) / 100.f; + const float filament_area = float((M_PI / 4.f) * std::pow(gcodegen.config().filament_diameter.get_at(size_t(extruder_id)), 2)); + const float flow_per_mm = + filament_area > 0.f ? + (layer_height * (line_width - layer_height * float(1.f - M_PI / 4.f)) / filament_area) * std::max(0.f, extra_flow) : + 0.f; + if (line_width <= 0.f || flow_per_mm <= 0.f) + return gcodegen.set_extruder(unsigned(extruder_id), toolchange_print_z); + + const float inset_x = std::min(line_width, std::max(0.f, (slot.ru.x() - slot.ld.x()) * 0.25f)); + const float inset_y = std::min(line_width * 0.5f, std::max(0.f, (slot.ru.y() - slot.rd.y()) * 0.25f)); + const float x_min = slot.ld.x() + inset_x; + const float x_max = slot.rd.x() - inset_x; + const float y_min = slot.ld.y() + inset_y; + const float y_max = slot.lu.y() - inset_y; + if (x_max - x_min <= float(EPSILON) || y_max - y_min <= float(EPSILON)) + return gcodegen.set_extruder(unsigned(extruder_id), toolchange_print_z); + + std::vector local_path; + local_path.reserve(16); + local_path.emplace_back(x_min, y_min); + local_path.emplace_back(x_max, y_min); + + bool moving_left = true; + float y = y_min; + const float line_spacing = std::max(line_width, line_width * std::max(1.f, extra_spacing)); + while (y + line_spacing < y_max - float(EPSILON)) { + y = std::min(y + line_spacing, y_max); + local_path.emplace_back(moving_left ? x_max : x_min, y); + local_path.emplace_back(moving_left ? x_min : x_max, y); + moving_left = !moving_left; + } + + const float alpha = m_wipe_tower_rotation / 180.f * float(M_PI); + auto transform_wt_pt = [&alpha, this](const Vec2f& pt) -> Vec2f { + return Eigen::Rotation2Df(alpha) * pt + m_wipe_tower_pos; + }; + const Vec2f plate_origin_2d(m_plate_origin(0), m_plate_origin(1)); + const Vec2f start_pos = transform_wt_pt(local_path.front()); + const Vec2f start_machine = start_pos + plate_origin_2d; + + gcodegen.m_next_wipe_x = start_pos.x(); + gcodegen.m_next_wipe_y = start_pos.y(); + + gcode += gcodegen.writer().unlift(); + + if (gcodegen.writer().extruder() != nullptr) { + auto type = ZHopType(gcodegen.m_config.z_hop_types.get_at(gcodegen.m_writer.extruder()->id())); + if (type == ZHopType::zhtAuto) + type = ZHopType::zhtSpiral; + if (gcodegen.m_config.z_hop_when_prime.get_at(gcodegen.m_writer.extruder()->id())) + gcode += gcodegen.retract(false, false, gcodegen.to_lift_type(type)); + } + + gcodegen.m_avoid_crossing_perimeters.use_external_mp_once(); + gcode += gcodegen.travel_to(wipe_tower_point_to_object_point(gcodegen, start_machine), erMixed, + "Travel to Local-Z wipe tower reserve"); + gcode += gcodegen.unretract(); + + if (!is_approx(tower_z, current_z)) { + gcode += gcodegen.writer().retract(); + gcode += gcodegen.writer().travel_to_z(tower_z, "Travel to Local-Z tower layer"); + gcode += gcodegen.writer().unretract(); + } + + gcode += gcodegen.set_extruder(unsigned(extruder_id), toolchange_print_z); + gcode += gcodegen.writer().travel_to_z(tower_z, "Force restore Local-Z tower Z", true); + { + Vec3d restored_position{gcodegen.writer().get_position()}; + restored_position.z() = tower_z; + gcodegen.writer().set_position(restored_position); + } + gcode += gcodegen.unretract(); + gcode += gcodegen.writer().travel_to_xy(start_machine.cast(), "Return to Local-Z wipe tower reserve"); + + gcode += ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height) + float_to_string_decimal_point(layer_height, 3) + "\n"; + gcode += ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Role) + ExtrusionEntity::role_to_string(erWipeTower) + "\n"; + gcode += ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Width) + float_to_string_decimal_point(line_width, 3) + "\n"; + + double feedrate = std::max(1.0, double(gcodegen.config().wipe_tower_max_purge_speed.value)) * 60.0; + if (m_layer_idx == 0) + feedrate = std::min(feedrate, std::max(1.0, double(gcodegen.config().initial_layer_speed.value)) * 60.0); + gcode += gcodegen.writer().set_speed(feedrate, "Local-Z wipe tower reserve"); + + for (size_t point_idx = 1; point_idx < local_path.size(); ++point_idx) { + const Vec2f machine_pt = transform_wt_pt(local_path[point_idx]) + plate_origin_2d; + const double length = (local_path[point_idx] - local_path[point_idx - 1]).norm(); + if (length <= EPSILON) + continue; + gcode += gcodegen.writer().extrude_to_xy(machine_pt.cast(), flow_per_mm * float(length), + point_idx == 1 ? "Local-Z tower purge" : std::string()); + } + + const Vec2f end_machine = transform_wt_pt(local_path.back()) + plate_origin_2d; + gcodegen.set_last_pos(wipe_tower_point_to_object_point(gcodegen, end_machine)); + gcodegen.m_wipe.reset_path(); + for (size_t point_idx = local_path.size(); point_idx-- > 0;) { + const Vec2f machine_pt = transform_wt_pt(local_path[point_idx]) + plate_origin_2d; + gcodegen.m_wipe.path.points.emplace_back(wipe_tower_point_to_object_point(gcodegen, machine_pt)); + } + + if (!is_approx(tower_z, current_z)) { + const Extruder *active_extruder = gcodegen.writer().extruder(); + const bool can_wipe = active_extruder != nullptr && + gcodegen.config().wipe.get_at(active_extruder->id()) && + gcodegen.m_wipe.has_path() && + scale_(gcodegen.config().wipe_distance.get_at(active_extruder->id())) > SCALED_EPSILON; + if (can_wipe) { + Wipe::RetractionValues wipe_retractions = gcodegen.m_wipe.calculateWipeRetractionLengths(gcodegen, false); + gcode += gcodegen.writer().retract(true, wipe_retractions.retractLengthBeforeWipe); + gcode += gcodegen.m_wipe.wipe(gcodegen, wipe_retractions.retractLengthDuringWipe, false, false); + } + gcode += gcodegen.writer().retract(); + gcodegen.m_wipe.reset_path(); + gcode += gcodegen.writer().travel_to_z(current_z, "Travel back to Local-Z pass"); + gcode += gcodegen.writer().unretract(); + } + + gcodegen.m_avoid_crossing_perimeters.use_external_mp_once(); + BOOST_LOG_TRIVIAL(debug) << "Local-Z toolchange emitted on wipe tower reserve" << " layer_idx=" << m_layer_idx << " extruder_id=" << extruder_id - << " tool_change_idx=" << m_tool_change_idx; - return gcodegen.set_extruder(unsigned(extruder_id), - gcodegen.writer().get_position().z() - gcodegen.config().z_offset.value); + << " reserve_slot=" << (slot_idx - 1); + return gcode; }; if (local_z_unplanned) @@ -2633,6 +2818,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream& file, ThumbnailsGenerato if (has_wipe_tower && !layers_to_print.empty()) { m_wipe_tower.reset(new WipeTowerIntegration(print.config(), print.get_plate_index(), print.get_plate_origin(), *print.wipe_tower_data().priming.get(), print.wipe_tower_data().tool_changes, + print.wipe_tower_data().local_z_reserve_boxes, *print.wipe_tower_data().final_purge.get())); // BBS file.write(m_writer.travel_to_z(initial_layer_print_height + m_config.z_offset.value, "Move to the first layer height")); @@ -5108,6 +5294,31 @@ LayerResult GCode::process_layer(const Print& print, LocalZPassBucket* bucket { nullptr }; }; + auto same_local_z_pass_group = [](const LocalZPassRef& lhs, const LocalZPassRef& rhs) { + assert(lhs.bucket != nullptr && rhs.bucket != nullptr); + assert(lhs.bucket->plan != nullptr && rhs.bucket->plan != nullptr); + return lhs.layer_to_print_idx == rhs.layer_to_print_idx && + std::abs(lhs.bucket->plan->print_z - rhs.bucket->plan->print_z) <= EPSILON; + }; + + auto local_z_bucket_extruders = [](const LocalZPassBucket& bucket) { + std::vector extruders; + extruders.reserve(bucket.by_extruder.size()); + for (const auto& by_extruder_entry : bucket.by_extruder) { + if (!by_extruder_entry.second.empty()) + extruders.push_back(by_extruder_entry.first); + } + return extruders; + }; + + auto shared_local_z_extruder = [](const std::vector& lhs, const std::vector& rhs) -> int { + for (unsigned int extruder_id : lhs) { + if (std::find(rhs.begin(), rhs.end(), extruder_id) != rhs.end()) + return static_cast(extruder_id); + } + return -1; + }; + std::vector local_z_pass_refs; if (local_z_perimeter_phase_b_enabled) { for (size_t layer_to_print_idx = 0; layer_to_print_idx < local_z_layer_contexts.size(); ++layer_to_print_idx) { @@ -5140,101 +5351,149 @@ LayerResult GCode::process_layer(const Print& print, } } + std::vector layer_extruders = layer_tools.extruders; + for (const auto& by_extruder_entry : by_extruder) { + if (std::find(layer_extruders.begin(), layer_extruders.end(), by_extruder_entry.first) == layer_extruders.end()) + layer_extruders.emplace_back(by_extruder_entry.first); + } + if (!local_z_pass_refs.empty()) { const int local_z_phase_b_start_extruder = (has_wipe_tower && m_writer.extruder() != nullptr) ? int(m_writer.extruder()->id()) : -1; + int local_z_phase_b_active_extruder = (m_writer.extruder() != nullptr) ? int(m_writer.extruder()->id()) : -1; bool local_z_phase_b_changed_extruder = false; BOOST_LOG_TRIVIAL(info) << "Local-Z phase-b emitting" << " print_z=" << print_z << " perimeter_passes=" << local_z_pass_refs.size(); gcode += "; local-z phase-b perimeter passes begin\n"; - for (const LocalZPassRef& pass_ref : local_z_pass_refs) { - assert(pass_ref.bucket != nullptr && pass_ref.bucket->plan != nullptr); - const SubLayerPlan& pass_plan = *pass_ref.bucket->plan; - const double pass_z = pass_plan.print_z + m_config.z_offset.value; - const double saved_nominal_z = m_nominal_z; - const float saved_last_layer_z = m_last_layer_z; - // Ensure all travel/lift logic inside this pass references the micro-pass Z, - // not the base layer nominal Z. - m_nominal_z = pass_z; - m_last_layer_z = float(pass_z); - BOOST_LOG_TRIVIAL(debug) << "Local-Z pass emit" - << " print_z=" << print_z - << " layer_to_print_idx=" << pass_ref.layer_to_print_idx - << " layer_id=" << pass_plan.layer_id - << " pass_index=" << pass_plan.pass_index - << " pass_print_z=" << pass_plan.print_z - << " pass_flow_height=" << pass_plan.flow_height - << " extruder_buckets=" << pass_ref.bucket->by_extruder.size(); - if (std::abs(m_writer.get_position().z() - pass_z) > EPSILON) { - gcode += this->retract(false, false, LiftType::NormalLift); - gcode += m_writer.travel_to_z(pass_z, "Local-Z perimeter pass"); + size_t pass_ref_idx = 0; + while (pass_ref_idx < local_z_pass_refs.size()) { + size_t pass_group_end = pass_ref_idx + 1; + while (pass_group_end < local_z_pass_refs.size() && + same_local_z_pass_group(local_z_pass_refs[pass_ref_idx], local_z_pass_refs[pass_group_end])) { + ++pass_group_end; } - for (auto& by_extruder_entry : pass_ref.bucket->by_extruder) { - const unsigned int local_extruder_id = by_extruder_entry.first; - std::vector& objects_by_extruder = by_extruder_entry.second; - if (objects_by_extruder.empty()) - continue; + std::vector> pass_group_extruders; + pass_group_extruders.reserve(pass_group_end - pass_ref_idx); + for (size_t group_idx = pass_ref_idx; group_idx < pass_group_end; ++group_idx) + pass_group_extruders.push_back(local_z_bucket_extruders(*local_z_pass_refs[group_idx].bucket)); - if (has_wipe_tower && m_writer.need_toolchange(local_extruder_id)) - local_z_phase_b_changed_extruder = true; - if (has_wipe_tower && m_wipe_tower) { - gcode += m_wipe_tower->tool_change(*this, int(local_extruder_id), false, true); - // Local-Z phase-b uses the wipe tower outside the normal per-layer - // extruder loop, so mirror the usual toolchange bookkeeping here. - // This forces the next object path to refresh WIDTH/HEIGHT tags - // after prime tower G-code, keeping the preview in sync with the - // actual local-Z pass height. - m_last_processor_extrusion_role = erWipeTower; - } else { - gcode += this->set_extruder(local_extruder_id, pass_plan.print_z); - } + // Buckets that land on the same Local-Z plane are independent, so prefer + // whichever one lets us keep the currently active extruder. + const std::vector ordered_pass_group = + LocalZOrderOptimizer::order_pass_group(pass_group_extruders, local_z_phase_b_active_extruder); + + for (size_t ordered_group_idx = 0; ordered_group_idx < ordered_pass_group.size(); ++ordered_group_idx) { + const size_t group_local_idx = ordered_pass_group[ordered_group_idx]; + const LocalZPassRef& pass_ref = local_z_pass_refs[pass_ref_idx + group_local_idx]; + assert(pass_ref.bucket != nullptr && pass_ref.bucket->plan != nullptr); + const SubLayerPlan& pass_plan = *pass_ref.bucket->plan; + const double pass_z = pass_plan.print_z + m_config.z_offset.value; + const double saved_nominal_z = m_nominal_z; + const float saved_last_layer_z = m_last_layer_z; + // Ensure all travel/lift logic inside this pass references the micro-pass Z, + // not the base layer nominal Z. + m_nominal_z = pass_z; + m_last_layer_z = float(pass_z); + BOOST_LOG_TRIVIAL(debug) << "Local-Z pass emit" + << " print_z=" << print_z + << " layer_to_print_idx=" << pass_ref.layer_to_print_idx + << " layer_id=" << pass_plan.layer_id + << " pass_index=" << pass_plan.pass_index + << " pass_print_z=" << pass_plan.print_z + << " pass_flow_height=" << pass_plan.flow_height + << " extruder_buckets=" << pass_ref.bucket->by_extruder.size(); if (std::abs(m_writer.get_position().z() - pass_z) > EPSILON) { - BOOST_LOG_TRIVIAL(warning) << "Local-Z pass z restore" - << " print_z=" << print_z - << " layer_id=" << pass_plan.layer_id - << " pass_index=" << pass_plan.pass_index - << " extruder=" << local_extruder_id - << " expected_pass_z=" << pass_z - << " observed_z_after_toolchange=" << m_writer.get_position().z(); - gcode += m_writer.travel_to_z(pass_z, "Local-Z pass z restore"); + gcode += this->retract(false, false, LiftType::NormalLift); + gcode += m_writer.travel_to_z(pass_z, "Local-Z perimeter pass"); } - std::vector instances_to_print = - sort_print_object_instances(objects_by_extruder, layers, ordering, single_object_instance_idx); - for (InstanceToPrint& instance_to_print : instances_to_print) { - const LayerToPrint& layer_to_print = layers[instance_to_print.layer_id]; - const bool object_layer_over_raft = - layer_to_print.object_layer && layer_to_print.object_layer->id() > 0 && - instance_to_print.print_object.slicing_parameters().raft_layers() == layer_to_print.object_layer->id(); + const std::vector& group_bucket_extruders = pass_group_extruders[group_local_idx]; + int preferred_last_extruder = -1; + if (ordered_group_idx + 1 < ordered_pass_group.size()) { + preferred_last_extruder = + shared_local_z_extruder(group_bucket_extruders, + pass_group_extruders[ordered_pass_group[ordered_group_idx + 1]]); + } + const std::vector ordered_bucket_extruders = + LocalZOrderOptimizer::order_bucket_extruders(group_bucket_extruders, + local_z_phase_b_active_extruder, + preferred_last_extruder); - m_config.apply(instance_to_print.print_object.config(), true); - m_layer = layer_to_print.layer(); - m_object_layer_over_raft = object_layer_over_raft; - const bool saved_reduce_crossing_wall = m_config.reduce_crossing_wall.value; - // Local-Z phase-b emits many short micro-passes. Avoid-crossing - // travel planning is expensive and fragile on these fragments, so - // keep travel simple here. - m_config.reduce_crossing_wall.value = false; - m_avoid_crossing_perimeters.disable_once(); + for (unsigned int local_extruder_id : ordered_bucket_extruders) { + auto by_extruder_it = pass_ref.bucket->by_extruder.find(local_extruder_id); + if (by_extruder_it == pass_ref.bucket->by_extruder.end()) + continue; - const Point& offset = instance_to_print.print_object.instances()[instance_to_print.instance_id].shift; - std::pair this_object_copy(&instance_to_print.print_object, offset); - if (m_last_obj_copy != this_object_copy) - m_avoid_crossing_perimeters.use_external_mp_once(); - m_last_obj_copy = this_object_copy; - this->set_origin(unscale(offset)); + std::vector& objects_by_extruder = by_extruder_it->second; + if (objects_by_extruder.empty()) + continue; - for (ObjectByExtruder::Island& island : instance_to_print.object_by_extruder.islands) { - gcode += this->extrude_perimeters(print, island.by_region, first_layer, false); - gcode += this->extrude_perimeters(print, island.by_region, first_layer, true); + if (has_wipe_tower && m_writer.need_toolchange(local_extruder_id)) + local_z_phase_b_changed_extruder = true; + if (has_wipe_tower && m_wipe_tower) { + gcode += m_wipe_tower->tool_change(*this, int(local_extruder_id), false, true, print_z + m_config.z_offset.value); + // Local-Z phase-b uses the wipe tower outside the normal per-layer + // extruder loop, so mirror the usual toolchange bookkeeping here. + // This forces the next object path to refresh WIDTH/HEIGHT tags + // after prime tower G-code, keeping the preview in sync with the + // actual local-Z pass height. + m_last_processor_extrusion_role = erWipeTower; + } else { + gcode += this->set_extruder(local_extruder_id, pass_plan.print_z); } - m_config.reduce_crossing_wall.value = saved_reduce_crossing_wall; + if (std::abs(m_writer.get_position().z() - pass_z) > EPSILON) { + BOOST_LOG_TRIVIAL(warning) << "Local-Z pass z restore" + << " print_z=" << print_z + << " layer_id=" << pass_plan.layer_id + << " pass_index=" << pass_plan.pass_index + << " extruder=" << local_extruder_id + << " expected_pass_z=" << pass_z + << " observed_z_after_toolchange=" << m_writer.get_position().z(); + gcode += m_writer.travel_to_z(pass_z, "Local-Z pass z restore"); + } + std::vector instances_to_print = + sort_print_object_instances(objects_by_extruder, layers, ordering, single_object_instance_idx); + + for (InstanceToPrint& instance_to_print : instances_to_print) { + const LayerToPrint& layer_to_print = layers[instance_to_print.layer_id]; + const bool object_layer_over_raft = + layer_to_print.object_layer && layer_to_print.object_layer->id() > 0 && + instance_to_print.print_object.slicing_parameters().raft_layers() == layer_to_print.object_layer->id(); + + m_config.apply(instance_to_print.print_object.config(), true); + m_layer = layer_to_print.layer(); + m_object_layer_over_raft = object_layer_over_raft; + const bool saved_reduce_crossing_wall = m_config.reduce_crossing_wall.value; + // Local-Z phase-b emits many short micro-passes. Avoid-crossing + // travel planning is expensive and fragile on these fragments, so + // keep travel simple here. + m_config.reduce_crossing_wall.value = false; + m_avoid_crossing_perimeters.disable_once(); + + const Point& offset = instance_to_print.print_object.instances()[instance_to_print.instance_id].shift; + std::pair this_object_copy(&instance_to_print.print_object, offset); + if (m_last_obj_copy != this_object_copy) + m_avoid_crossing_perimeters.use_external_mp_once(); + m_last_obj_copy = this_object_copy; + this->set_origin(unscale(offset)); + + for (ObjectByExtruder::Island& island : instance_to_print.object_by_extruder.islands) { + gcode += this->extrude_perimeters(print, island.by_region, first_layer, false); + gcode += this->extrude_perimeters(print, island.by_region, first_layer, true); + } + m_config.reduce_crossing_wall.value = saved_reduce_crossing_wall; + } + + local_z_phase_b_active_extruder = int(local_extruder_id); } + + m_nominal_z = saved_nominal_z; + m_last_layer_z = saved_last_layer_z; } - m_nominal_z = saved_nominal_z; - m_last_layer_z = saved_last_layer_z; + + pass_ref_idx = pass_group_end; } // Keep wipe tower planning synchronized with the normal per-layer extruder loop: @@ -5248,7 +5507,8 @@ LayerResult GCode::process_layer(const Print& print, << " restore_extruder=" << local_z_phase_b_start_extruder; gcode += "; local-z phase-b restore pre-pass extruder for wipe tower\n"; if (m_wipe_tower) { - gcode += m_wipe_tower->tool_change(*this, local_z_phase_b_start_extruder, false, true); + gcode += m_wipe_tower->tool_change(*this, local_z_phase_b_start_extruder, false, true, + print_z + m_config.z_offset.value); m_last_processor_extrusion_role = erWipeTower; } else { gcode += this->set_extruder(static_cast(local_z_phase_b_start_extruder), print_z); @@ -5266,12 +5526,6 @@ LayerResult GCode::process_layer(const Print& print, } gcode += "; local-z phase-b perimeter passes end\n"; } - - std::vector layer_extruders = layer_tools.extruders; - for (const auto& by_extruder_entry : by_extruder) { - if (std::find(layer_extruders.begin(), layer_extruders.end(), by_extruder_entry.first) == layer_extruders.end()) - layer_extruders.emplace_back(by_extruder_entry.first); - } // Extrude the skirt, brim, support, perimeters, infill ordered by the extruders. for (unsigned int extruder_id : layer_extruders) { if (print.config().skirt_type == stCombined && !print.skirt().empty()) diff --git a/src/libslic3r/GCode.hpp b/src/libslic3r/GCode.hpp index 03708b8e4c..a87491414c 100644 --- a/src/libslic3r/GCode.hpp +++ b/src/libslic3r/GCode.hpp @@ -79,6 +79,7 @@ public: const Vec3d plate_origin, const std::vector &priming, const std::vector> &tool_changes, + const std::vector> &local_z_reserve_boxes, const WipeTower::ToolChangeResult &final_purge) : m_left(/*float(print_config.wipe_tower_x.value)*/ 0.f), m_right(float(/*print_config.wipe_tower_x.value +*/ print_config.prime_tower_width.value)), @@ -87,9 +88,11 @@ public: m_extruder_offsets(print_config.extruder_offset.values), m_priming(priming), m_tool_changes(tool_changes), + m_local_z_reserve_boxes(local_z_reserve_boxes), m_final_purge(final_purge), m_layer_idx(-1), m_tool_change_idx(0), + m_local_z_reserve_slot_idx(local_z_reserve_boxes.size(), 0), m_plate_origin(plate_origin), m_single_extruder_multi_material(print_config.single_extruder_multi_material), m_enable_timelapse_print(print_config.timelapse_type.value == TimelapseType::tlSmooth), @@ -97,10 +100,16 @@ public: {} std::string prime(GCode &gcodegen); - void next_layer() { ++ m_layer_idx; m_tool_change_idx = 0; } + void next_layer() { + ++ m_layer_idx; + m_tool_change_idx = 0; + if (m_layer_idx >= 0 && size_t(m_layer_idx) < m_local_z_reserve_slot_idx.size()) + m_local_z_reserve_slot_idx[size_t(m_layer_idx)] = 0; + } // If local_z_unplanned is true, emit a wipe/toolchange without consuming the preplanned // per-layer wipe-tower sequence (used by Local-Z phase-b extra toolchanges). - std::string tool_change(GCode &gcodegen, int extruder_id, bool finish_layer, bool local_z_unplanned = false); + std::string tool_change(GCode &gcodegen, int extruder_id, bool finish_layer, bool local_z_unplanned = false, + double local_z_nominal_layer_z = -1.); bool is_empty_wipe_tower_gcode(GCode &gcodegen, int extruder_id, bool finish_layer); std::string finalize(GCode &gcodegen); std::vector used_filament_length() const; @@ -127,10 +136,12 @@ private: // Reference to cached values at the Printer class. const std::vector &m_priming; const std::vector> &m_tool_changes; + const std::vector> &m_local_z_reserve_boxes; const WipeTower::ToolChangeResult &m_final_purge; // Current layer index. int m_layer_idx; int m_tool_change_idx; + std::vector m_local_z_reserve_slot_idx; double m_last_wipe_tower_print_z = 0.f; // BBS diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index 8bbcc51447..ccb819a91e 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -263,6 +263,7 @@ ToolOrdering::ToolOrdering(const PrintObject &object, unsigned int first_extrude { m_is_BBL_printer = object.print()->is_BBL_printer(); m_print_full_config = &object.print()->full_print_config(); + m_print_config_ptr = &object.print()->config(); m_print_object_ptr = &object; // Mixed filament support. m_mixed_mgr = &object.print()->mixed_filament_manager(); @@ -1033,7 +1034,7 @@ void ToolOrdering::reorder_extruders_for_minimum_flush_volume() return false; }; - std::optionalcurrent_extruder_id; + std::optional current_extruder_id; for (int i = 0; i < m_layer_tools.size(); ++i) { LayerTools& lt = m_layer_tools[i]; if (lt.extruders.empty()) diff --git a/src/libslic3r/GCode/WipeTower2.cpp b/src/libslic3r/GCode/WipeTower2.cpp index e3ea8bc306..00ac99c326 100644 --- a/src/libslic3r/GCode/WipeTower2.cpp +++ b/src/libslic3r/GCode/WipeTower2.cpp @@ -1278,6 +1278,7 @@ WipeTower2::WipeTower2(const PrintConfig& config, , m_extra_flow(float(config.wipe_tower_extra_flow / 100.)) , m_extra_spacing_wipe(float(config.wipe_tower_extra_spacing / 100. * config.wipe_tower_extra_flow / 100.)) , m_extra_spacing_ramming(float(config.wipe_tower_extra_spacing / 100.)) + , m_local_z_wipe_tower_purge_lines(float(config.local_z_wipe_tower_purge_lines)) , m_y_shift(0.f) , m_z_pos(0.f) , m_bridging(float(config.wipe_tower_bridging)) @@ -1612,6 +1613,63 @@ WipeTower::ToolChangeResult WipeTower2::tool_change(size_t tool) return construct_tcr(writer, false, old_tool, false); } +WipeTower::ToolChangeResult WipeTower2::local_z_tool_change(size_t new_tool, + const WipeTower::box_coordinates& cleaning_box, + float wipe_volume) +{ + const size_t old_tool = m_current_tool; + + WipeTowerWriter2 writer(m_layer_height, m_perimeter_width, m_gcode_flavor, m_filpar, m_enable_arc_fitting, m_printer_model); + writer.set_extrusion_flow(m_extrusion_flow) + .set_z(m_z_pos) + .set_initial_tool(m_current_tool) + .append(";--------------------\n" + "; CP TOOLCHANGE START\n"); + + writer.comment_with_value(" toolchange #", m_num_tool_changes + 1); + writer.append(std::string("; material : " + (m_current_tool < m_filpar.size() ? m_filpar[m_current_tool].material : "(NONE)") + " -> " + + m_filpar[new_tool].material + "\n") + .c_str()) + .append(";--------------------\n"); + writer.append(";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Tower_Start) + "\n"); + + writer.speed_override_backup(); + writer.speed_override(100); + writer.set_initial_position(cleaning_box.ld, m_wipe_tower_width, m_wipe_tower_depth, 0.f); + + if (m_set_extruder_trimpot) + writer.set_extruder_trimpot(750); + + const int old_tool_temp = is_first_layer() ? m_filpar[m_current_tool].first_layer_temperature : m_filpar[m_current_tool].temperature; + const int new_tool_temp = is_first_layer() ? m_filpar[new_tool].first_layer_temperature : m_filpar[new_tool].temperature; + + toolchange_Unload(writer, cleaning_box, m_filpar[m_current_tool].material, old_tool_temp, new_tool_temp); + toolchange_Change(writer, new_tool, m_filpar[new_tool].material); + toolchange_Load(writer, cleaning_box); + writer.travel(writer.x(), writer.y() - m_perimeter_width); + toolchange_Wipe(writer, cleaning_box, wipe_volume); + writer.append(";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Wipe_Tower_End) + "\n"); + + ++m_num_tool_changes; + + if (m_set_extruder_trimpot) + writer.set_extruder_trimpot(550); + writer.speed_override_restore(); + writer.feedrate(m_travel_speed * 60.f) + .flush_planner_queue() + .reset_extruder() + .append("; CP TOOLCHANGE END\n" + ";------------------\n" + "\n\n"); + + if (m_current_tool < m_used_filament_length.size()) + m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); + + WipeTower::ToolChangeResult result = construct_tcr(writer, false, old_tool, false); + result.purge_volume = wipe_volume; + return result; +} + // Ram the hot material out of the melt zone, retract the filament into the cooling tubes and let it cool. void WipeTower2::toolchange_Unload(WipeTowerWriter2& writer, const WipeTower::box_coordinates& cleaning_box, @@ -1991,9 +2049,11 @@ WipeTower::ToolChangeResult WipeTower2::finish_layer() // If spare layers are excluded -> if 1 or less toolchange has been done, it must be still the first layer, too. So slow down. bool first_layer = is_first_layer() || (m_num_tool_changes <= 1 && m_no_sparse_layers); float feedrate = first_layer ? m_first_layer_speed * 60.f : std::min(m_wipe_tower_max_purge_speed * 60.f, m_infill_speed * 60.f); - float current_depth = m_layer_info->depth - m_layer_info->toolchanges_depth(); - WipeTower::box_coordinates fill_box(Vec2f(m_perimeter_width, m_layer_info->depth - (current_depth - m_perimeter_width)), - m_wipe_tower_width - 2 * m_perimeter_width, current_depth - m_perimeter_width); + float reserve_depth = m_layer_info->local_z_reserve_depth(); + float current_depth = std::max(0.f, m_layer_info->depth - m_layer_info->toolchanges_depth() - reserve_depth); + float fill_depth = std::max(0.f, current_depth - m_perimeter_width); + WipeTower::box_coordinates fill_box(Vec2f(m_perimeter_width, m_layer_info->depth - fill_depth), + m_wipe_tower_width - 2 * m_perimeter_width, fill_depth); writer.set_initial_position((m_left_to_right ? fill_box.ru : fill_box.lu), // so there is never a diagonal travel m_wipe_tower_width, m_wipe_tower_depth, m_internal_rotation); @@ -2242,6 +2302,54 @@ void WipeTower2::plan_toolchange(float z_par, float layer_height_par, unsigned i WipeTowerInfo::ToolChange(old_tool, new_tool, ramming_depth + wiping_depth, ramming_depth, first_wipe_line, wipe_volume)); } +void WipeTower2::plan_local_z_reserve(float z_par, float layer_height_par, size_t reserve_slot_count, float wipe_volume) +{ + if (reserve_slot_count == 0) + return; + + assert(m_plan.empty() || m_plan.back().z <= z_par + WT_EPSILON); + + if (m_plan.empty() || m_plan.back().z + WT_EPSILON < z_par) + m_plan.push_back(WipeTowerInfo(z_par, layer_height_par)); + + const float mini_wipe_depth = m_local_z_wipe_tower_purge_lines * m_perimeter_width * m_extra_spacing_wipe; + const float wipe_width = std::max(0.f, m_wipe_tower_width - 3.f * m_perimeter_width); + const float wiping_depth = wipe_width > WT_EPSILON ? + get_wipe_depth(std::max(0.f, wipe_volume), layer_height_par, m_perimeter_width, m_extra_flow, + m_extra_spacing_wipe, wipe_width) : + 0.f; + + float max_ramming_depth = 0.f; + if (wipe_width > WT_EPSILON) { + for (const FilamentParameters& filament : m_filpar) { + const bool do_ramming = (m_semm && m_enable_filament_ramming) || filament.multitool_ramming; + if (!do_ramming || filament.ramming_speed.empty()) + continue; + + const float line_width = m_perimeter_width * filament.ramming_line_width_multiplicator; + const float line_step = + (m_perimeter_width * filament.ramming_line_width_multiplicator * filament.ramming_step_multiplicator) * + m_extra_spacing_ramming; + if (line_width <= WT_EPSILON || line_step <= WT_EPSILON) + continue; + + const float ramming_volume = 0.25f * std::accumulate(filament.ramming_speed.begin(), filament.ramming_speed.end(), 0.f); + const float length_to_extrude = volume_to_length(ramming_volume, line_width, layer_height_par); + const float ramming_depth = + (float(int(length_to_extrude / wipe_width) + 1) * line_step); + max_ramming_depth = std::max(max_ramming_depth, ramming_depth); + } + } + + const float full_toolchange_depth = max_ramming_depth + wiping_depth; + const float slot_depth = + std::max(2.5f * m_perimeter_width, std::max(mini_wipe_depth + m_perimeter_width, full_toolchange_depth + m_perimeter_width)); + + WipeTowerInfo &layer = m_plan.back(); + layer.local_z_reserve_slot_depth = std::max(layer.local_z_reserve_slot_depth, slot_depth); + layer.local_z_reserve_slot_count += reserve_slot_count; +} + void WipeTower2::plan_tower() { // Calculate m_wipe_tower_depth (maximum depth for all the layers) and propagate depths downwards @@ -2252,7 +2360,7 @@ void WipeTower2::plan_tower() m_current_height = 0.f; for (int layer_index = int(m_plan.size()) - 1; layer_index >= 0; --layer_index) { - float this_layer_depth = std::max(m_plan[layer_index].depth, m_plan[layer_index].toolchanges_depth()); + float this_layer_depth = std::max(m_plan[layer_index].depth, m_plan[layer_index].planned_depth()); m_plan[layer_index].depth = this_layer_depth; if (this_layer_depth > m_wipe_tower_depth - m_perimeter_width) @@ -2431,6 +2539,64 @@ std::vector> WipeTower2::get_z_and_depth_pairs() const return out; } +static Vec2f rotate_local_z_reserve_point(const Vec2f& pt, float tower_width, float tower_depth, float y_shift, float internal_angle_deg) +{ + Vec2f shifted = pt; + shifted.x() -= tower_width / 2.f; + shifted.y() += y_shift - tower_depth / 2.f; + const double angle = internal_angle_deg * double(M_PI / 180.); + const double c = std::cos(angle); + const double s = std::sin(angle); + return Vec2f(float(shifted.x() * c - shifted.y() * s) + tower_width / 2.f, + float(shifted.x() * s + shifted.y() * c) + tower_depth / 2.f); +} + +std::vector> WipeTower2::get_local_z_reserve_boxes() const +{ + std::vector> out; + out.reserve(m_plan.size()); + + for (size_t layer_idx = 0; layer_idx < m_plan.size(); ++layer_idx) { + const WipeTowerInfo& layer = m_plan[layer_idx]; + std::vector layer_boxes; + layer_boxes.reserve(layer.local_z_reserve_slot_count); + + if (layer.local_z_reserve_slot_count > 0 && layer.local_z_reserve_slot_depth > WT_EPSILON) { + const float y_shift = layer.depth < m_wipe_tower_depth - m_perimeter_width ? + (m_wipe_tower_depth - layer.depth - m_perimeter_width) / 2.f : 0.f; + const float internal_angle = (layer_idx % 2 == 0) ? 180.f : 0.f; + + for (size_t slot_idx = 0; slot_idx < layer.local_z_reserve_slot_count; ++slot_idx) { + const float slot_start = layer.toolchanges_depth() + float(slot_idx) * layer.local_z_reserve_slot_depth; + const float width = std::max(0.f, m_wipe_tower_width - 2.f * m_perimeter_width); + const float height = std::max(0.f, layer.local_z_reserve_slot_depth - m_perimeter_width); + if (width <= WT_EPSILON || height <= WT_EPSILON) + continue; + + const Vec2f ld_unrotated(m_perimeter_width, slot_start + m_perimeter_width); + const Vec2f rd_unrotated = ld_unrotated + Vec2f(width, 0.f); + const Vec2f ru_unrotated = ld_unrotated + Vec2f(width, height); + const Vec2f lu_unrotated = ld_unrotated + Vec2f(0.f, height); + + const Vec2f ld = rotate_local_z_reserve_point(ld_unrotated, m_wipe_tower_width, m_wipe_tower_depth, y_shift, internal_angle); + const Vec2f rd = rotate_local_z_reserve_point(rd_unrotated, m_wipe_tower_width, m_wipe_tower_depth, y_shift, internal_angle); + const Vec2f ru = rotate_local_z_reserve_point(ru_unrotated, m_wipe_tower_width, m_wipe_tower_depth, y_shift, internal_angle); + const Vec2f lu = rotate_local_z_reserve_point(lu_unrotated, m_wipe_tower_width, m_wipe_tower_depth, y_shift, internal_angle); + + const float min_x = std::min({ld.x(), rd.x(), ru.x(), lu.x()}); + const float max_x = std::max({ld.x(), rd.x(), ru.x(), lu.x()}); + const float min_y = std::min({ld.y(), rd.y(), ru.y(), lu.y()}); + const float max_y = std::max({ld.y(), rd.y(), ru.y(), lu.y()}); + layer_boxes.emplace_back(Vec2f(min_x, min_y), max_x - min_x, max_y - min_y); + } + } + + out.emplace_back(std::move(layer_boxes)); + } + + return out; +} + Polygon WipeTower2::generate_rib_polygon(const WipeTower::box_coordinates& wt_box) { auto get_current_layer_rib_len = [](float cur_height, float max_height, float max_len) -> float { diff --git a/src/libslic3r/GCode/WipeTower2.hpp b/src/libslic3r/GCode/WipeTower2.hpp index 736430046b..a76c8a9203 100644 --- a/src/libslic3r/GCode/WipeTower2.hpp +++ b/src/libslic3r/GCode/WipeTower2.hpp @@ -50,12 +50,14 @@ public: // Appends into internal structure m_plan containing info about the future wipe tower // to be used before building begins. The entries must be added ordered in z. void plan_toolchange(float z_par, float layer_height_par, unsigned int old_tool, unsigned int new_tool, float wipe_volume = 0.f); + void plan_local_z_reserve(float z_par, float layer_height_par, size_t reserve_slot_count, float wipe_volume = 0.f); // Iterates through prepared m_plan, generates ToolChangeResults and appends them to "result" void generate(std::vector> &result); float get_depth() const { return m_wipe_tower_depth; } std::vector> get_z_and_depth_pairs() const; + std::vector> get_local_z_reserve_boxes() const; float get_brim_width() const { return m_wipe_tower_brim_width_real; } float get_wipe_tower_height() const { return m_wipe_tower_height; } @@ -118,6 +120,8 @@ public: // Returns gcode for a toolchange and a final print head position. // On the first layer, extrude a brim around the future wipe tower first. WipeTower::ToolChangeResult tool_change(size_t new_tool); + WipeTower::ToolChangeResult local_z_tool_change(size_t new_tool, const WipeTower::box_coordinates& cleaning_box, float wipe_volume); + void set_current_tool(size_t tool) { m_current_tool = tool; } // Fill the unfilled space with a sparse infill. // Call this method only if layer_finished() is false. @@ -254,6 +258,7 @@ private: float m_extra_flow = 1.f; float m_extra_spacing_wipe = 1.f; float m_extra_spacing_ramming = 1.f; + float m_local_z_wipe_tower_purge_lines = 3.f; bool is_first_layer() const { return size_t(m_layer_info - m_plan.begin()) == m_first_layer_idx; } @@ -289,6 +294,10 @@ private: float height; // layer height float depth; // depth of the layer based on all layers above float toolchanges_depth() const { float sum = 0.f; for (const auto &a : tool_changes) sum += a.required_depth; return sum; } + float local_z_reserve_slot_depth { 0.f }; + size_t local_z_reserve_slot_count { 0 }; + float local_z_reserve_depth() const { return local_z_reserve_slot_depth * float(local_z_reserve_slot_count); } + float planned_depth() const { return toolchanges_depth() + local_z_reserve_depth(); } std::vector tool_changes; diff --git a/src/libslic3r/LocalZOrderOptimizer.hpp b/src/libslic3r/LocalZOrderOptimizer.hpp new file mode 100644 index 0000000000..21a32880c7 --- /dev/null +++ b/src/libslic3r/LocalZOrderOptimizer.hpp @@ -0,0 +1,72 @@ +#ifndef slic3r_LocalZOrderOptimizer_hpp_ +#define slic3r_LocalZOrderOptimizer_hpp_ + +#include +#include +#include + +namespace Slic3r { +namespace LocalZOrderOptimizer { + +inline bool bucket_contains_extruder(const std::vector &extruders, int extruder_id) +{ + return extruder_id >= 0 && + std::find(extruders.begin(), extruders.end(), static_cast(extruder_id)) != extruders.end(); +} + +inline std::vector order_bucket_extruders(std::vector extruders, + int current_extruder, + int preferred_last_extruder = -1) +{ + extruders.erase(std::unique(extruders.begin(), extruders.end()), extruders.end()); + if (extruders.empty()) + return extruders; + + if (current_extruder >= 0) { + auto current_it = std::find(extruders.begin(), extruders.end(), static_cast(current_extruder)); + if (current_it != extruders.end()) + std::rotate(extruders.begin(), current_it, extruders.end()); + } + + if (preferred_last_extruder >= 0 && extruders.size() > 1 && static_cast(extruders.front()) != preferred_last_extruder) { + auto preferred_it = std::find(extruders.begin() + 1, extruders.end(), static_cast(preferred_last_extruder)); + if (preferred_it != extruders.end()) + std::rotate(preferred_it, preferred_it + 1, extruders.end()); + } + + return extruders; +} + +inline std::vector order_pass_group(const std::vector> &group_extruders, int current_extruder) +{ + std::vector remaining(group_extruders.size()); + std::iota(remaining.begin(), remaining.end(), size_t(0)); + + std::vector ordered; + ordered.reserve(group_extruders.size()); + + int active_extruder = current_extruder; + while (!remaining.empty()) { + auto next_it = std::find_if(remaining.begin(), remaining.end(), [&](size_t idx) { + return bucket_contains_extruder(group_extruders[idx], active_extruder); + }); + if (next_it == remaining.end()) + next_it = remaining.begin(); + + const size_t next_idx = *next_it; + ordered.push_back(next_idx); + + const std::vector ordered_bucket = order_bucket_extruders(group_extruders[next_idx], active_extruder); + if (!ordered_bucket.empty()) + active_extruder = static_cast(ordered_bucket.back()); + + remaining.erase(next_it); + } + + return ordered; +} + +} // namespace LocalZOrderOptimizer +} // namespace Slic3r + +#endif diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index b76114d9dd..68a273fafb 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -883,7 +883,7 @@ static std::vector s_Preset_print_options { "tree_support_brim_width", "gcode_comments", "gcode_label_objects", "initial_layer_travel_speed", "exclude_object", "slow_down_layers", "infill_anchor", "infill_anchor_max","initial_layer_min_bead_width", "make_overhang_printable", "make_overhang_printable_angle", "make_overhang_printable_hole_size" ,"notes", - "wipe_tower_cone_angle", "wipe_tower_extra_spacing","wipe_tower_max_purge_speed", + "wipe_tower_cone_angle", "wipe_tower_extra_spacing","wipe_tower_max_purge_speed", "local_z_wipe_tower_purge_lines", "wipe_tower_wall_type", "wipe_tower_extra_rib_length", "wipe_tower_rib_width", "wipe_tower_fillet_wall", "wipe_tower_filament", "wiping_volumes_extruders","wipe_tower_bridging", "wipe_tower_extra_flow","single_extruder_multi_material_priming", "wipe_tower_rotation_angle", "tree_support_branch_distance_organic", "tree_support_branch_diameter_organic", "tree_support_branch_angle_organic", diff --git a/src/libslic3r/PresetBundle.cpp b/src/libslic3r/PresetBundle.cpp index 78ce62a913..52c33b68c3 100644 --- a/src/libslic3r/PresetBundle.cpp +++ b/src/libslic3r/PresetBundle.cpp @@ -9,6 +9,9 @@ #include "common_func/common_func.hpp" #include +#include +#include +#include #include #include #include @@ -32,6 +35,30 @@ namespace Slic3r { +namespace { + +bool startup_profile_enabled() +{ + static const bool enabled = [] { + const char* value = std::getenv("ORCA_STARTUP_PROFILE"); + if (value == nullptr) + return false; + + std::string normalized(value); + std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + return normalized == "1" || normalized == "true" || normalized == "yes" || normalized == "on"; + }(); + return enabled; +} + +void startup_profile_log(const std::string& message) +{ + if (startup_profile_enabled()) + BOOST_LOG_TRIVIAL(warning) << "[StartupProfile] " << message; +} + +} // namespace + static std::vector s_project_options { "flush_volumes_vector", "flush_volumes_matrix", @@ -250,6 +277,10 @@ void PresetBundle::copy_files(const std::string& from) PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, ForwardCompatibilitySubstitutionRule substitution_rule, const PresetPreferences& preferred_selection/* = PresetPreferences()*/) { + const bool startup_profile = startup_profile_enabled(); + const auto total_start = std::chrono::steady_clock::now(); + auto phase_start = total_start; + // First load the vendor specific system presets. PresetsConfigSubstitutions substitutions; std::string errors_cummulative; @@ -258,6 +289,13 @@ PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, Forward BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" enter, substitution_rule %1%, preferred printer_model_id %2%")%substitution_rule%preferred_selection.printer_model_id; //BBS: change system config to json std::tie(substitutions, errors_cummulative) = this->load_system_presets_from_json(substitution_rule); + if (startup_profile) { + const auto now = std::chrono::steady_clock::now(); + startup_profile_log("PresetBundle::load_presets step=load_system_presets_from_json step_ms=" + + std::to_string(std::chrono::duration_cast(now - phase_start).count()) + + " total_ms=" + std::to_string(std::chrono::duration_cast(now - total_start).count())); + phase_start = now; + } // BBS load preset from user's folder, load system default if // BBS: change directories by design @@ -267,16 +305,34 @@ PresetsConfigSubstitutions PresetBundle::load_presets(AppConfig &config, Forward } else { load_user_presets(dir_user_presets, substitution_rule); } + if (startup_profile) { + const auto now = std::chrono::steady_clock::now(); + startup_profile_log("PresetBundle::load_presets step=load_user_presets step_ms=" + + std::to_string(std::chrono::duration_cast(now - phase_start).count()) + + " total_ms=" + std::to_string(std::chrono::duration_cast(now - total_start).count())); + phase_start = now; + } this->update_multi_material_filament_presets(); this->update_compatible(PresetSelectCompatibleType::Never); this->load_selections(config, preferred_selection); + if (startup_profile) { + const auto now = std::chrono::steady_clock::now(); + startup_profile_log("PresetBundle::load_presets step=post_load_selection step_ms=" + + std::to_string(std::chrono::duration_cast(now - phase_start).count()) + + " total_ms=" + std::to_string(std::chrono::duration_cast(now - total_start).count())); + } set_calibrate_printer(""); //BBS: add config related logs BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(" finished, returned substitutions %1%")%substitutions.size(); + if (startup_profile) { + const auto total_ms = std::chrono::duration_cast(std::chrono::steady_clock::now() - total_start).count(); + startup_profile_log("PresetBundle::load_presets end substitutions=" + std::to_string(substitutions.size()) + + " total_ms=" + std::to_string(total_ms)); + } return substitutions; } @@ -1204,6 +1260,9 @@ void PresetBundle::remove_users_preset(AppConfig &config, std::map PresetBundle::load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule) { + const bool startup_profile = startup_profile_enabled(); + const auto total_start = std::chrono::steady_clock::now(); + //BBS: add config related logs BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, compatibility_rule %1%")%compatibility_rule; if (compatibility_rule == ForwardCompatibilitySubstitutionRule::EnableSystemSilent) @@ -1245,6 +1304,7 @@ std::pair PresetBundle::load_system_pre for (auto &vendor_name : vendor_names) { + const auto vendor_start = std::chrono::steady_clock::now(); if (validation_mode && !vendor_to_validate.empty() && vendor_name != vendor_to_validate && vendor_name != ORCA_FILAMENT_LIBRARY) continue; @@ -1279,6 +1339,12 @@ std::pair PresetBundle::load_system_pre errors_cummulative += "\n"; } } + + if (startup_profile) { + const auto vendor_ms = std::chrono::duration_cast(std::chrono::steady_clock::now() - vendor_start).count(); + startup_profile_log("PresetBundle::load_system_presets_from_json vendor=" + vendor_name + + " vendor_ms=" + std::to_string(vendor_ms)); + } } if (first) { @@ -1286,9 +1352,14 @@ std::pair PresetBundle::load_system_pre this->reset(false); } - this->update_system_maps(); + this->update_system_maps(); //BBS: add config related logs BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" finished, errors_cummulative %1%")%errors_cummulative; + if (startup_profile) { + const auto total_ms = std::chrono::duration_cast(std::chrono::steady_clock::now() - total_start).count(); + startup_profile_log("PresetBundle::load_system_presets_from_json end vendor_count=" + std::to_string(vendor_names.size()) + + " total_ms=" + std::to_string(total_ms)); + } return std::make_pair(std::move(substitutions), errors_cummulative); } @@ -2729,6 +2800,9 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool std::pair PresetBundle::load_vendor_configs_from_json( const std::string &path, const std::string &vendor_name, LoadConfigBundleAttributes flags, ForwardCompatibilitySubstitutionRule compatibility_rule, const PresetBundle* base_bundle) { + const bool startup_profile = startup_profile_enabled(); + const auto total_start = std::chrono::steady_clock::now(); + // Enable substitutions for user config bundle, throw an exception when loading a system profile. ConfigSubstitutionContext substitution_context { compatibility_rule }; PresetsConfigSubstitutions substitutions; @@ -2830,6 +2904,14 @@ std::pair PresetBundle::load_vendor_configs_ //goto __error_process; } + if (startup_profile) { + startup_profile_log("PresetBundle::load_vendor_configs_from_json vendor=" + vendor_name + + " machine_models=" + std::to_string(machine_model_subfiles.size()) + + " process=" + std::to_string(process_subfiles.size()) + + " filaments=" + std::to_string(filament_subfiles.size()) + + " machines=" + std::to_string(machine_subfiles.size())); + } + if (flags.has(LoadConfigBundleAttribute::LoadFilamentOnly)) { machine_model_subfiles.clear(); machine_subfiles.clear(); @@ -3171,6 +3253,7 @@ std::pair PresetBundle::load_vendor_configs_ presets = &this->prints; configs.clear(); filament_id_maps.clear(); + auto process_start = std::chrono::steady_clock::now(); for (auto& subfile : process_subfiles) { std::string reason = parse_subfile(substitution_context, substitutions, flags, subfile, configs, filament_id_maps, presets, presets_loaded); @@ -3182,12 +3265,18 @@ std::pair PresetBundle::load_vendor_configs_ throw ConfigurationError((boost::format("Failed loading configuration file %1%\nSuggest cleaning the directory %2% firstly") % subfile_path % path).str()); } } + if (startup_profile) { + const auto process_ms = std::chrono::duration_cast(std::chrono::steady_clock::now() - process_start).count(); + startup_profile_log("PresetBundle::load_vendor_configs_from_json vendor=" + vendor_name + + " section=process section_ms=" + std::to_string(process_ms)); + } //3.2) paste the filaments presets = &this->filaments; configs.clear(); filament_id_maps.clear(); const auto is_orca_lib = vendor_name == ORCA_FILAMENT_LIBRARY; + auto filament_start = std::chrono::steady_clock::now(); for (auto& subfile : filament_subfiles) { std::string reason = parse_subfile(substitution_context, substitutions, flags, subfile, configs, filament_id_maps, presets, @@ -3200,6 +3289,11 @@ std::pair PresetBundle::load_vendor_configs_ throw ConfigurationError((boost::format("Failed loading configuration file %1%\nSuggest cleaning the directory %2% firstly") % subfile_path % path).str()); } } + if (startup_profile) { + const auto filament_ms = std::chrono::duration_cast(std::chrono::steady_clock::now() - filament_start).count(); + startup_profile_log("PresetBundle::load_vendor_configs_from_json vendor=" + vendor_name + + " section=filament section_ms=" + std::to_string(filament_ms)); + } if (is_orca_lib) { m_config_maps = configs; m_filament_id_maps = filament_id_maps; @@ -3209,6 +3303,7 @@ std::pair PresetBundle::load_vendor_configs_ presets = &this->printers; configs.clear(); filament_id_maps.clear(); + auto machine_start = std::chrono::steady_clock::now(); for (auto& subfile : machine_subfiles) { std::string reason = parse_subfile(substitution_context, substitutions, flags, subfile, configs, filament_id_maps, presets, presets_loaded); @@ -3220,6 +3315,14 @@ std::pair PresetBundle::load_vendor_configs_ throw ConfigurationError((boost::format("Failed loading configuration file %1%\nSuggest cleaning the directory %2% firstly") % subfile_path % path).str()); } } + if (startup_profile) { + const auto machine_ms = std::chrono::duration_cast(std::chrono::steady_clock::now() - machine_start).count(); + const auto total_ms = std::chrono::duration_cast(std::chrono::steady_clock::now() - total_start).count(); + startup_profile_log("PresetBundle::load_vendor_configs_from_json vendor=" + vendor_name + + " section=machine section_ms=" + std::to_string(machine_ms) + + " presets_loaded=" + std::to_string(presets_loaded) + + " total_ms=" + std::to_string(total_ms)); + } //BBS: add config related logs BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", finished, presets_loaded %1%")%presets_loaded; diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index de75c7756a..9f1585ab7f 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -52,6 +52,50 @@ template class PrintState; PrintRegion::PrintRegion(const PrintRegionConfig &config) : PrintRegion(config, config.hash()) {} PrintRegion::PrintRegion(PrintRegionConfig &&config) : PrintRegion(std::move(config), config.hash()) {} +static size_t estimate_local_z_wipe_tower_reserve_slots(const PrintObject& print_object, coordf_t print_z) +{ + const Layer* object_layer = print_object.get_layer_at_printz(print_z, EPSILON); + if (object_layer == nullptr) + return 0; + + const auto& intervals = print_object.local_z_intervals(); + const auto& plans = print_object.local_z_sublayer_plan(); + if (intervals.empty() || plans.empty()) + return 0; + + const size_t layer_id = size_t(object_layer->id()); + const auto interval_it = std::find_if(intervals.begin(), intervals.end(), [layer_id](const LocalZInterval& interval) { + return interval.layer_id == layer_id; + }); + if (interval_it == intervals.end() || !interval_it->has_mixed_paint || interval_it->sublayer_count <= 1 || + interval_it->first_sublayer_idx >= plans.size()) { + return 0; + } + + const size_t first_idx = interval_it->first_sublayer_idx; + const size_t end_idx = std::min(plans.size(), first_idx + interval_it->sublayer_count); + size_t reserve_slots = 0; + int previous_extruder = -1; + for (size_t plan_idx = first_idx; plan_idx < end_idx; ++plan_idx) { + const SubLayerPlan& plan = plans[plan_idx]; + if (!plan.split_interval) + continue; + + for (size_t extruder_id = 0; extruder_id < plan.painted_masks_by_extruder.size(); ++extruder_id) { + if (plan.painted_masks_by_extruder[extruder_id].empty()) + continue; + if (previous_extruder != int(extruder_id)) { + ++reserve_slots; + previous_extruder = int(extruder_id); + } + } + } + + if (reserve_slots > 0) + ++reserve_slots; + return reserve_slots; +} + //BBS // ORCA: Now this is a parameter //float Print::min_skirt_length = 0; @@ -2879,6 +2923,17 @@ void Print::_make_wipe_tower() current_extruder_id = extruder_id; } } + + if (m_config.dithering_local_z_mode) { + size_t local_z_reserve_slots = 0; + for (const PrintObject* print_object : m_objects) + local_z_reserve_slots += estimate_local_z_wipe_tower_reserve_slots(*print_object, layer_tools.print_z); + if (local_z_reserve_slots > 0) { + wipe_tower.plan_local_z_reserve((float) layer_tools.print_z, (float) layer_tools.wipe_tower_layer_height, + local_z_reserve_slots, (float) m_config.prime_volume); + } + } + layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this); if (&layer_tools == &m_wipe_tower_data.tool_ordering.back() || (&layer_tools + 1)->wipe_tower_partitions == 0) break; @@ -2890,6 +2945,7 @@ void Print::_make_wipe_tower() wipe_tower.generate(m_wipe_tower_data.tool_changes); m_wipe_tower_data.depth = wipe_tower.get_depth(); m_wipe_tower_data.z_and_depth_pairs = wipe_tower.get_z_and_depth_pairs(); + m_wipe_tower_data.local_z_reserve_boxes = wipe_tower.get_local_z_reserve_boxes(); m_wipe_tower_data.brim_width = wipe_tower.get_brim_width(); m_wipe_tower_data.height = wipe_tower.get_wipe_tower_height(); diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index 396c15cbd5..3d0f6483a9 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -778,6 +778,7 @@ struct WipeTowerData // Depth of the wipe tower to pass to GLCanvas3D for exact bounding box: float depth; std::vector> z_and_depth_pairs; + std::vector> local_z_reserve_boxes; float brim_width; float height; @@ -788,6 +789,7 @@ struct WipeTowerData used_filament.clear(); number_of_toolchanges = -1; depth = 0.f; + local_z_reserve_boxes.clear(); brim_width = 0.f; } diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 1a81612d78..78e814053a 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -6026,6 +6026,16 @@ void PrintConfigDef::init_fff_params() def->max = 300.; def->set_default_value(new ConfigOptionPercent(100.)); + def = this->add("local_z_wipe_tower_purge_lines", coFloat); + def->label = L("Local-Z mini wipe lines"); + def->tooltip = L("Number of purge lines reserved for each runtime Local-Z wipe tower toolchange. " + "Higher values improve cleanup but increase tower depth. " + "Only used when Local-Z dithering and the prime tower are enabled."); + def->sidetext = L("lines"); + def->mode = comAdvanced; + def->min = 1.0; + def->set_default_value(new ConfigOptionFloat(3.0)); + def = this->add("idle_temperature", coInts); def->label = L("Idle temperature"); def->tooltip = L("Nozzle temperature when the tool is currently not used in multi-tool setups. " diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 93ee468b16..c62066c99e 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1403,6 +1403,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionFloat, prime_tower_brim_chamfer_max_width)) ((ConfigOptionFloat, wipe_tower_bridging)) ((ConfigOptionPercent, wipe_tower_extra_flow)) + ((ConfigOptionFloat, local_z_wipe_tower_purge_lines)) ((ConfigOptionFloats, flush_volumes_matrix)) ((ConfigOptionFloats, flush_volumes_vector)) diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 367cd9d26a..103a15e53e 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -774,6 +774,11 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co "wipe_tower_no_sparse_layers"}) toggle_line(el, have_prime_tower && !is_BBL_Printer); + const bool local_z_dithering_enabled = + config->has("dithering_local_z_mode") && config->option("dithering_local_z_mode") != nullptr && + config->opt_bool("dithering_local_z_mode"); + toggle_line("local_z_wipe_tower_purge_lines", have_prime_tower && !is_BBL_Printer && local_z_dithering_enabled); + WipeTowerWallType wipe_tower_wall_type = config->opt_enum("wipe_tower_wall_type"); toggle_line("wipe_tower_cone_angle", have_prime_tower && !is_BBL_Printer && wipe_tower_wall_type == WipeTowerWallType::wtwCone); toggle_line("wipe_tower_extra_rib_length", have_prime_tower && !is_BBL_Printer && wipe_tower_wall_type == WipeTowerWallType::wtwRib); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index c4167f7324..cbb6a4b193 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -27,6 +27,8 @@ #include "slic3r/GUI/I18N.hpp" #include +#include +#include #include #include #include @@ -179,6 +181,71 @@ namespace GUI { class MainFrame; +namespace { + +bool startup_profile_enabled() +{ + static const bool enabled = [] { + const char* value = std::getenv("ORCA_STARTUP_PROFILE"); + if (value == nullptr) + return false; + + std::string normalized(value); + std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + return normalized == "1" || normalized == "true" || normalized == "yes" || normalized == "on"; + }(); + return enabled; +} + +class StartupProfiler +{ +public: + explicit StartupProfiler(const char* phase) + : m_phase(phase) + , m_enabled(startup_profile_enabled()) + , m_phase_start(std::chrono::steady_clock::now()) + , m_step_start(m_phase_start) + { + if (m_enabled) + BOOST_LOG_TRIVIAL(warning) << "[StartupProfile] phase=" << m_phase << " begin"; + } + + bool enabled() const { return m_enabled; } + + void mark(const char* step) + { + if (!m_enabled) + return; + const auto now = std::chrono::steady_clock::now(); + const auto step_ms = std::chrono::duration_cast(now - m_step_start).count(); + const auto total_ms = std::chrono::duration_cast(now - m_phase_start).count(); + BOOST_LOG_TRIVIAL(warning) << "[StartupProfile] phase=" << m_phase << " step=" << step << " step_ms=" << step_ms << " total_ms=" << total_ms; + m_step_start = now; + } + + void note(const std::string& message) const + { + if (m_enabled) + BOOST_LOG_TRIVIAL(warning) << "[StartupProfile] phase=" << m_phase << " " << message; + } + + ~StartupProfiler() + { + if (!m_enabled) + return; + const auto total_ms = std::chrono::duration_cast(std::chrono::steady_clock::now() - m_phase_start).count(); + BOOST_LOG_TRIVIAL(warning) << "[StartupProfile] phase=" << m_phase << " end total_ms=" << total_ms; + } + +private: + const char* m_phase; + bool m_enabled; + std::chrono::steady_clock::time_point m_phase_start; + std::chrono::steady_clock::time_point m_step_start; +}; + +} // namespace + void start_ping_test() { return; @@ -1069,21 +1136,29 @@ GUI_App::GUI_App() , m_wcp_download_manager(&WCPDownloadManager::getInstance()) , m_other_instance_message_handler(std::make_unique()) { + StartupProfiler profiler("GUI_App::GUI_App"); + //app config initializes early becasuse it is used in instance checking in Snapmaker_Orca.cpp this->init_app_config(); + profiler.mark("init_app_config"); this->init_download_path(); + profiler.mark("init_download_path"); #if wxUSE_WEBVIEW_EDGE this->init_webview_runtime(); + profiler.mark("init_webview_runtime"); #endif reset_to_active(); + profiler.mark("reset_to_active"); // test m_page_http_server.setPort(PAGE_HTTP_PORT); m_page_http_server.set_request_handler(HttpServer::web_server_handle_request); m_page_http_server.start(); + profiler.mark("m_page_http_server.start"); m_fltviews.set_app(this); + profiler.mark("m_fltviews.set_app"); } void GUI_App::shutdown(bool isRecreate) @@ -2067,11 +2142,14 @@ bool GUI_App::check_older_app_config(Semver current_version, bool backup) } void GUI_App::copy_web_resources() { + StartupProfiler profiler("GUI_App::copy_web_resources"); + auto data_web_path = boost::filesystem::path(data_dir()) / "web"; if (!boost::filesystem::exists(data_web_path / "flutter_web")) { auto source_path = boost::filesystem::path(resources_dir()) / "web" / "flutter_web"; auto target_path = data_web_path / "flutter_web"; copy_directory_recursively(source_path, target_path); + profiler.mark("copy flutter_web (missing target)"); } else { auto source_version_file = boost::filesystem::path(resources_dir()) / "web" / "flutter_web" / "version.json"; auto target_version_file = data_web_path / "flutter_web" / "version.json"; @@ -2087,10 +2165,13 @@ void GUI_App::copy_web_resources() { auto source_path = boost::filesystem::path(resources_dir()) / "web" / "flutter_web"; auto target_path = data_web_path / "flutter_web"; copy_directory_recursively(source_path, target_path); + profiler.mark("copy flutter_web (version upgrade)"); + } else { + profiler.note("flutter_web already up to date"); } } catch (std::exception& e) { - + profiler.note(std::string("version check failed: ") + e.what()); } } } @@ -2276,6 +2357,8 @@ class wxBoostLog : public wxLog bool GUI_App::on_init_inner() { + StartupProfiler profiler("GUI_App::on_init_inner"); + wxLog::SetActiveTarget(new wxBoostLog()); #if BBL_RELEASE_TO_PUBLIC wxLog::SetLogLevel(wxLOG_Message); @@ -2288,6 +2371,7 @@ bool GUI_App::on_init_inner() #ifdef NDEBUG wxImage::SetDefaultLoadFlags(0); // ignore waring in release build #endif + profiler.mark("wx init/log/image handlers"); #if defined(_WIN32) && ! defined(_WIN64) // BBS: remove 32bit build prompt @@ -2414,6 +2498,7 @@ bool GUI_App::on_init_inner() init_label_colours(); init_fonts(); wxGetApp().Update_dark_mode_flag(); + profiler.mark("language/theme/fonts"); #ifdef _MSW_DARK_MODE @@ -2451,6 +2536,7 @@ bool GUI_App::on_init_inner() remove_old_networking_plugins(); } } + profiler.mark("version/network plugin cleanup"); if(app_config->get("version") != SLIC3R_VERSION) { app_config->set("version", SLIC3R_VERSION); @@ -2485,11 +2571,14 @@ bool GUI_App::on_init_inner() // just checking for existence of Slic3r::data_dir is not enough : it may be an empty directory // supplied as argument to --datadir; in that case we should still run the wizard preset_bundle->setup_directories(); + profiler.mark("preset_bundle->setup_directories"); copy_web_resources(); + profiler.mark("copy_web_resources"); if (m_init_app_config_from_older) copy_older_config(); + profiler.mark("copy_older_config_if_needed"); if (is_editor()) { #ifdef __WXMSW__ @@ -2610,6 +2699,7 @@ bool GUI_App::on_init_inner() preset_bundle->set_default_suppressed(true); preset_bundle->backup_user_folder(); + profiler.mark("preset_bundle->backup_user_folder"); Bind(EVT_SHOW_IP_DIALOG, &GUI_App::show_ip_address_enter_dialog_handler, this); @@ -2634,7 +2724,9 @@ bool GUI_App::on_init_inner() } } */ copy_network_if_available(); + profiler.mark("copy_network_if_available"); on_init_network(); + profiler.mark("on_init_network"); if (m_agent && m_agent->is_user_login()) { enable_user_preset_folder(true); @@ -2654,6 +2746,7 @@ bool GUI_App::on_init_inner() show_error(nullptr, ex.what()); } //} + profiler.mark("preset_bundle->load_presets"); #ifdef WIN32 #if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) @@ -2673,6 +2766,7 @@ bool GUI_App::on_init_inner() auto downloadUlr = m_updateDialog->getUrl(); wxLaunchDefaultBrowser(downloadUlr); }); + profiler.mark("mainframe construction"); // hide settings tabs after first Layout if (is_editor()) { @@ -2698,6 +2792,7 @@ bool GUI_App::on_init_inner() } else load_current_presets(); + profiler.mark("load_current_presets"); if (plater_ != nullptr) { plater_->reset_project_dirty_initial_presets(); @@ -2710,6 +2805,7 @@ bool GUI_App::on_init_inner() #endif mainframe->Show(true); BOOST_LOG_TRIVIAL(info) << "main frame firstly shown"; + profiler.mark("mainframe->Show"); obj_list()->set_min_height(); @@ -2786,6 +2882,8 @@ bool GUI_App::on_init_inner() "configuration file.\nPlease note, application settings will be lost, but printer profiles will not be affected.")); } + profiler.mark("on_init_inner return"); + return true; } @@ -2931,16 +3029,21 @@ void GUI_App::copy_network_if_available() bool GUI_App::on_init_network(bool try_backup) { + StartupProfiler profiler("GUI_App::on_init_network"); + bool create_network_agent = false; auto should_load_networking_plugin = app_config->get_bool("installed_networking"); if(!should_load_networking_plugin) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "Don't load plugin as installed_networking is false"; + profiler.note("installed_networking=false, plugin load skipped"); } else { int load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(); + profiler.mark("NetworkAgent::initialize_network_module"); __retry: if (!load_agent_dll) { BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, load dll ok"; if (check_networking_version()) { + profiler.mark("check_networking_version"); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": on_init_network, compatibility version"; auto bambu_source = Slic3r::NetworkAgent::get_bambu_source_entry(); if (!bambu_source) { @@ -2957,6 +3060,7 @@ __retry: int result = Slic3r::NetworkAgent::unload_network_module(); BOOST_LOG_TRIVIAL(info) << "on_init_network, version mismatch, unload_network_module, result = " << result; load_agent_dll = Slic3r::NetworkAgent::initialize_network_module(true); + profiler.mark("initialize_network_module(backup)"); try_backup = false; goto __retry; } @@ -3023,6 +3127,7 @@ __retry: std::string country_code = app_config->get_country_code(); m_agent->set_country_code(country_code); m_agent->start(); + profiler.mark("m_agent->start"); } } else { @@ -3036,6 +3141,8 @@ __retry: m_user_manager = new Slic3r::UserManager(); } + profiler.note(std::string("create_network_agent=") + (create_network_agent ? "true" : "false")); + return true; } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 4e42859452..32b4c07aff 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -6181,7 +6181,7 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) "brim_width", "wall_loops", "wall_filament", "sparse_infill_density", "sparse_infill_filament", "top_shell_layers", "enable_support", "support_filament", "support_interface_filament", "support_top_z_distance", "support_bottom_z_distance", "raft_layers", - "wipe_tower_rotation_angle", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "wipe_tower_max_purge_speed", + "wipe_tower_rotation_angle", "wipe_tower_cone_angle", "wipe_tower_extra_spacing", "wipe_tower_extra_flow", "local_z_wipe_tower_purge_lines", "wipe_tower_max_purge_speed", "wipe_tower_wall_type", "wipe_tower_extra_rib_length","wipe_tower_rib_width","wipe_tower_fillet_wall", "wipe_tower_filament", "best_object_pos" diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 1d0d793c96..bb420ce16d 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2445,6 +2445,7 @@ void TabPrint::build() optgroup->append_single_option_line("wipe_tower_bridging", "multimaterial_settings_prime_tower#maximal-bridging-distance"); optgroup->append_single_option_line("wipe_tower_extra_spacing", "multimaterial_settings_prime_tower#wipe-tower-purge-lines-spacing"); optgroup->append_single_option_line("wipe_tower_extra_flow", "multimaterial_settings_prime_tower#extra-flow-for-purge"); + optgroup->append_single_option_line("local_z_wipe_tower_purge_lines", "multimaterial_settings_prime_tower"); optgroup->append_single_option_line("wipe_tower_max_purge_speed", "multimaterial_settings_prime_tower#maximum-wipe-tower-print-speed"); optgroup->append_single_option_line("wipe_tower_wall_type", "multimaterial_settings_prime_tower#wall-type"); optgroup->append_single_option_line("wipe_tower_cone_angle", "multimaterial_settings_prime_tower#stabilization-cone-apex-angle"); diff --git a/src/slic3r/Utils/Bonjour.cpp b/src/slic3r/Utils/Bonjour.cpp index a8f9ae8dbe..1acb33f895 100644 --- a/src/slic3r/Utils/Bonjour.cpp +++ b/src/slic3r/Utils/Bonjour.cpp @@ -23,18 +23,20 @@ namespace { void quarantine_socket(udp::socket& socket, bool& socket_usable, const char* action, const std::exception& e) { -#ifdef __APPLE__ socket_usable = false; boost::system::error_code ec; socket.cancel(ec); socket.close(ec); BOOST_LOG_TRIVIAL(error) << action << ": " << e.what(); -#else - (void) socket; - (void) socket_usable; - (void) action; - BOOST_LOG_TRIVIAL(error) << e.what(); -#endif +} + +template +void retain_usable_socket(std::vector& sockets, SocketT* socket) +{ + if (socket->is_usable()) + sockets.emplace_back(socket); + else + delete socket; } } @@ -699,20 +701,18 @@ UdpSocket::UdpSocket( Bonjour::ReplyFn replyfn, const asio::ip::address& multica UdpSocket::~UdpSocket() { - try { + boost::system::error_code ec; // 取消所有异步操作 - socket.cancel(); + socket.cancel(ec); // 关闭socket - if (socket.is_open()) { - socket.close(); - } + if (socket.is_open()) + socket.close(ec); - BOOST_LOG_TRIVIAL(debug) << "UdpSocket destroyed, socket closed"; - } - catch (const std::exception& e) { - BOOST_LOG_TRIVIAL(error) << "Error closing socket in destructor: " << e.what(); - } + if (ec) + BOOST_LOG_TRIVIAL(error) << "Error closing socket in destructor: " << ec.message(); + else + BOOST_LOG_TRIVIAL(debug) << "UdpSocket destroyed, socket closed"; } void UdpSocket::cancel() @@ -941,13 +941,13 @@ void Bonjour::priv::lookup_perform() } // create ipv4 socket for each interface // each will send to querry to for both ipv4 and ipv6 - for (const auto& intrfc : interfaces) - sockets.emplace_back(new LookupSocket(txt_keys, service, service_dn, protocol, replyfn, BonjourRequest::MCAST_IP4, intrfc, io_service)); + for (const auto& intrfc : interfaces) + retain_usable_socket(sockets, new LookupSocket(txt_keys, service, service_dn, protocol, replyfn, BonjourRequest::MCAST_IP4, intrfc, io_service)); } else { BOOST_LOG_TRIVIAL(info) << "Failed to resolve ipv4 interfaces: " << ec.message(); } if (sockets.empty()) - sockets.emplace_back(new LookupSocket(txt_keys, service, service_dn, protocol, replyfn, BonjourRequest::MCAST_IP4, io_service)); + retain_usable_socket(sockets, new LookupSocket(txt_keys, service, service_dn, protocol, replyfn, BonjourRequest::MCAST_IP4, io_service)); // ipv6 interfaces interfaces.clear(); //udp::resolver::query query(host, PORT, boost::asio::ip::resolver_query_base::numeric_service); @@ -962,9 +962,9 @@ void Bonjour::priv::lookup_perform() // create ipv6 socket for each interface // each will send to querry to for both ipv4 and ipv6 for (const auto& intrfc : interfaces) - sockets.emplace_back(new LookupSocket(txt_keys, service, service_dn, protocol, replyfn, BonjourRequest::MCAST_IP6, intrfc, io_service)); + retain_usable_socket(sockets, new LookupSocket(txt_keys, service, service_dn, protocol, replyfn, BonjourRequest::MCAST_IP6, intrfc, io_service)); if (interfaces.empty()) - sockets.emplace_back(new LookupSocket(txt_keys, service, service_dn, protocol, replyfn, BonjourRequest::MCAST_IP6, io_service)); + retain_usable_socket(sockets, new LookupSocket(txt_keys, service, service_dn, protocol, replyfn, BonjourRequest::MCAST_IP6, io_service)); } else { BOOST_LOG_TRIVIAL(info)<< "Failed to resolve ipv6 interfaces: " << ec.message(); } @@ -1047,12 +1047,12 @@ void Bonjour::priv::resolve_perform() // create ipv4 socket for each interface // each will send to querry to for both ipv4 and ipv6 for (const auto& intrfc : interfaces) - sockets.emplace_back(new ResolveSocket(hostname, reply_callback, BonjourRequest::MCAST_IP4, intrfc, io_service)); + retain_usable_socket(sockets, new ResolveSocket(hostname, reply_callback, BonjourRequest::MCAST_IP4, intrfc, io_service)); } else { BOOST_LOG_TRIVIAL(info) << "Failed to resolve ipv4 interfaces: " << ec.message(); } if (sockets.empty()) - sockets.emplace_back(new ResolveSocket(hostname, reply_callback, BonjourRequest::MCAST_IP4, io_service)); + retain_usable_socket(sockets, new ResolveSocket(hostname, reply_callback, BonjourRequest::MCAST_IP4, io_service)); // ipv6 interfaces interfaces.clear(); @@ -1066,9 +1066,9 @@ void Bonjour::priv::resolve_perform() // create ipv6 socket for each interface // each will send to querry to for both ipv4 and ipv6 for (const auto& intrfc : interfaces) - sockets.emplace_back(new ResolveSocket(hostname, reply_callback, BonjourRequest::MCAST_IP6, intrfc, io_service)); + retain_usable_socket(sockets, new ResolveSocket(hostname, reply_callback, BonjourRequest::MCAST_IP6, intrfc, io_service)); if (interfaces.empty()) - sockets.emplace_back(new ResolveSocket(hostname, reply_callback, BonjourRequest::MCAST_IP6, io_service)); + retain_usable_socket(sockets, new ResolveSocket(hostname, reply_callback, BonjourRequest::MCAST_IP6, io_service)); } else { BOOST_LOG_TRIVIAL(info) << "Failed to resolve ipv6 interfaces: " << ec.message(); } diff --git a/src/slic3r/Utils/Bonjour.hpp b/src/slic3r/Utils/Bonjour.hpp index 92cd79fbac..9ce582df9f 100644 --- a/src/slic3r/Utils/Bonjour.hpp +++ b/src/slic3r/Utils/Bonjour.hpp @@ -162,6 +162,7 @@ public: void send(); void async_receive(); void cancel(); + bool is_usable() const { return m_socket_usable; } protected: void receive_handler(SharedSession session, const boost::system::error_code& error, size_t bytes); virtual SharedSession create_session() const = 0; diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt index 07320fab41..38052e4b6b 100644 --- a/tests/libslic3r/CMakeLists.txt +++ b/tests/libslic3r/CMakeLists.txt @@ -11,6 +11,7 @@ add_executable(${_TEST_NAME}_tests test_elephant_foot_compensation.cpp test_geometry.cpp test_mixed_filament.cpp + test_local_z_order_optimizer.cpp test_placeholder_parser.cpp test_polygon.cpp test_mutable_polygon.cpp diff --git a/tests/libslic3r/test_local_z_order_optimizer.cpp b/tests/libslic3r/test_local_z_order_optimizer.cpp new file mode 100644 index 0000000000..51da2f885e --- /dev/null +++ b/tests/libslic3r/test_local_z_order_optimizer.cpp @@ -0,0 +1,29 @@ +#include + +#include "libslic3r/LocalZOrderOptimizer.hpp" + +using namespace Slic3r; + +TEST_CASE("Local-Z bucket ordering starts with the active extruder when possible", "[LocalZ][GCode]") +{ + const std::vector ordered = + LocalZOrderOptimizer::order_bucket_extruders({1, 2}, 2, 1); + + REQUIRE(ordered == std::vector{2, 1}); +} + +TEST_CASE("Local-Z pass groups keep a matching active extruder bucket first", "[LocalZ][GCode]") +{ + const std::vector ordered = + LocalZOrderOptimizer::order_pass_group({{1}, {2}}, 2); + + REQUIRE(ordered == std::vector{1, 0}); +} + +TEST_CASE("Local-Z pass groups keep original order when no bucket matches the active extruder", "[LocalZ][GCode]") +{ + const std::vector ordered = + LocalZOrderOptimizer::order_pass_group({{1}, {2}, {3}}, 0); + + REQUIRE(ordered == std::vector{0, 1, 2}); +}