From 88726d76e84547ff9613b847323ffd53836399e3 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 04:24:41 -0500 Subject: [PATCH 01/15] Purge tower part 1 --- result.json | 10 ++ src/libslic3r/GCode/ToolOrdering.cpp | 4 + src/libslic3r/Preset.cpp | 2 + src/libslic3r/Print.cpp | 223 ++++++++++++++++++++++++- src/libslic3r/Print.hpp | 12 ++ src/libslic3r/PrintConfig.cpp | 24 +++ src/libslic3r/PrintConfig.hpp | 5 + src/libslic3r/PrintObject.cpp | 3 +- src/libslic3r/PrintObjectSlice.cpp | 21 +++ src/slic3r/GUI/ConfigManipulation.cpp | 17 +- src/slic3r/GUI/GLCanvas3D.cpp | 8 +- src/slic3r/GUI/Jobs/ArrangeJob.cpp | 6 + src/slic3r/GUI/Plater.cpp | 227 ++++++++++++++++++++++++++ src/slic3r/GUI/Tab.cpp | 1 + 14 files changed, 548 insertions(+), 15 deletions(-) create mode 100644 result.json diff --git a/result.json b/result.json new file mode 100644 index 0000000000..517f6bffa5 --- /dev/null +++ b/result.json @@ -0,0 +1,10 @@ +{ + "error_string": "Success.", + "export_time": 0, + "layer_height": 0.0, + "plate_index": 0, + "prepare_time": 0, + "return_code": 0, + "sparse_infill_density": 0.0, + "wall_loops": 0 +} diff --git a/src/libslic3r/GCode/ToolOrdering.cpp b/src/libslic3r/GCode/ToolOrdering.cpp index f96748848f..85a8436c3e 100644 --- a/src/libslic3r/GCode/ToolOrdering.cpp +++ b/src/libslic3r/GCode/ToolOrdering.cpp @@ -381,6 +381,10 @@ bool ToolOrdering::insert_wipe_tower_extruder() { if (!m_print_config_ptr || !m_print_config_ptr->enable_prime_tower) return false; + // Belt mode has no classic wipe tower; the dedicated wipe tower filament + // must not inject extra toolchanges into the purge prism planning. + if (m_print_config_ptr->belt_printer) + return false; if (m_print_config_ptr->wipe_tower_filament == 0) return false; diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 3e6e2b9aaf..092923e336 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1160,6 +1160,8 @@ static std::vector s_Preset_print_options{ "prime_volume", "prime_tower_infill_gap", "prime_tower_flat_ironing", + "belt_purge_tower_width", + "belt_purge_tower_object", "enable_tower_interface_features", "enable_tower_interface_cooldown_during_tower", "wipe_tower_no_sparse_layers", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 017fa19736..390bba1ff2 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -22,6 +22,7 @@ #include "MaterialType.hpp" #include "Model.hpp" #include "format.hpp" +#include "LocalesUtils.hpp" #include #include @@ -389,6 +390,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "prime_volume" || opt_key == "flush_into_infill" || opt_key == "flush_into_support" + || opt_key == "belt_purge_tower_width" || opt_key == "initial_layer_infill_speed" || opt_key == "travel_speed" || opt_key == "travel_speed_z" @@ -1473,6 +1475,15 @@ StringObjectException Print::validate(std::vector *warnin } if (m_config.enable_prime_tower) { + if (m_config.belt_printer.value && m_config.print_sequence == PrintSequence::ByObject + && extruders.size() > 1 && warning != nullptr) { + StringObjectException warningtemp; + warningtemp.string = L("The belt purge tower is not generated in \"By object\" print sequence; " + "filament changes will not be purged."); + warningtemp.opt_key = "enable_prime_tower"; + warningtemp.is_warning = true; + *warning = warningtemp; + } for (const PrintObject* object : m_objects) { if (object->config().precise_z_height.value) { warn(L("Enabling both precise Z height and the prime tower may cause slicing errors."), "precise_z_height"); @@ -1596,7 +1607,7 @@ StringObjectException Print::validate(std::vector *warnin return {_u8L("Variable layer height is not supported with Organic supports.") }; } - if (this->has_wipe_tower() && ! m_objects.empty()) { + if ((this->has_wipe_tower() || this->has_belt_purge_tower()) && ! m_objects.empty()) { // Make sure all extruders use same diameter filament and have the same nozzle diameter // EPSILON comparison is used for nozzles and 10 % tolerance is used for filaments double first_nozzle_diam = m_config.nozzle_diameter.get_at(extruders.front()); @@ -1612,12 +1623,17 @@ StringObjectException Print::validate(std::vector *warnin } } - if (! m_config.use_relative_e_distances) - return { L("The Wipe Tower is currently only supported with the relative extruder addressing (use_relative_e_distances=1).") }; + // The following two constraints come from the classic wipe tower G-code + // generator; purging into the belt purge prism uses normal object + // extrusions and does not need them. + if (this->has_wipe_tower()) { + if (! m_config.use_relative_e_distances) + return { L("The Wipe Tower is currently only supported with the relative extruder addressing (use_relative_e_distances=1).") }; + + if (m_config.ooze_prevention && m_config.single_extruder_multi_material) + return {L("Ooze prevention is only supported with the wipe tower when 'single_extruder_multi_material' is off.")}; + } - if (m_config.ooze_prevention && m_config.single_extruder_multi_material) - return {L("Ooze prevention is only supported with the wipe tower when 'single_extruder_multi_material' is off.")}; - #if 0 if (m_config.gcode_flavor != gcfRepRapSprinter && m_config.gcode_flavor != gcfRepRapFirmware && m_config.gcode_flavor != gcfRepetier && m_config.gcode_flavor != gcfMarlinLegacy && m_config.gcode_flavor != gcfMarlinFirmware) @@ -2708,7 +2724,10 @@ void Print::process(long long *time_cost_with_cache, bool use_cache) m_wipe_tower_data.clear(); m_tool_ordering.clear(); - if (this->has_wipe_tower()) { + if (this->has_belt_purge_tower() && this->config().print_sequence != PrintSequence::ByObject) { + this->_plan_belt_purge(); + } + else if (this->has_wipe_tower()) { this->_make_wipe_tower(); } else if (this->config().print_sequence != PrintSequence::ByObject) { @@ -4085,6 +4104,13 @@ int Print::get_config_index(int filament_id, int layer_id, const std::vector 2) return true; @@ -4097,6 +4123,16 @@ bool Print::has_wipe_tower() const return false; } +// Belt purge prism: purging after filament changes is routed into a sliced +// prism object via the flush-into-objects machinery instead of a wipe tower. +bool Print::has_belt_purge_tower() const +{ + return m_config.belt_printer.value + && m_config.enable_prime_tower.value + && !m_config.spiral_mode.value + && m_config.filament_diameter.values.size() > 1; +} + const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const { // If the wipe tower wasn't created yet, make sure the depth and brim_width members are set to default. @@ -4183,6 +4219,179 @@ bool Print::enable_timelapse_print() const return m_config.timelapse_type.value == TimelapseType::tlSmooth; } +// Belt mode: snap the purge prism's layer grid onto the printed objects' grid. +// After belt slicing every object's layer print_z carries a per-object global +// z offset (mesh-vertex-scan belt_z_shift + instance-Y-dependent terms), so +// objects at different belt-Y positions do not share a layer grid. Purge +// marking looks absorbers up with get_layer_at_printz(lt.print_z, EPSILON), +// so the prism only absorbs purge at toolchange print_z values that coincide +// with one of its own layers. Shifting the prism by at most half a layer +// (a sub-layer-height displacement along the belt) makes its grid residue +// match the reference object's; both grids step by the same layer height +// (enforced by validate()), so matching residues means exact layer matches. +void Print::_align_belt_purge_layers() +{ + PrintObject *prism = nullptr; + for (PrintObject *po : m_objects) + if (po->config().belt_purge_tower_object.value) { + prism = po; + break; + } + if (prism == nullptr || prism->layers().empty()) + return; + + const double h = prism->config().layer_height.value; + if (h <= EPSILON) + return; + + // Grid residue of an object's layer grid: identical for all of an object's + // layers above the first since they step by h. + auto grid_offset = [h](const PrintObject *po) { + const double z = po->layers().front()->print_z; + return z - std::floor(z / h) * h; // in [0, h) + }; + + // Reference grid: the tallest non-prism object (proxy for the object with + // the most toolchange layers). + const PrintObject *ref = nullptr; + double ref_top = -std::numeric_limits::max(); + for (const PrintObject *po : m_objects) { + if (po->config().belt_purge_tower_object.value || po->layers().empty()) + continue; + const double top = po->layers().back()->print_z; + if (top > ref_top) { + ref_top = top; + ref = po; + } + } + if (ref == nullptr) + return; + + const double ref_offset = grid_offset(ref); + bool grids_mismatch = false; + for (const PrintObject *po : m_objects) { + if (po == ref || po->config().belt_purge_tower_object.value || po->layers().empty()) + continue; + double d = std::abs(grid_offset(po) - ref_offset); + d = std::min(d, h - d); + if (d > 5. * EPSILON) { + grids_mismatch = true; + break; + } + } + + // Shift normalized to (-h/2, h/2]. + double delta = ref_offset - grid_offset(prism); + if (delta > 0.5 * h) + delta -= h; + else if (delta <= -0.5 * h) + delta += h; + + BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] purge prism grid snap" + << " ref=" << ref->model_object()->name + << " ref_offset=" << ref_offset + << " prism_offset=" << grid_offset(prism) + << " delta=" << delta + << " grids_mismatch=" << grids_mismatch; + + prism->belt_shift_layer_grid(delta); + + if (grids_mismatch) + this->active_step_add_warning( + PrintStateBase::WarningLevel::NON_CRITICAL, + _u8L("Objects on the plate are sliced on different layer grids; the belt purge tower can only follow " + "one of them. Filament changes on layers of the other objects may not be fully purged.")); +} + +// Belt mode replacement for _make_wipe_tower(): plan filament-change purging +// into the belt purge prism (and any other flush_into_* object) using the +// flush-into-objects machinery, without generating classic wipe tower G-code. +// The toolchange itself is emitted by GCode::set_extruder() via the +// change_filament_gcode macro; the overrides marked here make the new +// filament's first extrusions land in the purge prism. +void Print::_plan_belt_purge() +{ + m_wipe_tower_data.clear(); + + // Must run before ToolOrdering is built: LayerTools merge per-object layer + // print_z values, and the prism only absorbs purge where its (snapped) + // layers coincide with the toolchange layers. + this->_align_belt_purge_layers(); + + const unsigned int number_of_extruders = (unsigned int) m_config.filament_colour.values.size(); + + // No initial priming extrusions: there is no tower to prime on. + m_wipe_tower_data.tool_ordering = ToolOrdering(*this, (unsigned int) -1, false); + m_wipe_tower_data.tool_ordering.sort_and_build_data(*this, (unsigned int) -1, false); + + if (m_wipe_tower_data.tool_ordering.empty() || m_wipe_tower_data.tool_ordering.last_extruder() == unsigned(-1)) + throw Slic3r::SlicingError("The print is empty. The model is not printable with current print settings."); + + if (!m_wipe_tower_data.tool_ordering.has_wipe_tower()) + // No toolchanges anywhere, nothing to purge. + return; + + this->throw_if_canceled(); + + // Flush volumes per filament pair, mirroring the generic wipe tower path: + // full flush matrix for single extruder multi material with purging enabled, + // plain prime volume otherwise. + std::vector flush_matrix(cast( + get_flush_volumes_matrix(m_config.flush_volumes_matrix.values, 0, m_config.nozzle_diameter.values.size()))); + std::vector> wipe_volumes; + for (unsigned int i = 0; i < number_of_extruders; ++i) + wipe_volumes.push_back(std::vector(flush_matrix.begin() + i * number_of_extruders, + flush_matrix.begin() + (i + 1) * number_of_extruders)); + const bool use_flush_matrix = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material; + const float flush_multiplier = (float) m_config.flush_multiplier.get_at(0); + + float total_leftover = 0.f; + float worst_layer_leftover = 0.f; + double worst_layer_z = 0.; + + unsigned int current_extruder_id = m_wipe_tower_data.tool_ordering.first_extruder(); + for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) { + float layer_leftover = 0.f; + for (const unsigned int extruder_id : layer_tools.extruders) { + if (extruder_id == current_extruder_id) + continue; + float volume_to_wipe = use_flush_matrix ? + wipe_volumes[current_extruder_id][extruder_id] * flush_multiplier : + (float) m_config.prime_volume; + float leftover = layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_extruder_id, extruder_id, + volume_to_wipe); + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] purge toolchange print_z=" << layer_tools.print_z + << " filament " << current_extruder_id << "->" << extruder_id + << " requested=" << volume_to_wipe + << " absorbed=" << volume_to_wipe - leftover + << " leftover=" << leftover; + layer_leftover += leftover; + current_extruder_id = extruder_id; + } + layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this); + if (layer_leftover > 0.f) { + total_leftover += layer_leftover; + if (layer_leftover > worst_layer_leftover) { + worst_layer_leftover = layer_leftover; + worst_layer_z = layer_tools.print_z; + } + } + this->throw_if_canceled(); + } + + if (total_leftover > 1.f) { + this->active_step_add_warning( + PrintStateBase::WarningLevel::CRITICAL, + Slic3r::format(_u8L("The belt purge tower cannot absorb the full purge volume: %1% mm³ in total could not " + "be purged (worst layer: %2% mm³ at height %3%). The print may show color bleeding. " + "Increase the belt purge tower width, or reduce flushing volumes."), + int(std::ceil(total_leftover)), int(std::ceil(worst_layer_leftover)), + Slic3r::float_to_string_decimal_point(worst_layer_z, 2))); + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge planning leftover total=" << total_leftover + << " worst_layer=" << worst_layer_leftover << " at print_z=" << worst_layer_z; + } +} + void Print::_make_wipe_tower() { m_wipe_tower_data.clear(); diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index ff5a505360..fc78c5efef 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -558,6 +558,10 @@ private: std::vector> detect_extruder_geometric_unprintables() const; void slice_volumes(); + // Belt mode: shift the sliced layer grid (layer print_z and the belt floor + // clipping shift) by delta, used by Print::_align_belt_purge_layers() to + // snap the purge prism's layers onto the printed objects' layer grid. + void belt_shift_layer_grid(double delta); //BBS ExPolygons _shrink_contour_holes(double contour_delta, double hole_delta, const ExPolygons& polys) const; // BBS @@ -1085,6 +1089,8 @@ public: // Wipe tower support. bool has_wipe_tower() const; + // Belt purge prism (belt-mode replacement for the wipe tower). + bool has_belt_purge_tower() const; const WipeTowerData& wipe_tower_data(size_t filaments_cnt = 0) const; const ToolOrdering& tool_ordering() const { return m_tool_ordering; } @@ -1330,6 +1336,12 @@ private: void _make_skirt(); void _make_wipe_tower(); + // Belt mode: route filament-change purging into the belt purge prism + // (flush-into-objects) without generating a classic wipe tower. + void _plan_belt_purge(); + // Belt mode: shift the purge prism's layer grid to match the printed + // objects' grid so toolchange layers find prism layers to purge into. + void _align_belt_purge_layers(); void finalize_first_layer_convex_hull(); void update_filament_self_index_cache(); // Deduplicates, per filament, the (extruder type x volume type) variants the grouping diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index d0ce14ec35..cef9a840a8 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -7321,6 +7321,20 @@ void PrintConfigDef::init_fff_params() def->set_default_value(new ConfigOptionEnum(BeltSupportZOffsetMode::Unconditional)); } + def = this->add("belt_purge_tower_width", coFloat); + def->label = L("Belt purge tower width"); + def->category = L("Printable space"); + def->tooltip = L("Width (machine X, across the belt) of the purge prism that is automatically " + "generated on belt printers when the prime tower is enabled and multiple " + "filaments are used. Filament-change purging is routed into this prism's " + "extrusions instead of a classic wipe tower. Its height is computed " + "automatically from the worst-case purge volume per layer: a wider prism " + "results in a shorter one."); + def->sidetext = L("mm"); + def->min = 1.; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloat(35.)); + def = this->add("tree_support_branch_angle", coFloat); def->label = L("Tree support branch angle"); def->category = L("Support"); @@ -7966,6 +7980,16 @@ void PrintConfigDef::init_fff_params() "It will not take effect unless the prime tower is enabled."); def->set_default_value(new ConfigOptionBool(false)); + // Internal marker (not shown in any settings tab): identifies the auto-generated + // belt purge prism so it can be updated/removed by the auto-manager and aligned + // to the object layer grid by the backend. Persisted to 3mf like any per-object key. + def = this->add("belt_purge_tower_object", coBool); + def->category = L("Flush options"); + def->label = L("Belt purge tower object"); + def->tooltip = L("Marks the auto-generated belt purge prism. Managed automatically; do not set manually."); + def->mode = comDevelop; + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("wipe_tower_bridging", coFloat); def->label = L("Maximal bridging distance"); def->tooltip = L("Maximal distance between supports on sparse infill sections."); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 3342a54b6d..ffd0320149 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1228,6 +1228,9 @@ PRINT_CONFIG_CLASS_DEFINE( // BBS ((ConfigOptionBool, flush_into_infill)) ((ConfigOptionBool, flush_into_support)) + // Marker for the auto-generated belt purge prism; identifies the object to + // the auto-manager (GUI) and the layer-grid alignment step (backend). + ((ConfigOptionBool, belt_purge_tower_object)) // BBS ((ConfigOptionFloat, tree_support_branch_distance)) ((ConfigOptionFloat, tree_support_tip_diameter)) @@ -1802,6 +1805,8 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionFloat, belt_support_floor_offset)) ((ConfigOptionEnum, belt_support_floor_mode)) ((ConfigOptionEnum, belt_support_z_offset_mode)) + // Width (machine X, across the belt) of the auto-generated belt purge prism. + ((ConfigOptionFloat, belt_purge_tower_width)) //BBS ((ConfigOptionInts, additional_cooling_fan_speed)) ((ConfigOptionInts, close_additional_fan_first_x_layers)) diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 68cedfc534..cd0bc34a8e 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1663,7 +1663,8 @@ bool PrintObject::invalidate_state_by_config_options( } else if ( opt_key == "flush_into_infill" || opt_key == "flush_into_objects" - || opt_key == "flush_into_support") { + || opt_key == "flush_into_support" + || opt_key == "belt_purge_tower_object") { invalidated |= m_print->invalidate_step(psWipeTower); invalidated |= m_print->invalidate_step(psGCodeExport); } else { diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index 1212100eac..b95bc4903f 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1392,6 +1392,27 @@ void apply_fuzzy_skin_segmentation(PrintObject &print_object, ThrowOnCancel thro }); // end of parallel_for } +// Belt mode: shift the sliced layer grid by delta. Mirrors the global_z_offset +// application in slice() — layer print_z and belt_floor_z_shift move together +// so belt floor clipping stays consistent with the shifted grid. Used by +// Print::_align_belt_purge_layers() to snap the purge prism onto the printed +// objects' layer grid; |delta| <= half a layer height, i.e. a sub-layer shift +// of the prism along the belt. +void PrintObject::belt_shift_layer_grid(double delta) +{ + if (std::abs(delta) < EPSILON) + return; + for (Layer *layer : m_layers) + layer->print_z += delta; + for (SupportLayer *layer : m_support_layers) + layer->print_z += delta; + m_slicing_params.belt_floor_z_shift += delta; + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] belt_shift_layer_grid" + << " obj=" << this->model_object()->name + << " delta=" << delta + << " first_layer.print_z=" << (m_layers.empty() ? 0. : m_layers.front()->print_z); +} + // 1) Decides Z positions of the layers, // 2) Initializes layers and their regions // 3) Slices the object meshes diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 3fc69d6e4b..ffd4d43a71 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -971,11 +971,16 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in toggle_line("preheat_steps", have_ooze_prevention && (preheat_steps > 0)); bool have_prime_tower = config->opt_bool("enable_prime_tower"); + // ORCA-Belt: belt printers replace the classic wipe tower with the + // auto-generated belt purge prism — only its width is tunable; the + // classic tower geometry options do not apply. (is_belt_printer is + // computed at the top of this function.) + toggle_line("belt_purge_tower_width", have_prime_tower && is_belt_printer); for (auto el : {"prime_tower_width", "prime_tower_brim_width", "prime_tower_skip_points", "wipe_tower_wall_type", "prime_tower_infill_gap","prime_tower_enable_framework", "enable_tower_interface_features"}) - toggle_line(el, have_prime_tower); + toggle_line(el, have_prime_tower && !is_belt_printer); toggle_line("enable_tower_interface_cooldown_during_tower", - have_prime_tower && config->opt_bool("enable_tower_interface_features")); + have_prime_tower && !is_belt_printer && config->opt_bool("enable_tower_interface_features")); bool purge_in_primetower = preset_bundle->printers.get_edited_preset().config.opt_bool("purge_in_prime_tower"); @@ -983,17 +988,17 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in "wipe_tower_extra_spacing", "wipe_tower_max_purge_speed", "wipe_tower_bridging", "wipe_tower_extra_flow", "wipe_tower_no_sparse_layers"}) - toggle_line(el, have_prime_tower && supports_wipe_tower_2); + toggle_line(el, have_prime_tower && supports_wipe_tower_2 && !is_belt_printer); WipeTowerWallType wipe_tower_wall_type = config->opt_enum("wipe_tower_wall_type"); - bool have_rib_wall = (wipe_tower_wall_type == WipeTowerWallType::wtwRib)&&have_prime_tower; - toggle_line("wipe_tower_cone_angle", have_prime_tower && supports_wipe_tower_2 && wipe_tower_wall_type == WipeTowerWallType::wtwCone); + bool have_rib_wall = (wipe_tower_wall_type == WipeTowerWallType::wtwRib)&&have_prime_tower&&!is_belt_printer; + toggle_line("wipe_tower_cone_angle", have_prime_tower && supports_wipe_tower_2 && !is_belt_printer && wipe_tower_wall_type == WipeTowerWallType::wtwCone); toggle_line("wipe_tower_extra_rib_length", have_rib_wall); toggle_line("wipe_tower_rib_width", have_rib_wall); toggle_line("wipe_tower_fillet_wall", have_rib_wall); toggle_field("prime_tower_width", have_prime_tower && !have_rib_wall); - toggle_line("single_extruder_multi_material_priming", !bSEMM && have_prime_tower && supports_wipe_tower_2); + toggle_line("single_extruder_multi_material_priming", !bSEMM && have_prime_tower && supports_wipe_tower_2 && !is_belt_printer); toggle_line("prime_volume",have_prime_tower && (!purge_in_primetower || !bSEMM)); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index e3e84bc1c7..8ff05862a8 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2866,7 +2866,13 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re need_wipe_tower |= dynamic_cast(dconfig.option("enable_wrapping_detection"))->value; } - if (wt && (need_wipe_tower || filaments_count > 1) && !wxGetApp().plater()->only_gcode_mode() && !wxGetApp().plater()->is_gcode_3mf()) { + // Belt printers replace the classic wipe tower with the auto-generated + // belt purge prism (a real model object), so never draw the tower widget. + bool is_belt_printer = false; + if (const auto *belt_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("belt_printer")) + is_belt_printer = belt_opt->value; + + if (wt && !is_belt_printer && (need_wipe_tower || filaments_count > 1) && !wxGetApp().plater()->only_gcode_mode() && !wxGetApp().plater()->is_gcode_3mf()) { for (int plate_id = 0; plate_id < n_plates; plate_id++) { // If print ByObject and there is only one object in the plate, the wipe tower is allowed to be generated. PartPlate* part_plate = ppl.get_plate(plate_id); diff --git a/src/slic3r/GUI/Jobs/ArrangeJob.cpp b/src/slic3r/GUI/Jobs/ArrangeJob.cpp index 6b74b71af1..4e143de7d8 100644 --- a/src/slic3r/GUI/Jobs/ArrangeJob.cpp +++ b/src/slic3r/GUI/Jobs/ArrangeJob.cpp @@ -291,6 +291,12 @@ void ArrangeJob::prepare_wipe_tower() bool enable_prime_tower = op && op->getBool(); if (!enable_prime_tower || params.is_seq_print) return; + // Belt printers have no classic wipe tower; purging goes into the belt + // purge prism, which is a real model object and arranges like any other. + if (const auto *belt_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("belt_printer"); + belt_opt && belt_opt->value) + return; + bool smooth_timelapse = false; auto sop = current_config.option("timelapse_type"); if (sop) { smooth_timelapse = sop->getInt() == TimelapseType::tlSmooth; } diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 365384ecf9..d03c389dc7 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -5475,6 +5475,10 @@ struct Plater::priv void exit_gizmo(); void remove(size_t obj_idx); bool delete_object_from_model(size_t obj_idx, bool refresh_immediately = true); //BBS + // ORCA-Belt: keep the auto-generated belt purge prism in sync with the + // config and plate contents. Returns true when the model was mutated + // (caller should refresh the scene). + bool ensure_belt_purge_tower(); void delete_all_objects_from_model(); void reset(bool apply_presets_change = false); void center_selection(); @@ -8879,6 +8883,225 @@ void Plater::priv::process_validation_warnings(const std::vectorconfig.option("belt_purge_tower_object"); + return opt != nullptr && opt->getBool(); + }; + + std::vector prism_idxs; + for (int i = 0; i < (int) model.objects.size(); ++i) + if (is_prism(model.objects[i])) + prism_idxs.push_back(i); + + // Deletes prism objects, keeping the sidebar and part plates in sync + // (same primitives as Plater::priv::remove(), minus scene update — the + // caller refreshes the scene). + auto remove_prisms = [this](const std::vector &idxs) { + for (auto it = idxs.rbegin(); it != idxs.rend(); ++it) { + model.delete_object(size_t(*it)); + partplate_list.notify_instance_removed(*it, -1); + sidebar->obj_list()->delete_object_from_list(size_t(*it)); + } + }; + + const auto &printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; + const auto &print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto &project_config = wxGetApp().preset_bundle->project_config; + + const auto *belt_opt = printer_config.option("belt_printer"); + const bool belt = belt_opt != nullptr && belt_opt->value; + const bool prime_tower_enabled = print_config.has("enable_prime_tower") && print_config.opt_bool("enable_prime_tower"); + const auto *seq_opt = print_config.option>("print_sequence"); + const bool by_object = seq_opt != nullptr && seq_opt->value == PrintSequence::ByObject; + + // Filaments used and bounding extent of the non-prism objects on the + // current plate (1-based filament ids; volume extruder 0 = object default). + PartPlate *plate = partplate_list.get_curr_plate(); + std::set filaments; + double y_min = std::numeric_limits::max(); + double y_max = -std::numeric_limits::max(); + double z_max = 0.; + bool have_objects = false; + if (belt && plate != nullptr) { + for (int obj_idx = 0; obj_idx < (int) model.objects.size(); ++obj_idx) { + const ModelObject *mo = model.objects[obj_idx]; + if (is_prism(mo)) + continue; + int obj_extruder = 1; + if (const ConfigOption *opt = mo->config.option("extruder"); opt != nullptr && opt->getInt() > 0) + obj_extruder = opt->getInt(); + bool any_instance_on_plate = false; + for (int inst_idx = 0; inst_idx < (int) mo->instances.size(); ++inst_idx) { + if (!plate->contain_instance_totally(obj_idx, inst_idx)) + continue; + any_instance_on_plate = true; + const BoundingBoxf3 bb = mo->instance_bounding_box(inst_idx); + y_min = std::min(y_min, bb.min.y()); + y_max = std::max(y_max, bb.max.y()); + z_max = std::max(z_max, bb.max.z()); + } + if (!any_instance_on_plate) + continue; + have_objects = true; + for (const ModelVolume *mv : mo->volumes) + for (int e : mv->get_extruders()) + filaments.insert(e > 0 ? e : obj_extruder); + } + } + + const bool wanted = belt && prime_tower_enabled && !by_object && have_objects && filaments.size() > 1; + if (!wanted) { + if (prism_idxs.empty()) + return false; + remove_prisms(prism_idxs); + BOOST_LOG_TRIVIAL(info) << "[BELT-DEBUG] belt purge tower removed (conditions not met)"; + return true; + } + + // --- Sizing ----------------------------------------------------------- + const double width = print_config.has("belt_purge_tower_width") ? std::max(1., print_config.opt_float("belt_purge_tower_width")) : 35.; + const double layer_h = print_config.has("layer_height") ? print_config.opt_float("layer_height") : 0.2; + + // Belt tilt: standard belt printers rotate about X. For other axes (or no + // rotation) fall back to vertical slicing geometry (theta = 90 deg); the + // backend leftover warning catches any resulting under-absorption. + double theta = M_PI / 2.; + const auto *axis_opt = printer_config.option>("belt_slice_rotation"); + const auto *angle_opt = printer_config.option("belt_slice_rotation_angle"); + if (axis_opt != nullptr && axis_opt->value == BeltRotationAxis::X && angle_opt != nullptr && std::abs(angle_opt->value) > EPSILON) + theta = std::clamp(Geometry::deg2rad(std::abs(angle_opt->value)), Geometry::deg2rad(5.), M_PI / 2.); + const double sin_t = std::sin(theta); + const double cot_t = std::cos(theta) / sin_t; + + // Worst-case purge volume of one layer: up to (filament count - 1) + // toolchanges, each needing the worst flush matrix entry (mirrors the + // volume selection in Print::_plan_belt_purge()). + const bool use_matrix = (print_config.has("purge_in_prime_tower") && print_config.opt_bool("purge_in_prime_tower")) + && (printer_config.has("single_extruder_multi_material") && printer_config.opt_bool("single_extruder_multi_material")); + double max_flush = print_config.has("prime_volume") ? print_config.opt_float("prime_volume") : 45.; + if (use_matrix) { + const size_t extruder_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); + const std::vector matrix = get_flush_volumes_matrix( + project_config.option("flush_volumes_matrix")->values, 0, extruder_nums); + const auto * multi_opt = project_config.option("flush_multiplier"); + const double multiplier = multi_opt != nullptr && !multi_opt->values.empty() ? multi_opt->get_at(0) : 1.; + const int n_total = (int) (std::sqrt(double(matrix.size())) + 0.5); + double m = 0.; + for (int i : filaments) + for (int j : filaments) + if (i != j && i <= n_total && j <= n_total) + m = std::max(m, matrix[size_t(i - 1) * n_total + size_t(j - 1)]); + if (m > 0.) + max_flush = m * multiplier; + } + const double v_layer = double(filaments.size() - 1) * max_flush; + + // One tilted slicing plane cuts a width x (height/sin) rectangle out of + // the prism interior; one layer slab absorbs that area x layer height. + const double eta = 0.85; // perimeters/infill packing safety factor + double height = v_layer * sin_t / (width * layer_h * eta); + const double printable_height = printer_config.has("printable_height") ? printer_config.opt_float("printable_height") : 250.; + height = std::clamp(height, 2. * layer_h, std::max(2. * layer_h, printable_height)); + + // Length along the belt: cover the objects' Y extent; a slicing plane + // spans height*cot(theta) of belt travel, so pad both ends with the full + // lead-in (tilt sign is padded symmetrically) plus the objects' top + // overhang along the belt. + const double margin = 10.; + const double lead = height * cot_t; + double y_start = y_min - lead - margin; + double y_end = y_max + (z_max + height) * cot_t + margin; + + const Vec3d plate_origin = plate->get_origin(); + const auto *bed_opt = printer_config.option("printable_area"); + const bool infinite_y = printer_config.has("belt_printer_infinite_y") && printer_config.opt_bool("belt_printer_infinite_y"); + double lane_x = plate_origin.x() + 5.; // fallback left lane + if (bed_opt != nullptr && !bed_opt->values.empty()) { + const BoundingBoxf bed_ext = get_extents(bed_opt->values); + lane_x = plate_origin.x() + bed_ext.min.x() + 5.; + if (!infinite_y) { + y_start = std::max(y_start, plate_origin.y() + bed_ext.min.y() + 1.); + y_end = std::min(y_end, plate_origin.y() + bed_ext.max.y() - 1.); + } + } + const double length = std::max(y_end - y_start, 10.); + + const Vec3d desired_size(width, length, height); + const Vec3d desired_center(lane_x + 0.5 * width, y_start + 0.5 * length, 0.5 * height); + + // --- Idempotence check ------------------------------------------------- + // Coarse tolerances so object nudges and sizing jitter do not regenerate + // the prism on every background-process tick. + if (prism_idxs.size() == 1) { + const ModelObject *prism = model.objects[prism_idxs.front()]; + const BoundingBoxf3 bb = prism->bounding_box_exact(); + if ((bb.size() - desired_size).cwiseAbs().maxCoeff() < 5. && (bb.center() - desired_center).cwiseAbs().maxCoeff() < 5.) + return false; + } + + // --- (Re)create --------------------------------------------------------- + if (!prism_idxs.empty()) + remove_prisms(prism_idxs); + + ModelObject *new_object = model.add_object(); + new_object->name = _u8L("Belt Purge Tower"); + new_object->add_instance(); + ModelVolume *new_volume = new_object->add_volume(make_cube(width, length, height)); + new_volume->name = new_object->name; + + auto &cfg = new_object->config; + cfg.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + cfg.set_key_value("flush_into_objects", new ConfigOptionBool(true)); + cfg.set_key_value("extruder", new ConfigOptionInt(1)); + // Sacrificial solid prism: one wall, no shells, dense rectilinear infill — + // every extrusion is overriddable, so the absorbed volume matches the + // cross-section x layer-height estimate used for the height above. + cfg.set_key_value("wall_loops", new ConfigOptionInt(1)); + cfg.set_key_value("top_shell_layers", new ConfigOptionInt(0)); + cfg.set_key_value("bottom_shell_layers", new ConfigOptionInt(0)); + cfg.set_key_value("sparse_infill_density", new ConfigOptionPercent(100)); + cfg.set_key_value("sparse_infill_pattern", new ConfigOptionEnum(ipRectilinear)); + cfg.set_key_value("enable_support", new ConfigOptionBool(false)); + cfg.set_key_value("brim_type", new ConfigOptionEnum(btNoBrim)); + cfg.set_key_value("seam_slope_type", new ConfigOptionEnum(SeamScarfType::None)); + cfg.set_key_value("precise_z_height", new ConfigOptionBool(false)); + + new_object->invalidate_bounding_box(); + const BoundingBoxf3 mesh_bb = new_volume->mesh().bounding_box(); + new_object->translate(-mesh_bb.center()); + new_object->instances.front()->set_offset(desired_center); + new_object->ensure_on_bed(); + new_object->instances.front()->set_assemble_transformation(new_object->instances.front()->get_transformation()); + + const size_t obj_idx = model.objects.size() - 1; + // Registers the object in the sidebar and notifies the part plates; + // selection is left untouched (auto-managed object). + sidebar->obj_list()->add_object_to_list(obj_idx, /*call_selection_changed=*/false); + + BOOST_LOG_TRIVIAL(info) << "[BELT-DEBUG] belt purge tower generated" + << " W=" << width << " L=" << length << " H=" << height + << " v_layer=" << v_layer << " max_flush=" << max_flush + << " filaments=" << filaments.size() + << " theta_deg=" << Geometry::rad2deg(theta) + << " center=(" << desired_center.x() << "," << desired_center.y() << ")"; + return true; +} + // Update background processing thread from the current config and Model. // Returns a bitmask of UpdateBackgroundProcessReturnState. unsigned int Plater::priv::update_background_process(bool force_validation, bool postpone_error_messages, bool switch_print) @@ -8889,6 +9112,10 @@ unsigned int Plater::priv::update_background_process(bool force_validation, bool // If the update_background_process() was not called by the timer, kill the timer, // so the update_restart_background_process() will not be called again in vain. background_process_timer.Stop(); + // ORCA-Belt: sync the auto-managed belt purge prism before the model is + // applied to the Print below, so the prism change rides this same apply. + if (printer_technology == ptFFF && this->ensure_belt_purge_tower()) + return_state |= UPDATE_BACKGROUND_PROCESS_REFRESH_SCENE; // Update the "out of print bed" state of ModelInstances. update_print_volume_state(); // Apply new config to the possibly running background task. diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 8de8bf6e84..25287c79c3 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -3009,6 +3009,7 @@ void TabPrint::build() optgroup->append_single_option_line("enable_tower_interface_cooldown_during_tower", "multimaterial_settings_prime_tower"); optgroup->append_single_option_line("prime_tower_enable_framework", "multimaterial_settings_prime_tower"); optgroup->append_single_option_line("prime_tower_width", "multimaterial_settings_prime_tower#width"); + optgroup->append_single_option_line("belt_purge_tower_width", "multimaterial_settings_prime_tower"); optgroup->append_single_option_line("prime_volume", "multimaterial_settings_prime_tower"); optgroup->append_single_option_line("prime_tower_brim_width", "multimaterial_settings_prime_tower#brim-width"); optgroup->append_single_option_line("prime_tower_infill_gap", "multimaterial_settings_prime_tower"); From ec4e9717d3bdc17a0cbf1db3312497fe0e16131a Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 05:30:25 -0500 Subject: [PATCH 02/15] Part Two: Functional Results --- src/slic3r/GUI/Plater.cpp | 134 ++++++++++++++++++++++++++------------ 1 file changed, 92 insertions(+), 42 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index d03c389dc7..cda0a1557e 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -5479,6 +5479,21 @@ struct Plater::priv // config and plate contents. Returns true when the model was mutated // (caller should refresh the scene). bool ensure_belt_purge_tower(); + // Inputs of the last belt purge prism generation. Idempotence is keyed on + // these (not the prism's resulting bbox): the prism gets nudged by plate + // assignment after creation, so comparing its bbox would falsely detect a + // change and regenerate it on every background-process tick — which would + // re-invalidate a freshly sliced result and make the preview unreachable. + struct BeltPurgeSignature { + bool valid = false; + int filament_count = 0; + long key[7] = {0}; // rounded geometry inputs (0.1 mm units) + bool operator==(const BeltPurgeSignature &o) const { + if (valid != o.valid || filament_count != o.filament_count) return false; + for (int i = 0; i < 7; ++i) if (key[i] != o.key[i]) return false; + return true; + } + } m_belt_purge_sig; void delete_all_objects_from_model(); void reset(bool apply_presets_change = false); void center_selection(); @@ -8933,6 +8948,8 @@ bool Plater::priv::ensure_belt_purge_tower() // current plate (1-based filament ids; volume extruder 0 = object default). PartPlate *plate = partplate_list.get_curr_plate(); std::set filaments; + double x_min = std::numeric_limits::max(); + double x_max = -std::numeric_limits::max(); double y_min = std::numeric_limits::max(); double y_max = -std::numeric_limits::max(); double z_max = 0.; @@ -8951,6 +8968,8 @@ bool Plater::priv::ensure_belt_purge_tower() continue; any_instance_on_plate = true; const BoundingBoxf3 bb = mo->instance_bounding_box(inst_idx); + x_min = std::min(x_min, bb.min.x()); + x_max = std::max(x_max, bb.max.x()); y_min = std::min(y_min, bb.min.y()); y_max = std::max(y_max, bb.max.y()); z_max = std::max(z_max, bb.max.z()); @@ -8966,10 +8985,11 @@ bool Plater::priv::ensure_belt_purge_tower() const bool wanted = belt && prime_tower_enabled && !by_object && have_objects && filaments.size() > 1; if (!wanted) { + m_belt_purge_sig = BeltPurgeSignature{}; if (prism_idxs.empty()) return false; remove_prisms(prism_idxs); - BOOST_LOG_TRIVIAL(info) << "[BELT-DEBUG] belt purge tower removed (conditions not met)"; + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] belt purge tower removed (conditions not met)"; return true; } @@ -8977,17 +8997,27 @@ bool Plater::priv::ensure_belt_purge_tower() const double width = print_config.has("belt_purge_tower_width") ? std::max(1., print_config.opt_float("belt_purge_tower_width")) : 35.; const double layer_h = print_config.has("layer_height") ? print_config.opt_float("layer_height") : 0.2; - // Belt tilt: standard belt printers rotate about X. For other axes (or no - // rotation) fall back to vertical slicing geometry (theta = 90 deg); the - // backend leftover warning catches any resulting under-absorption. - double theta = M_PI / 2.; - const auto *axis_opt = printer_config.option>("belt_slice_rotation"); + // Belt geometry. The rotation axis is the gantry tilt axis; the belt + // travels along the *other* horizontal axis (X-rotation -> belt along Y, + // the CR-30 default). The purge prism is a long bar laid along the belt + // travel direction, beside the parts. For no/Z rotation we fall back to + // vertical slicing geometry (theta = 90 deg). + const auto *axis_opt = printer_config.option>("belt_slice_rotation"); const auto *angle_opt = printer_config.option("belt_slice_rotation_angle"); - if (axis_opt != nullptr && axis_opt->value == BeltRotationAxis::X && angle_opt != nullptr && std::abs(angle_opt->value) > EPSILON) + const BeltRotationAxis rot = axis_opt != nullptr ? axis_opt->value : BeltRotationAxis::X; + const bool belt_is_y = (rot != BeltRotationAxis::Y); // X / None / Z -> belt along Y + double theta = M_PI / 2.; + if ((rot == BeltRotationAxis::X || rot == BeltRotationAxis::Y) && angle_opt != nullptr && std::abs(angle_opt->value) > EPSILON) theta = std::clamp(Geometry::deg2rad(std::abs(angle_opt->value)), Geometry::deg2rad(5.), M_PI / 2.); const double sin_t = std::sin(theta); const double cot_t = std::cos(theta) / sin_t; + // Parts' extent along the belt-travel axis and the lateral (across-belt) axis. + const double belt_min = belt_is_y ? y_min : x_min; + const double belt_max = belt_is_y ? y_max : x_max; + const double lat_min = belt_is_y ? x_min : y_min; + const double lat_max = belt_is_y ? x_max : y_max; + // Worst-case purge volume of one layer: up to (filament count - 1) // toolchanges, each needing the worst flush matrix entry (mirrors the // volume selection in Print::_plan_belt_purge()). @@ -9018,41 +9048,56 @@ bool Plater::priv::ensure_belt_purge_tower() const double printable_height = printer_config.has("printable_height") ? printer_config.opt_float("printable_height") : 250.; height = std::clamp(height, 2. * layer_h, std::max(2. * layer_h, printable_height)); - // Length along the belt: cover the objects' Y extent; a slicing plane - // spans height*cot(theta) of belt travel, so pad both ends with the full - // lead-in (tilt sign is padded symmetrically) plus the objects' top - // overhang along the belt. - const double margin = 10.; - const double lead = height * cot_t; - double y_start = y_min - lead - margin; - double y_end = y_max + (z_max + height) * cot_t + margin; + // --- Idempotence (input-keyed) ---------------------------------------- + // Key on the generation inputs, NOT the prism's resulting bbox: plate + // assignment nudges the prism after creation, so a bbox comparison sees a + // "change" every tick and regenerates the prism, which re-invalidates any + // freshly sliced result and makes the G-code preview unreachable. + auto q = [](double v) { return std::lround(v * 10.0); }; // 0.1 mm quantization + BeltPurgeSignature sig; + sig.valid = true; + sig.filament_count = (int) filaments.size(); + sig.key[0] = q(width); + sig.key[1] = q(layer_h); + sig.key[2] = q(height); + sig.key[3] = q(belt_min); + sig.key[4] = q(belt_max); + sig.key[5] = q(lat_min); + sig.key[6] = q(z_max); + if (prism_idxs.size() == 1 && sig == m_belt_purge_sig) + return false; // already up to date — do not touch the model - const Vec3d plate_origin = plate->get_origin(); - const auto *bed_opt = printer_config.option("printable_area"); - const bool infinite_y = printer_config.has("belt_printer_infinite_y") && printer_config.opt_bool("belt_printer_infinite_y"); - double lane_x = plate_origin.x() + 5.; // fallback left lane - if (bed_opt != nullptr && !bed_opt->values.empty()) { - const BoundingBoxf bed_ext = get_extents(bed_opt->values); - lane_x = plate_origin.x() + bed_ext.min.x() + 5.; - if (!infinite_y) { - y_start = std::max(y_start, plate_origin.y() + bed_ext.min.y() + 1.); - y_end = std::min(y_end, plate_origin.y() + bed_ext.max.y() - 1.); - } + // --- Position ---------------------------------------------------------- + // Lay the bar along the belt, anchored at the parts' belt-axis minimum so + // it leads the print at the belt entry. A tilted slicing plane through a + // part of height z reaches belt position y + z*cot(theta), so extend the + // trailing end to cover the tallest part's upper toolchanges plus the + // prism's own tilt span. + const double margin = 5.; + const double belt_start = belt_min - margin; // anchored at parts' belt minimum + const double belt_end = belt_max + (z_max + height) * cot_t + margin; // trailing tilt allowance + const double length = std::max(belt_end - belt_start, 10.); + const double belt_center = 0.5 * (belt_start + belt_end); + + // Lateral placement: just beside the parts (the -lateral side by default, + // flipping to the +lateral side if that would fall off the bed). + const Vec3d plate_origin = plate->get_origin(); + const double gap = 5.; + double lat_center = lat_min - gap - 0.5 * width; + if (const auto *bed_opt = printer_config.option("printable_area"); + bed_opt != nullptr && !bed_opt->values.empty()) { + const BoundingBoxf bed_ext = get_extents(bed_opt->values); + const double bed_lat_min = plate_origin[belt_is_y ? 0 : 1] + (belt_is_y ? bed_ext.min.x() : bed_ext.min.y()); + if (lat_center - 0.5 * width < bed_lat_min) + lat_center = lat_max + gap + 0.5 * width; // not enough room on the -lateral side } - const double length = std::max(y_end - y_start, 10.); - const Vec3d desired_size(width, length, height); - const Vec3d desired_center(lane_x + 0.5 * width, y_start + 0.5 * length, 0.5 * height); - - // --- Idempotence check ------------------------------------------------- - // Coarse tolerances so object nudges and sizing jitter do not regenerate - // the prism on every background-process tick. - if (prism_idxs.size() == 1) { - const ModelObject *prism = model.objects[prism_idxs.front()]; - const BoundingBoxf3 bb = prism->bounding_box_exact(); - if ((bb.size() - desired_size).cwiseAbs().maxCoeff() < 5. && (bb.center() - desired_center).cwiseAbs().maxCoeff() < 5.) - return false; - } + // Orient the bar: long axis = belt travel, width = lateral, height = Z. + const double size_x = belt_is_y ? width : length; + const double size_y = belt_is_y ? length : width; + const Vec3d desired_center(belt_is_y ? lat_center : belt_center, + belt_is_y ? belt_center : lat_center, + 0.5 * height); // --- (Re)create --------------------------------------------------------- if (!prism_idxs.empty()) @@ -9061,7 +9106,7 @@ bool Plater::priv::ensure_belt_purge_tower() ModelObject *new_object = model.add_object(); new_object->name = _u8L("Belt Purge Tower"); new_object->add_instance(); - ModelVolume *new_volume = new_object->add_volume(make_cube(width, length, height)); + ModelVolume *new_volume = new_object->add_volume(make_cube(size_x, size_y, height)); new_volume->name = new_object->name; auto &cfg = new_object->config; @@ -9093,12 +9138,17 @@ bool Plater::priv::ensure_belt_purge_tower() // selection is left untouched (auto-managed object). sidebar->obj_list()->add_object_to_list(obj_idx, /*call_selection_changed=*/false); - BOOST_LOG_TRIVIAL(info) << "[BELT-DEBUG] belt purge tower generated" + // Record the inputs that produced this prism so subsequent ticks are no-ops + // until the parts/config actually change. + m_belt_purge_sig = sig; + + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] belt purge tower generated" + << " belt_is_y=" << belt_is_y << " W=" << width << " L=" << length << " H=" << height << " v_layer=" << v_layer << " max_flush=" << max_flush << " filaments=" << filaments.size() << " theta_deg=" << Geometry::rad2deg(theta) - << " center=(" << desired_center.x() << "," << desired_center.y() << ")"; + << " center=(" << desired_center.x() << "," << desired_center.y() << "," << desired_center.z() << ")"; return true; } From 854dae8dd24ccd5e815b5aea1837b346bcb31215 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 06:04:10 -0500 Subject: [PATCH 03/15] strategy incremental --- src/libslic3r/Print.cpp | 27 ++++++++++++++++++++++ src/slic3r/GUI/Plater.cpp | 48 +++++++++++++++++++++++---------------- 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 390bba1ff2..b5d3701e41 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -4345,6 +4345,21 @@ void Print::_plan_belt_purge() const bool use_flush_matrix = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material; const float flush_multiplier = (float) m_config.flush_multiplier.get_at(0); + // Diagnostic: the prism only absorbs purge at toolchange layers whose + // print_z coincides with one of its own layers. Compare the prism's layer + // print_z range to the toolchange print_z range and count how many + // toolchange layers actually land on a prism layer. This distinguishes a + // range/grid-alignment failure (no coverage) from a capacity shortfall + // (covered but not enough cross-section). + const PrintObject *diag_prism = nullptr; + for (const PrintObject *po : m_objects) + if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { diag_prism = po; break; } + if (diag_prism != nullptr) + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge prism layer range print_z=[" + << diag_prism->layers().front()->print_z << ", " << diag_prism->layers().back()->print_z + << "] nlayers=" << diag_prism->layers().size(); + int tc_layers = 0, tc_layers_covered = 0; + float total_leftover = 0.f; float worst_layer_leftover = 0.f; double worst_layer_z = 0.; @@ -4352,9 +4367,16 @@ void Print::_plan_belt_purge() unsigned int current_extruder_id = m_wipe_tower_data.tool_ordering.first_extruder(); for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) { float layer_leftover = 0.f; + bool layer_has_tc = false; for (const unsigned int extruder_id : layer_tools.extruders) { if (extruder_id == current_extruder_id) continue; + if (!layer_has_tc) { + layer_has_tc = true; + ++tc_layers; + if (diag_prism != nullptr && diag_prism->get_layer_at_printz(layer_tools.print_z, EPSILON) != nullptr) + ++tc_layers_covered; + } float volume_to_wipe = use_flush_matrix ? wipe_volumes[current_extruder_id][extruder_id] * flush_multiplier : (float) m_config.prime_volume; @@ -4379,6 +4401,11 @@ void Print::_plan_belt_purge() this->throw_if_canceled(); } + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge coverage: " << tc_layers_covered << "/" << tc_layers + << " toolchange layers land on a prism layer" + << (tc_layers > 0 && tc_layers_covered == 0 ? " (RANGE/GRID MISALIGNMENT — prism absorbs nothing)" : + tc_layers_covered < tc_layers ? " (partial coverage)" : " (full coverage)"); + if (total_leftover > 1.f) { this->active_step_add_warning( PrintStateBase::WarningLevel::CRITICAL, diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index cda0a1557e..6e3ccd6da3 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9041,12 +9041,19 @@ bool Plater::priv::ensure_belt_purge_tower() } const double v_layer = double(filaments.size() - 1) * max_flush; - // One tilted slicing plane cuts a width x (height/sin) rectangle out of - // the prism interior; one layer slab absorbs that area x layer height. - const double eta = 0.85; // perimeters/infill packing safety factor - double height = v_layer * sin_t / (width * layer_h * eta); + // Height from the per-layer purge demand. A tilted slicing plane cuts a + // width x (height/sin) rectangle out of the bar, so one layer slab absorbs + // width * (height/sin) * layer_height of purge. Solve for the height that + // holds the worst-case per-layer purge, with: + // eta - infill/perimeter packing (not all of the cross-section is solid) + // safety - margin for the tilt ramp at the bar ends and layer-grid + // alignment slop, where a layer cuts less than the full section + // and a minimum so the tower is a real printable body rather than a sliver. + const double eta = 0.85; + const double safety = 1.6; const double printable_height = printer_config.has("printable_height") ? printer_config.opt_float("printable_height") : 250.; - height = std::clamp(height, 2. * layer_h, std::max(2. * layer_h, printable_height)); + double height = safety * v_layer * sin_t / (width * layer_h * eta); + height = std::clamp(height, 8.0, std::max(8.0, printable_height)); // --- Idempotence (input-keyed) ---------------------------------------- // Key on the generation inputs, NOT the prism's resulting bbox: plate @@ -9068,28 +9075,31 @@ bool Plater::priv::ensure_belt_purge_tower() return false; // already up to date — do not touch the model // --- Position ---------------------------------------------------------- - // Lay the bar along the belt, anchored at the parts' belt-axis minimum so - // it leads the print at the belt entry. A tilted slicing plane through a - // part of height z reaches belt position y + z*cot(theta), so extend the - // trailing end to cover the tallest part's upper toolchanges plus the - // prism's own tilt span. + // Default belt-printer placement (per user request): + // - belt-travel axis: anchored at the parts' belt-axis MINIMUM (the belt + // entry / parts' min Y for an X-rotation belt) so the tower leads in. + // - across-belt axis: parked right up against the bed's MAXIMUM edge + // (max X for an X-rotation belt), out of the way of the parts. + // The bar extends from the parts' belt minimum past the parts; a tilted + // slicing plane through a part of height z reaches belt position + // y + z*cot(theta), so the trailing end covers the tallest part's upper + // toolchanges plus the bar's own tilt span. const double margin = 5.; - const double belt_start = belt_min - margin; // anchored at parts' belt minimum + const double belt_start = belt_min; // parts' belt-axis minimum const double belt_end = belt_max + (z_max + height) * cot_t + margin; // trailing tilt allowance const double length = std::max(belt_end - belt_start, 10.); const double belt_center = 0.5 * (belt_start + belt_end); - // Lateral placement: just beside the parts (the -lateral side by default, - // flipping to the +lateral side if that would fall off the bed). + // Across-belt: flush against the bed's maximum edge (small inset), falling + // back to just past the parts if the bed shape is unavailable. const Vec3d plate_origin = plate->get_origin(); - const double gap = 5.; - double lat_center = lat_min - gap - 0.5 * width; + const double inset = 1.; + double lat_center = lat_max + 5. + 0.5 * width; if (const auto *bed_opt = printer_config.option("printable_area"); bed_opt != nullptr && !bed_opt->values.empty()) { - const BoundingBoxf bed_ext = get_extents(bed_opt->values); - const double bed_lat_min = plate_origin[belt_is_y ? 0 : 1] + (belt_is_y ? bed_ext.min.x() : bed_ext.min.y()); - if (lat_center - 0.5 * width < bed_lat_min) - lat_center = lat_max + gap + 0.5 * width; // not enough room on the -lateral side + const BoundingBoxf bed_ext = get_extents(bed_opt->values); + const double bed_lat_max = plate_origin[belt_is_y ? 0 : 1] + (belt_is_y ? bed_ext.max.x() : bed_ext.max.y()); + lat_center = bed_lat_max - inset - 0.5 * width; } // Orient the bar: long axis = belt travel, width = lateral, height = Z. From 60e9ee26c9f8701abb25b0fa69c46419e45f9f50 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 06:27:42 -0500 Subject: [PATCH 04/15] strategy incremental 2 --- src/slic3r/GUI/Plater.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 6e3ccd6da3..0dd0210ba4 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9090,15 +9090,18 @@ bool Plater::priv::ensure_belt_purge_tower() const double length = std::max(belt_end - belt_start, 10.); const double belt_center = 0.5 * (belt_start + belt_end); - // Across-belt: flush against the bed's maximum edge (small inset), falling - // back to just past the parts if the bed shape is unavailable. - const Vec3d plate_origin = plate->get_origin(); + // Across-belt: flush against the bed's maximum edge, inset by half the bar + // width so the bar's far edge sits on the boundary and the whole bar stays + // on the bed (otherwise the center lands on the edge and half the bar hangs + // off → "objects laid over the boundary" → no slicing). Coordinates are in + // the printable_area / instance frame (do NOT add plate origin — the belt + // calibration objects are positioned the same way, see _calib_temp_belt_*). const double inset = 1.; - double lat_center = lat_max + 5. + 0.5 * width; + double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts if (const auto *bed_opt = printer_config.option("printable_area"); bed_opt != nullptr && !bed_opt->values.empty()) { const BoundingBoxf bed_ext = get_extents(bed_opt->values); - const double bed_lat_max = plate_origin[belt_is_y ? 0 : 1] + (belt_is_y ? bed_ext.max.x() : bed_ext.max.y()); + const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y(); lat_center = bed_lat_max - inset - 0.5 * width; } From 62d8f22f52d4fc04d2ed157f8911694d6cf961c6 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 07:02:14 -0500 Subject: [PATCH 05/15] purge tower still centered on X max --- src/slic3r/GUI/Plater.cpp | 47 +++++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 0dd0210ba4..d00396d97e 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9010,7 +9010,6 @@ bool Plater::priv::ensure_belt_purge_tower() if ((rot == BeltRotationAxis::X || rot == BeltRotationAxis::Y) && angle_opt != nullptr && std::abs(angle_opt->value) > EPSILON) theta = std::clamp(Geometry::deg2rad(std::abs(angle_opt->value)), Geometry::deg2rad(5.), M_PI / 2.); const double sin_t = std::sin(theta); - const double cot_t = std::cos(theta) / sin_t; // Parts' extent along the belt-travel axis and the lateral (across-belt) axis. const double belt_min = belt_is_y ? y_min : x_min; @@ -9075,36 +9074,46 @@ bool Plater::priv::ensure_belt_purge_tower() return false; // already up to date — do not touch the model // --- Position ---------------------------------------------------------- - // Default belt-printer placement (per user request): - // - belt-travel axis: anchored at the parts' belt-axis MINIMUM (the belt - // entry / parts' min Y for an X-rotation belt) so the tower leads in. - // - across-belt axis: parked right up against the bed's MAXIMUM edge - // (max X for an X-rotation belt), out of the way of the parts. - // The bar extends from the parts' belt minimum past the parts; a tilted - // slicing plane through a part of height z reaches belt position - // y + z*cot(theta), so the trailing end covers the tallest part's upper - // toolchanges plus the bar's own tilt span. + // Belt-travel axis: anchor the bar at the parts' belt-axis MINIMUM and span + // just the parts' belt extent (+ small margin). Filament changes happen + // over the parts' belt band, so the bar must sit there to absorb them; + // extending it far up the belt moves its body away from the toolchange + // layers and the purge goes unabsorbed (empirically, moving the tower + // toward the parts' Y-min clears the "cannot absorb" warning). const double margin = 5.; - const double belt_start = belt_min; // parts' belt-axis minimum - const double belt_end = belt_max + (z_max + height) * cot_t + margin; // trailing tilt allowance + const double belt_start = belt_min; // parts' belt-axis minimum + const double belt_end = belt_max + margin; // just past the parts — stay low / near the parts const double length = std::max(belt_end - belt_start, 10.); const double belt_center = 0.5 * (belt_start + belt_end); // Across-belt: flush against the bed's maximum edge, inset by half the bar // width so the bar's far edge sits on the boundary and the whole bar stays - // on the bed (otherwise the center lands on the edge and half the bar hangs - // off → "objects laid over the boundary" → no slicing). Coordinates are in - // the printable_area / instance frame (do NOT add plate origin — the belt - // calibration objects are positioned the same way, see _calib_temp_belt_*). - const double inset = 1.; - double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts + // on the bed. The bed (printable_area) is plate-local but model instances + // live in the plate's world frame, so add the plate origin's lateral + // component (plate-origin compensation is needed throughout the belt + // pipeline). lat_min/lat_max come from instance_bounding_box (world frame). + const Vec3d plate_origin = plate->get_origin(); + const double lat_origin = plate_origin[belt_is_y ? 0 : 1]; + const double inset = 1.; + double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts + BoundingBoxf bed_ext_dbg; if (const auto *bed_opt = printer_config.option("printable_area"); bed_opt != nullptr && !bed_opt->values.empty()) { const BoundingBoxf bed_ext = get_extents(bed_opt->values); + bed_ext_dbg = bed_ext; const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y(); - lat_center = bed_lat_max - inset - 0.5 * width; + lat_center = lat_origin + bed_lat_max - inset - 0.5 * width; } + // Ground-truth diagnostic for placement frames (warning level so it lands + // in the GUI debug log): plate origin, bed extents, parts bbox, result. + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge place" + << " plate_origin=(" << plate_origin.x() << "," << plate_origin.y() << ")" + << " parts_x=[" << x_min << "," << x_max << "] parts_y=[" << y_min << "," << y_max << "] z_max=" << z_max + << " bed_ext=[" << bed_ext_dbg.min.x() << "," << bed_ext_dbg.min.y() + << " -> " << bed_ext_dbg.max.x() << "," << bed_ext_dbg.max.y() << "]" + << " lat_center=" << lat_center << " belt=[" << belt_start << "," << belt_end << "]"; + // Orient the bar: long axis = belt travel, width = lateral, height = Z. const double size_x = belt_is_y ? width : length; const double size_y = belt_is_y ? length : width; From 7cc50d750c2928629e443e7501004915f710e51c Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 07:20:12 -0500 Subject: [PATCH 06/15] automated placement works --- src/slic3r/GUI/Plater.cpp | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index d00396d97e..b634a65440 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9148,11 +9148,18 @@ bool Plater::priv::ensure_belt_purge_tower() cfg.set_key_value("seam_slope_type", new ConfigOptionEnum(SeamScarfType::None)); cfg.set_key_value("precise_z_height", new ConfigOptionBool(false)); + // Position by the belt-calibration pattern: drop to the bed, then translate + // the instance by the delta between the object's ACTUAL bbox center and the + // target. Setting the instance offset directly is unreliable here — the + // freshly added cube's local frame is not centered, so set_offset() lands + // the min corner (not the center) on the target, leaving the bar centered + // on the bed edge with half of it hanging off. new_object->invalidate_bounding_box(); - const BoundingBoxf3 mesh_bb = new_volume->mesh().bounding_box(); - new_object->translate(-mesh_bb.center()); - new_object->instances.front()->set_offset(desired_center); new_object->ensure_on_bed(); + const BoundingBoxf3 cur = new_object->bounding_box_exact(); + new_object->translate_instances(Vec3d(desired_center.x() - cur.center().x(), + desired_center.y() - cur.center().y(), + 0.0)); new_object->instances.front()->set_assemble_transformation(new_object->instances.front()->get_transformation()); const size_t obj_idx = model.objects.size() - 1; @@ -9170,7 +9177,9 @@ bool Plater::priv::ensure_belt_purge_tower() << " v_layer=" << v_layer << " max_flush=" << max_flush << " filaments=" << filaments.size() << " theta_deg=" << Geometry::rad2deg(theta) - << " center=(" << desired_center.x() << "," << desired_center.y() << "," << desired_center.z() << ")"; + << " desired_center=(" << desired_center.x() << "," << desired_center.y() << "," << desired_center.z() << ")" + << " achieved_center=(" << new_object->bounding_box_exact().center().x() << "," + << new_object->bounding_box_exact().center().y() << ")"; return true; } From 79c93733d33c327d7a1cc30ed1ae6d66865950dd Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 08:00:16 -0500 Subject: [PATCH 07/15] purge tower additional compensation --- src/slic3r/GUI/Plater.cpp | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index b634a65440..f20a9ec60f 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9074,17 +9074,22 @@ bool Plater::priv::ensure_belt_purge_tower() return false; // already up to date — do not touch the model // --- Position ---------------------------------------------------------- - // Belt-travel axis: anchor the bar at the parts' belt-axis MINIMUM and span - // just the parts' belt extent (+ small margin). Filament changes happen - // over the parts' belt band, so the bar must sit there to absorb them; - // extending it far up the belt moves its body away from the toolchange - // layers and the purge goes unabsorbed (empirically, moving the tower - // toward the parts' Y-min clears the "cannot absorb" warning). - const double margin = 5.; - const double belt_start = belt_min; // parts' belt-axis minimum - const double belt_end = belt_max + margin; // just past the parts — stay low / near the parts - const double length = std::max(belt_end - belt_start, 10.); - const double belt_center = 0.5 * (belt_start + belt_end); + // Belt-travel axis: span the parts' belt band, extended at BOTH tilted ends + // by the ramp allowance. The bar's tilted ends are triangular ramps where a + // slicing plane cuts less than the full height, so those layers can't hold a + // full purge. Pull the leading edge toward Y=0 below the parts' minimum and + // push the trailing edge past the parts' maximum, each by height/sin(theta) + // (= sqrt(2)*height at 45 deg), so the full-cross-section region covers the + // whole parts' band and every toolchange layer gets a full slice of the bar. + // (Filament changes happen over the parts' belt band — empirically the tower + // must sit there, anchored near the parts' Y-min, to clear the "cannot + // absorb" warning.) + const double margin = 5.; + const double ramp_compensation = height / sin_t; + const double belt_start = std::max(0.0, belt_min - ramp_compensation); // leading ramp, toward Y=0 + const double belt_end = belt_max + margin + ramp_compensation; // trailing ramp, past the parts + const double length = std::max(belt_end - belt_start, 10.); + const double belt_center = 0.5 * (belt_start + belt_end); // Across-belt: flush against the bed's maximum edge, inset by half the bar // width so the bar's far edge sits on the boundary and the whole bar stays From d367bcef923d22d7ecf297ab2e49f3b9d17fbc70 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 08:13:45 -0500 Subject: [PATCH 08/15] extra height compensation --- src/slic3r/GUI/Plater.cpp | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index f20a9ec60f..33fd309f0e 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9010,6 +9010,7 @@ bool Plater::priv::ensure_belt_purge_tower() if ((rot == BeltRotationAxis::X || rot == BeltRotationAxis::Y) && angle_opt != nullptr && std::abs(angle_opt->value) > EPSILON) theta = std::clamp(Geometry::deg2rad(std::abs(angle_opt->value)), Geometry::deg2rad(5.), M_PI / 2.); const double sin_t = std::sin(theta); + const double cot_t = std::cos(theta) / sin_t; // Parts' extent along the belt-travel axis and the lateral (across-belt) axis. const double belt_min = belt_is_y ? y_min : x_min; @@ -9074,20 +9075,22 @@ bool Plater::priv::ensure_belt_purge_tower() return false; // already up to date — do not touch the model // --- Position ---------------------------------------------------------- - // Belt-travel axis: span the parts' belt band, extended at BOTH tilted ends - // by the ramp allowance. The bar's tilted ends are triangular ramps where a - // slicing plane cuts less than the full height, so those layers can't hold a - // full purge. Pull the leading edge toward Y=0 below the parts' minimum and - // push the trailing edge past the parts' maximum, each by height/sin(theta) - // (= sqrt(2)*height at 45 deg), so the full-cross-section region covers the - // whole parts' band and every toolchange layer gets a full slice of the bar. - // (Filament changes happen over the parts' belt band — empirically the tower - // must sit there, anchored near the parts' Y-min, to clear the "cannot - // absorb" warning.) + // Belt-travel axis. With the mesh rotated by theta before slicing, a machine + // point (y,z) maps to slicing-Z = y*sin(theta) + z*cos(theta). The parts + // occupy slicing-Z in [y_min*sin, y_max*sin + z_max*cos], and the bar's + // FULL-cross-section region (the part not in a triangular end ramp) spans + // slicing-Z [belt_start*sin + H*cos, belt_end*sin]. Covering the parts' + // whole band therefore needs: + // - leading edge belt_start <= y_min - H*cot(theta) (bar's own ramp) + // - trailing edge belt_end >= y_max + z_max*cot(theta) (parts' top + // features print further up the belt, so the bar must reach there). + // The leading side uses the bar-ramp allowance height/sin (>= H*cot, slight + // over-cover); the trailing side adds the parts' height projection + // z_max*cot, which dominates the bar's own ramp. const double margin = 5.; const double ramp_compensation = height / sin_t; - const double belt_start = std::max(0.0, belt_min - ramp_compensation); // leading ramp, toward Y=0 - const double belt_end = belt_max + margin + ramp_compensation; // trailing ramp, past the parts + const double belt_start = std::max(0.0, belt_min - ramp_compensation); // leading ramp, toward Y=0 + const double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach const double length = std::max(belt_end - belt_start, 10.); const double belt_center = 0.5 * (belt_start + belt_end); From 131b61b726c30550f833d0c4757bfc6414603909 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 08:45:33 -0500 Subject: [PATCH 09/15] auto purge tower height calculation works --- src/slic3r/GUI/Plater.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 33fd309f0e..656a7d31e2 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -9021,7 +9021,13 @@ bool Plater::priv::ensure_belt_purge_tower() // Worst-case purge volume of one layer: up to (filament count - 1) // toolchanges, each needing the worst flush matrix entry (mirrors the // volume selection in Print::_plan_belt_purge()). - const bool use_matrix = (print_config.has("purge_in_prime_tower") && print_config.opt_bool("purge_in_prime_tower")) + // NOTE: both purge_in_prime_tower and single_extruder_multi_material are + // PRINTER options (Preset.cpp s_Preset_printer_options) — read them from the + // printer preset. Reading purge_in_prime_tower from the print preset returns + // has()==false, collapsing use_matrix to false and sizing the tower for the + // small prime_volume instead of the real color-change flush. This must match + // the backend Print::_plan_belt_purge() which reads both from the merged config. + const bool use_matrix = (printer_config.has("purge_in_prime_tower") && printer_config.opt_bool("purge_in_prime_tower")) && (printer_config.has("single_extruder_multi_material") && printer_config.opt_bool("single_extruder_multi_material")); double max_flush = print_config.has("prime_volume") ? print_config.opt_float("prime_volume") : 45.; if (use_matrix) { From c80f1ab312285d7884a021abd9815d4bf230ccde Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 09:51:48 -0500 Subject: [PATCH 10/15] cancel top of purge tower early if no extra parts to print --- src/libslic3r/Print.cpp | 21 +++++++++++++++++++++ src/libslic3r/Print.hpp | 3 +++ src/libslic3r/PrintObjectSlice.cpp | 25 +++++++++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index b5d3701e41..5a0a6f8d8b 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -4345,6 +4345,27 @@ void Print::_plan_belt_purge() const bool use_flush_matrix = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material; const float flush_multiplier = (float) m_config.flush_multiplier.get_at(0); + // Cancel the purge prism early: pre-scan the tool ordering for the highest + // print_z that actually has a toolchange, then drop the prism's layers above + // it so the tower stops at the last color swap (saves filament/time). This + // MUST happen before the marking loop below: ensure_perimeters_infills_order + // force-overrides the prism's extrusions on every layer (it is a dedicated + // flush object), so truncating afterwards would leave dangling overrides + // pointing into deleted layers. + { + double last_tc_z = -1.; + unsigned int cur_ext = m_wipe_tower_data.tool_ordering.first_extruder(); + for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools()) + for (const unsigned int e : lt.extruders) + if (e != cur_ext) { last_tc_z = lt.print_z; cur_ext = e; } + if (last_tc_z >= 0.) + for (PrintObject *po : m_objects) + if (po->config().belt_purge_tower_object.value) { + po->belt_truncate_layers_above(last_tc_z); + break; + } + } + // Diagnostic: the prism only absorbs purge at toolchange layers whose // print_z coincides with one of its own layers. Compare the prism's layer // print_z range to the toolchange print_z range and count how many diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index fc78c5efef..2591f6cbd1 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -562,6 +562,9 @@ private: // clipping shift) by delta, used by Print::_align_belt_purge_layers() to // snap the purge prism's layers onto the printed objects' layer grid. void belt_shift_layer_grid(double delta); + // Belt mode: remove layers above z (cancel the purge prism past the last + // toolchange). Returns how many layers were dropped. + size_t belt_truncate_layers_above(coordf_t z); //BBS ExPolygons _shrink_contour_holes(double contour_delta, double hole_delta, const ExPolygons& polys) const; // BBS diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index b95bc4903f..f13b208a3a 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1413,6 +1413,31 @@ void PrintObject::belt_shift_layer_grid(double delta) << " first_layer.print_z=" << (m_layers.empty() ? 0. : m_layers.front()->print_z); } +// Belt mode: drop layers strictly above z (used to cancel the purge prism early +// once there are no more toolchanges above z, so the tower stops at the last +// color swap instead of wasting filament up the rest of the belt). Each layer's +// cross-section is already sliced, so removing upper layers does not affect the +// last toolchange's coverage. Deletes the Layer objects and clears the new top +// layer's upper-layer link. Returns the number of layers removed. +size_t PrintObject::belt_truncate_layers_above(coordf_t z) +{ + size_t keep = m_layers.size(); + while (keep > 0 && m_layers[keep - 1]->print_z > z + EPSILON) + --keep; + if (keep >= m_layers.size()) + return 0; + const size_t removed = m_layers.size() - keep; + for (size_t i = keep; i < m_layers.size(); ++i) + delete m_layers[i]; + m_layers.resize(keep); + if (!m_layers.empty()) + m_layers.back()->upper_layer = nullptr; + BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] truncate purge prism above print_z=" << z + << " kept=" << keep << " removed=" << removed + << " new_top=" << (m_layers.empty() ? 0. : m_layers.back()->print_z); + return removed; +} + // 1) Decides Z positions of the layers, // 2) Initializes layers and their regions // 3) Slices the object meshes From bcfb09481c4fdfdb10ca63e4a419518c5ce6b608 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 11:30:37 -0500 Subject: [PATCH 11/15] pull purge tower into its own files, make purge tower semi-transparent like other purge towers --- src/libslic3r/Preset.cpp | 1 + src/libslic3r/Print.cpp | 25 +- src/libslic3r/PrintConfig.cpp | 11 + src/libslic3r/PrintConfig.hpp | 3 + src/slic3r/CMakeLists.txt | 2 + src/slic3r/GUI/BeltPurgeTower.cpp | 311 +++++++++++++++++++++++++ src/slic3r/GUI/BeltPurgeTower.hpp | 48 ++++ src/slic3r/GUI/ConfigManipulation.cpp | 11 +- src/slic3r/GUI/GLCanvas3D.cpp | 22 ++ src/slic3r/GUI/Plater.cpp | 322 +------------------------- src/slic3r/GUI/Tab.cpp | 14 +- 11 files changed, 443 insertions(+), 327 deletions(-) create mode 100644 src/slic3r/GUI/BeltPurgeTower.cpp create mode 100644 src/slic3r/GUI/BeltPurgeTower.hpp diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 092923e336..1d99f19608 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -1412,6 +1412,7 @@ static std::vector s_Preset_printer_options { "belt_preslice_global", "first_layer_plane", "first_layer_plane_offset", "first_layer_plane_thickness", "belt_support_floor_offset", "belt_support_floor_mode", "belt_support_z_offset_mode", + "enable_belt_purge_tower", "gcode_flavor", "fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs", "single_extruder_multi_material", "manual_filament_change", "file_start_gcode", "machine_start_gcode", "machine_end_gcode", "before_layer_change_gcode", "printing_by_object_gcode", "layer_change_gcode", "time_lapse_gcode", "wrapping_detection_gcode", "change_filament_gcode", "change_extrusion_role_gcode", diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 5a0a6f8d8b..4004cd81de 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -359,6 +359,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n || opt_key == "hot_plate_temp" || opt_key == "textured_plate_temp" || opt_key == "enable_prime_tower" + || opt_key == "enable_belt_purge_tower" || opt_key == "enable_wrapping_detection" || opt_key == "prime_tower_enable_framework" || opt_key == "prime_tower_width" @@ -1474,16 +1475,18 @@ StringObjectException Print::validate(std::vector *warnin add_warning(layer_warning); } + if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value + && m_config.print_sequence == PrintSequence::ByObject + && extruders.size() > 1 && warning != nullptr) { + StringObjectException warningtemp; + warningtemp.string = L("The belt purge tower is not generated in \"By object\" print sequence; " + "filament changes will not be purged."); + warningtemp.opt_key = "enable_belt_purge_tower"; + warningtemp.is_warning = true; + *warning = warningtemp; + } + if (m_config.enable_prime_tower) { - if (m_config.belt_printer.value && m_config.print_sequence == PrintSequence::ByObject - && extruders.size() > 1 && warning != nullptr) { - StringObjectException warningtemp; - warningtemp.string = L("The belt purge tower is not generated in \"By object\" print sequence; " - "filament changes will not be purged."); - warningtemp.opt_key = "enable_prime_tower"; - warningtemp.is_warning = true; - *warning = warningtemp; - } for (const PrintObject* object : m_objects) { if (object->config().precise_z_height.value) { warn(L("Enabling both precise Z height and the prime tower may cause slicing errors."), "precise_z_height"); @@ -4127,8 +4130,10 @@ bool Print::has_wipe_tower() const // prism object via the flush-into-objects machinery instead of a wipe tower. bool Print::has_belt_purge_tower() const { + // Its own purge-tower "type", gated by the belt-only enable_belt_purge_tower + // option (not the classic enable_prime_tower). return m_config.belt_printer.value - && m_config.enable_prime_tower.value + && m_config.enable_belt_purge_tower.value && !m_config.spiral_mode.value && m_config.filament_diameter.values.size() > 1; } diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index cef9a840a8..3be885a49e 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -7321,6 +7321,17 @@ void PrintConfigDef::init_fff_params() def->set_default_value(new ConfigOptionEnum(BeltSupportZOffsetMode::Unconditional)); } + def = this->add("enable_belt_purge_tower", coBool); + def->label = L("Enable belt purge tower"); + def->category = L("Multimaterial"); + def->tooltip = L("Belt-printer replacement for the wipe/prime tower. When enabled on a belt " + "printer, a purge prism is automatically generated next to the printed parts " + "and filament-change purging is routed into it (the classic wipe tower cannot " + "be used on belt printers because its G-code bypasses the belt transform). " + "Only available on belt printers."); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("belt_purge_tower_width", coFloat); def->label = L("Belt purge tower width"); def->category = L("Printable space"); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index ffd0320149..07d8f3e3af 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1807,6 +1807,9 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionEnum, belt_support_z_offset_mode)) // Width (machine X, across the belt) of the auto-generated belt purge prism. ((ConfigOptionFloat, belt_purge_tower_width)) + // Belt-printer-only "type" of purge tower: enables the auto-generated belt + // purge prism (the belt replacement for the classic wipe/prime tower). + ((ConfigOptionBool, enable_belt_purge_tower)) //BBS ((ConfigOptionInts, additional_cooling_fan_speed)) ((ConfigOptionInts, close_additional_fan_first_x_layers)) diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index b98396f943..bdef8b587f 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -397,6 +397,8 @@ set(SLIC3R_GUI_SOURCES GUI/ParamsPanel.hpp GUI/PartPlate.cpp GUI/PartPlate.hpp + GUI/BeltPurgeTower.cpp + GUI/BeltPurgeTower.hpp GUI/FilamentGroupPopup.hpp GUI/FilamentGroupPopup.cpp GUI/PhysicalPrinterDialog.cpp diff --git a/src/slic3r/GUI/BeltPurgeTower.cpp b/src/slic3r/GUI/BeltPurgeTower.cpp new file mode 100644 index 0000000000..47ff6b6bd4 --- /dev/null +++ b/src/slic3r/GUI/BeltPurgeTower.cpp @@ -0,0 +1,311 @@ +#include "BeltPurgeTower.hpp" + +#include "GUI_App.hpp" +#include "GUI_ObjectList.hpp" +#include "PartPlate.hpp" +#include "I18N.hpp" + +#include "libslic3r/Model.hpp" +#include "libslic3r/Preset.hpp" +#include "libslic3r/PresetBundle.hpp" +#include "libslic3r/PrintConfig.hpp" +#include "libslic3r/TriangleMesh.hpp" +#include "libslic3r/Geometry.hpp" +#include "libslic3r/BoundingBox.hpp" + +#include +#include +#include +#include +#include + +#include + +namespace Slic3r { +namespace GUI { + +// The classic wipe tower is disabled in belt mode (its G-code bypasses the belt +// transform), so filament-change purging is routed into this prism via +// flush_into_objects (see Print::_plan_belt_purge()). The prism is a real model +// object so it is sliced through the normal pipeline and picks up the belt +// rotation. Width across the belt is user-set (belt_purge_tower_width); height +// is sized so each tilted slicing plane's cross-section through the prism can +// absorb the worst-case purge volume of one layer; length follows the printed +// objects along the belt (plus ramp/height compensation at both tilted ends). +bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, BeltPurgeSignature &sig) +{ + auto is_prism = [](const ModelObject *mo) { + const ConfigOption *opt = mo->config.option("belt_purge_tower_object"); + return opt != nullptr && opt->getBool(); + }; + + std::vector prism_idxs; + for (int i = 0; i < (int) model.objects.size(); ++i) + if (is_prism(model.objects[i])) + prism_idxs.push_back(i); + + // Deletes prism objects, keeping the sidebar and part plates in sync + // (same primitives as Plater::priv::remove(), minus scene update — the + // caller refreshes the scene). + auto remove_prisms = [&](const std::vector &idxs) { + for (auto it = idxs.rbegin(); it != idxs.rend(); ++it) { + model.delete_object(size_t(*it)); + partplate_list.notify_instance_removed(*it, -1); + obj_list->delete_object_from_list(size_t(*it)); + } + }; + + const auto &printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; + const auto &print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; + auto &project_config = wxGetApp().preset_bundle->project_config; + + const auto *belt_opt = printer_config.option("belt_printer"); + const bool belt = belt_opt != nullptr && belt_opt->value; + // Belt purge tower is its own type, gated by the belt-only printer option + // enable_belt_purge_tower (not the classic process enable_prime_tower). + const bool prime_tower_enabled = printer_config.has("enable_belt_purge_tower") && printer_config.opt_bool("enable_belt_purge_tower"); + const auto *seq_opt = print_config.option>("print_sequence"); + const bool by_object = seq_opt != nullptr && seq_opt->value == PrintSequence::ByObject; + + // Filaments used and bounding extent of the non-prism objects on the + // current plate (1-based filament ids; volume extruder 0 = object default). + PartPlate *plate = partplate_list.get_curr_plate(); + std::set filaments; + double x_min = std::numeric_limits::max(); + double x_max = -std::numeric_limits::max(); + double y_min = std::numeric_limits::max(); + double y_max = -std::numeric_limits::max(); + double z_max = 0.; + bool have_objects = false; + if (belt && plate != nullptr) { + for (int obj_idx = 0; obj_idx < (int) model.objects.size(); ++obj_idx) { + const ModelObject *mo = model.objects[obj_idx]; + if (is_prism(mo)) + continue; + int obj_extruder = 1; + if (const ConfigOption *opt = mo->config.option("extruder"); opt != nullptr && opt->getInt() > 0) + obj_extruder = opt->getInt(); + bool any_instance_on_plate = false; + for (int inst_idx = 0; inst_idx < (int) mo->instances.size(); ++inst_idx) { + if (!plate->contain_instance_totally(obj_idx, inst_idx)) + continue; + any_instance_on_plate = true; + const BoundingBoxf3 bb = mo->instance_bounding_box(inst_idx); + x_min = std::min(x_min, bb.min.x()); + x_max = std::max(x_max, bb.max.x()); + y_min = std::min(y_min, bb.min.y()); + y_max = std::max(y_max, bb.max.y()); + z_max = std::max(z_max, bb.max.z()); + } + if (!any_instance_on_plate) + continue; + have_objects = true; + for (const ModelVolume *mv : mo->volumes) + for (int e : mv->get_extruders()) + filaments.insert(e > 0 ? e : obj_extruder); + } + } + + const bool wanted = belt && prime_tower_enabled && !by_object && have_objects && filaments.size() > 1; + if (!wanted) { + sig = BeltPurgeSignature{}; + if (prism_idxs.empty()) + return false; + remove_prisms(prism_idxs); + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] belt purge tower removed (conditions not met)"; + return true; + } + + // --- Sizing ----------------------------------------------------------- + const double width = print_config.has("belt_purge_tower_width") ? std::max(1., print_config.opt_float("belt_purge_tower_width")) : 35.; + const double layer_h = print_config.has("layer_height") ? print_config.opt_float("layer_height") : 0.2; + + // Belt geometry. The rotation axis is the gantry tilt axis; the belt + // travels along the *other* horizontal axis (X-rotation -> belt along Y, + // the CR-30 default). The purge prism is a long bar laid along the belt + // travel direction, beside the parts. For no/Z rotation we fall back to + // vertical slicing geometry (theta = 90 deg). + const auto *axis_opt = printer_config.option>("belt_slice_rotation"); + const auto *angle_opt = printer_config.option("belt_slice_rotation_angle"); + const BeltRotationAxis rot = axis_opt != nullptr ? axis_opt->value : BeltRotationAxis::X; + const bool belt_is_y = (rot != BeltRotationAxis::Y); // X / None / Z -> belt along Y + double theta = M_PI / 2.; + if ((rot == BeltRotationAxis::X || rot == BeltRotationAxis::Y) && angle_opt != nullptr && std::abs(angle_opt->value) > EPSILON) + theta = std::clamp(Geometry::deg2rad(std::abs(angle_opt->value)), Geometry::deg2rad(5.), M_PI / 2.); + const double sin_t = std::sin(theta); + const double cot_t = std::cos(theta) / sin_t; + + // Parts' extent along the belt-travel axis and the lateral (across-belt) axis. + const double belt_min = belt_is_y ? y_min : x_min; + const double belt_max = belt_is_y ? y_max : x_max; + const double lat_min = belt_is_y ? x_min : y_min; + const double lat_max = belt_is_y ? x_max : y_max; + + // Worst-case purge volume of one layer: up to (filament count - 1) + // toolchanges, each needing the worst flush matrix entry (mirrors the + // volume selection in Print::_plan_belt_purge()). + // NOTE: both purge_in_prime_tower and single_extruder_multi_material are + // PRINTER options (Preset.cpp s_Preset_printer_options) — read them from the + // printer preset. Reading purge_in_prime_tower from the print preset returns + // has()==false, collapsing use_matrix to false and sizing the tower for the + // small prime_volume instead of the real color-change flush. This must match + // the backend Print::_plan_belt_purge() which reads both from the merged config. + const bool use_matrix = (printer_config.has("purge_in_prime_tower") && printer_config.opt_bool("purge_in_prime_tower")) + && (printer_config.has("single_extruder_multi_material") && printer_config.opt_bool("single_extruder_multi_material")); + double max_flush = print_config.has("prime_volume") ? print_config.opt_float("prime_volume") : 45.; + if (use_matrix) { + const size_t extruder_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); + const std::vector matrix = get_flush_volumes_matrix( + project_config.option("flush_volumes_matrix")->values, 0, extruder_nums); + const auto * multi_opt = project_config.option("flush_multiplier"); + const double multiplier = multi_opt != nullptr && !multi_opt->values.empty() ? multi_opt->get_at(0) : 1.; + const int n_total = (int) (std::sqrt(double(matrix.size())) + 0.5); + double m = 0.; + for (int i : filaments) + for (int j : filaments) + if (i != j && i <= n_total && j <= n_total) + m = std::max(m, matrix[size_t(i - 1) * n_total + size_t(j - 1)]); + if (m > 0.) + max_flush = m * multiplier; + } + const double v_layer = double(filaments.size() - 1) * max_flush; + + // Height from the per-layer purge demand. A tilted slicing plane cuts a + // width x (height/sin) rectangle out of the bar, so one layer slab absorbs + // width * (height/sin) * layer_height of purge. Solve for the height that + // holds the worst-case per-layer purge, with eta (infill/perimeter packing) + // and a safety margin for the tilt ramps / grid-alignment slop, plus a + // minimum so the tower is a real printable body rather than a sliver. + const double eta = 0.85; + const double safety = 1.6; + const double printable_height = printer_config.has("printable_height") ? printer_config.opt_float("printable_height") : 250.; + double height = safety * v_layer * sin_t / (width * layer_h * eta); + height = std::clamp(height, 8.0, std::max(8.0, printable_height)); + + // --- Idempotence (input-keyed) ---------------------------------------- + auto q = [](double v) { return std::lround(v * 10.0); }; // 0.1 mm quantization + BeltPurgeSignature new_sig; + new_sig.valid = true; + new_sig.filament_count = (int) filaments.size(); + new_sig.key[0] = q(width); + new_sig.key[1] = q(layer_h); + new_sig.key[2] = q(height); + new_sig.key[3] = q(belt_min); + new_sig.key[4] = q(belt_max); + new_sig.key[5] = q(lat_min); + new_sig.key[6] = q(z_max); + if (prism_idxs.size() == 1 && new_sig == sig) + return false; // already up to date — do not touch the model + + // --- Position ---------------------------------------------------------- + // Belt-travel axis. With the mesh rotated by theta before slicing, a machine + // point (y,z) maps to slicing-Z = y*sin(theta) + z*cos(theta). The parts + // occupy slicing-Z in [y_min*sin, y_max*sin + z_max*cos], and the bar's + // FULL-cross-section region (the part not in a triangular end ramp) spans + // slicing-Z [belt_start*sin + H*cos, belt_end*sin]. Covering the parts' + // whole band needs belt_start <= y_min - H*cot (bar's own leading ramp) and + // belt_end >= y_max + z_max*cot (parts' top features print further up the + // belt). The trailing z_max*cot term dominates the bar's own ramp. + const double margin = 5.; + const double ramp_compensation = height / sin_t; + const double belt_start = std::max(0.0, belt_min - ramp_compensation); // leading ramp, toward Y=0 + const double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach + const double length = std::max(belt_end - belt_start, 10.); + const double belt_center = 0.5 * (belt_start + belt_end); + + // Across-belt: flush against the bed's maximum edge, inset by half the bar + // width so the bar's far edge sits on the boundary and the whole bar stays + // on the bed. The bed (printable_area) is plate-local but model instances + // live in the plate's world frame, so add the plate origin's lateral + // component. lat_min/lat_max come from instance_bounding_box (world frame). + const Vec3d plate_origin = plate->get_origin(); + const double lat_origin = plate_origin[belt_is_y ? 0 : 1]; + const double inset = 1.; + double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts + BoundingBoxf bed_ext_dbg; + if (const auto *bed_opt = printer_config.option("printable_area"); + bed_opt != nullptr && !bed_opt->values.empty()) { + const BoundingBoxf bed_ext = get_extents(bed_opt->values); + bed_ext_dbg = bed_ext; + const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y(); + lat_center = lat_origin + bed_lat_max - inset - 0.5 * width; + } + + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge place" + << " plate_origin=(" << plate_origin.x() << "," << plate_origin.y() << ")" + << " parts_x=[" << x_min << "," << x_max << "] parts_y=[" << y_min << "," << y_max << "] z_max=" << z_max + << " bed_ext=[" << bed_ext_dbg.min.x() << "," << bed_ext_dbg.min.y() + << " -> " << bed_ext_dbg.max.x() << "," << bed_ext_dbg.max.y() << "]" + << " lat_center=" << lat_center << " belt=[" << belt_start << "," << belt_end << "]"; + + // Orient the bar: long axis = belt travel, width = lateral, height = Z. + const double size_x = belt_is_y ? width : length; + const double size_y = belt_is_y ? length : width; + const Vec3d desired_center(belt_is_y ? lat_center : belt_center, + belt_is_y ? belt_center : lat_center, + 0.5 * height); + + // --- (Re)create --------------------------------------------------------- + if (!prism_idxs.empty()) + remove_prisms(prism_idxs); + + ModelObject *new_object = model.add_object(); + new_object->name = _u8L("Belt Purge Tower"); + new_object->add_instance(); + ModelVolume *new_volume = new_object->add_volume(make_cube(size_x, size_y, height)); + new_volume->name = new_object->name; + + auto &cfg = new_object->config; + cfg.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + cfg.set_key_value("flush_into_objects", new ConfigOptionBool(true)); + cfg.set_key_value("extruder", new ConfigOptionInt(1)); + // Sacrificial solid prism: one wall, no shells, dense rectilinear infill — + // every extrusion is overriddable, so the absorbed volume matches the + // cross-section x layer-height estimate used for the height above. + cfg.set_key_value("wall_loops", new ConfigOptionInt(1)); + cfg.set_key_value("top_shell_layers", new ConfigOptionInt(0)); + cfg.set_key_value("bottom_shell_layers", new ConfigOptionInt(0)); + cfg.set_key_value("sparse_infill_density", new ConfigOptionPercent(100)); + cfg.set_key_value("sparse_infill_pattern", new ConfigOptionEnum(ipRectilinear)); + cfg.set_key_value("enable_support", new ConfigOptionBool(false)); + cfg.set_key_value("brim_type", new ConfigOptionEnum(btNoBrim)); + cfg.set_key_value("seam_slope_type", new ConfigOptionEnum(SeamScarfType::None)); + cfg.set_key_value("precise_z_height", new ConfigOptionBool(false)); + + // Position by the belt-calibration pattern: drop to the bed, then translate + // the instance by the delta between the object's ACTUAL bbox center and the + // target. Setting the instance offset directly is unreliable here — the + // freshly added cube's local frame is not centered, so set_offset() lands + // the min corner (not the center) on the target, leaving the bar centered + // on the bed edge with half of it hanging off. + new_object->invalidate_bounding_box(); + new_object->ensure_on_bed(); + const BoundingBoxf3 cur = new_object->bounding_box_exact(); + new_object->translate_instances(Vec3d(desired_center.x() - cur.center().x(), + desired_center.y() - cur.center().y(), + 0.0)); + new_object->instances.front()->set_assemble_transformation(new_object->instances.front()->get_transformation()); + + const size_t obj_idx = model.objects.size() - 1; + // Registers the object in the sidebar and notifies the part plates; + // selection is left untouched (auto-managed object). + obj_list->add_object_to_list(obj_idx, /*call_selection_changed=*/false); + + // Record the inputs that produced this prism so subsequent ticks are no-ops + // until the parts/config actually change. + sig = new_sig; + + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] belt purge tower generated" + << " belt_is_y=" << belt_is_y + << " W=" << width << " L=" << length << " H=" << height + << " v_layer=" << v_layer << " max_flush=" << max_flush + << " filaments=" << filaments.size() + << " theta_deg=" << Geometry::rad2deg(theta) + << " desired_center=(" << desired_center.x() << "," << desired_center.y() << "," << desired_center.z() << ")" + << " achieved_center=(" << new_object->bounding_box_exact().center().x() << "," + << new_object->bounding_box_exact().center().y() << ")"; + return true; +} + +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/BeltPurgeTower.hpp b/src/slic3r/GUI/BeltPurgeTower.hpp new file mode 100644 index 0000000000..b1735303c5 --- /dev/null +++ b/src/slic3r/GUI/BeltPurgeTower.hpp @@ -0,0 +1,48 @@ +#ifndef slic3r_GUI_BeltPurgeTower_hpp_ +#define slic3r_GUI_BeltPurgeTower_hpp_ + +// ORCA-Belt: auto-managed purge prism for belt printers. +// +// Kept in its own translation unit (not buried in Plater.cpp) so it stays out +// of the way of unrelated upstream changes and carries no regression risk for +// normal printers: nothing here runs unless the printer is a belt printer with +// the belt purge tower enabled. See Slic3r::GUI::ensure_belt_purge_tower(). + +namespace Slic3r { +class Model; +class PartPlateList; +namespace GUI { +class ObjectList; + +// Inputs of the last belt purge prism generation. Idempotence is keyed on these +// (not the prism's resulting bbox): the prism gets nudged by plate assignment +// after creation, so comparing its bbox would falsely detect a change and +// regenerate it on every background-process tick — which would re-invalidate a +// freshly sliced result and make the G-code preview unreachable. +struct BeltPurgeSignature +{ + bool valid = false; + int filament_count = 0; + long key[7] = {0}; // rounded geometry inputs (0.1 mm units) + bool operator==(const BeltPurgeSignature &o) const + { + if (valid != o.valid || filament_count != o.filament_count) + return false; + for (int i = 0; i < 7; ++i) + if (key[i] != o.key[i]) + return false; + return true; + } +}; + +// Keep the auto-generated belt purge prism in sync with the current config and +// plate contents. Creates / updates / removes the marked prism ModelObject. +// Returns true when the model was mutated (caller should refresh the scene). +// Runs on every background-process update, so it is idempotent: it only mutates +// the model when the desired prism differs from the cached signature in `sig`. +bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, BeltPurgeSignature &sig); + +} // namespace GUI +} // namespace Slic3r + +#endif // slic3r_GUI_BeltPurgeTower_hpp_ diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index ffd4d43a71..71a111ef29 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -972,10 +972,13 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in bool have_prime_tower = config->opt_bool("enable_prime_tower"); // ORCA-Belt: belt printers replace the classic wipe tower with the - // auto-generated belt purge prism — only its width is tunable; the - // classic tower geometry options do not apply. (is_belt_printer is - // computed at the top of this function.) - toggle_line("belt_purge_tower_width", have_prime_tower && is_belt_printer); + // auto-generated belt purge prism, enabled by the belt-only printer option + // enable_belt_purge_tower. Its width (a process option) is only relevant + // then. (is_belt_printer is computed at the top of this function.) + const bool have_belt_purge_tower = is_belt_printer + && preset_bundle->printers.get_edited_preset().config.has("enable_belt_purge_tower") + && preset_bundle->printers.get_edited_preset().config.opt_bool("enable_belt_purge_tower"); + toggle_line("belt_purge_tower_width", have_belt_purge_tower); for (auto el : {"prime_tower_width", "prime_tower_brim_width", "prime_tower_skip_points", "wipe_tower_wall_type", "prime_tower_infill_gap","prime_tower_enable_framework", "enable_tower_interface_features"}) toggle_line(el, have_prime_tower && !is_belt_printer); diff --git a/src/slic3r/GUI/GLCanvas3D.cpp b/src/slic3r/GUI/GLCanvas3D.cpp index 8ff05862a8..ca517c4303 100644 --- a/src/slic3r/GUI/GLCanvas3D.cpp +++ b/src/slic3r/GUI/GLCanvas3D.cpp @@ -2948,6 +2948,28 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re } update_volumes_colors_by_extruder(); + + // ORCA-Belt: render the auto-generated belt purge prism like a wipe tower — + // semi-transparent, in its (filament) color. It is a real sliced object, so + // the G-code preview already shows the actual per-layer purge colors; here in + // the editor we just make the block translucent so it reads as a purge tower + // rather than a solid part. update_colors_by_extruder() preserves alpha when + // not updating alpha, so lowering it once sticks across recolors. + if (m_model != nullptr) { + for (GLVolume *volume : m_volumes.volumes) { + if (volume == nullptr || volume->volume_idx() < 0) + continue; + const int obj_idx = volume->object_idx(); + if (obj_idx < 0 || obj_idx >= (int) m_model->objects.size()) + continue; + const ConfigOption *opt = m_model->objects[obj_idx]->config.option("belt_purge_tower_object"); + if (opt != nullptr && opt->getBool()) { + volume->color.a(0.66f); + volume->force_transparent = true; + } + } + } + // Update selection indices based on the old/new GLVolumeCollection. if (m_selection.get_mode() == Selection::Instance) m_selection.instances_changed(instance_ids_selected); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index 656a7d31e2..c2ddb897bf 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -105,6 +105,7 @@ #include "GUI_Preview.hpp" #include "3DBed.hpp" #include "PartPlate.hpp" +#include "BeltPurgeTower.hpp" #include "Camera.hpp" #include "Mouse3DController.hpp" #include "Tab.hpp" @@ -5476,24 +5477,10 @@ struct Plater::priv void remove(size_t obj_idx); bool delete_object_from_model(size_t obj_idx, bool refresh_immediately = true); //BBS // ORCA-Belt: keep the auto-generated belt purge prism in sync with the - // config and plate contents. Returns true when the model was mutated - // (caller should refresh the scene). + // config and plate contents (thin wrapper over GUI::ensure_belt_purge_tower + // in BeltPurgeTower.cpp). Returns true when the model was mutated. bool ensure_belt_purge_tower(); - // Inputs of the last belt purge prism generation. Idempotence is keyed on - // these (not the prism's resulting bbox): the prism gets nudged by plate - // assignment after creation, so comparing its bbox would falsely detect a - // change and regenerate it on every background-process tick — which would - // re-invalidate a freshly sliced result and make the preview unreachable. - struct BeltPurgeSignature { - bool valid = false; - int filament_count = 0; - long key[7] = {0}; // rounded geometry inputs (0.1 mm units) - bool operator==(const BeltPurgeSignature &o) const { - if (valid != o.valid || filament_count != o.filament_count) return false; - for (int i = 0; i < 7; ++i) if (key[i] != o.key[i]) return false; - return true; - } - } m_belt_purge_sig; + BeltPurgeSignature m_belt_purge_sig; void delete_all_objects_from_model(); void reset(bool apply_presets_change = false); void center_selection(); @@ -8898,305 +8885,16 @@ void Plater::priv::process_validation_warnings(const std::vectorconfig.option("belt_purge_tower_object"); - return opt != nullptr && opt->getBool(); - }; - - std::vector prism_idxs; - for (int i = 0; i < (int) model.objects.size(); ++i) - if (is_prism(model.objects[i])) - prism_idxs.push_back(i); - - // Deletes prism objects, keeping the sidebar and part plates in sync - // (same primitives as Plater::priv::remove(), minus scene update — the - // caller refreshes the scene). - auto remove_prisms = [this](const std::vector &idxs) { - for (auto it = idxs.rbegin(); it != idxs.rend(); ++it) { - model.delete_object(size_t(*it)); - partplate_list.notify_instance_removed(*it, -1); - sidebar->obj_list()->delete_object_from_list(size_t(*it)); - } - }; - - const auto &printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; - const auto &print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto &project_config = wxGetApp().preset_bundle->project_config; - - const auto *belt_opt = printer_config.option("belt_printer"); - const bool belt = belt_opt != nullptr && belt_opt->value; - const bool prime_tower_enabled = print_config.has("enable_prime_tower") && print_config.opt_bool("enable_prime_tower"); - const auto *seq_opt = print_config.option>("print_sequence"); - const bool by_object = seq_opt != nullptr && seq_opt->value == PrintSequence::ByObject; - - // Filaments used and bounding extent of the non-prism objects on the - // current plate (1-based filament ids; volume extruder 0 = object default). - PartPlate *plate = partplate_list.get_curr_plate(); - std::set filaments; - double x_min = std::numeric_limits::max(); - double x_max = -std::numeric_limits::max(); - double y_min = std::numeric_limits::max(); - double y_max = -std::numeric_limits::max(); - double z_max = 0.; - bool have_objects = false; - if (belt && plate != nullptr) { - for (int obj_idx = 0; obj_idx < (int) model.objects.size(); ++obj_idx) { - const ModelObject *mo = model.objects[obj_idx]; - if (is_prism(mo)) - continue; - int obj_extruder = 1; - if (const ConfigOption *opt = mo->config.option("extruder"); opt != nullptr && opt->getInt() > 0) - obj_extruder = opt->getInt(); - bool any_instance_on_plate = false; - for (int inst_idx = 0; inst_idx < (int) mo->instances.size(); ++inst_idx) { - if (!plate->contain_instance_totally(obj_idx, inst_idx)) - continue; - any_instance_on_plate = true; - const BoundingBoxf3 bb = mo->instance_bounding_box(inst_idx); - x_min = std::min(x_min, bb.min.x()); - x_max = std::max(x_max, bb.max.x()); - y_min = std::min(y_min, bb.min.y()); - y_max = std::max(y_max, bb.max.y()); - z_max = std::max(z_max, bb.max.z()); - } - if (!any_instance_on_plate) - continue; - have_objects = true; - for (const ModelVolume *mv : mo->volumes) - for (int e : mv->get_extruders()) - filaments.insert(e > 0 ? e : obj_extruder); - } - } - - const bool wanted = belt && prime_tower_enabled && !by_object && have_objects && filaments.size() > 1; - if (!wanted) { - m_belt_purge_sig = BeltPurgeSignature{}; - if (prism_idxs.empty()) - return false; - remove_prisms(prism_idxs); - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] belt purge tower removed (conditions not met)"; - return true; - } - - // --- Sizing ----------------------------------------------------------- - const double width = print_config.has("belt_purge_tower_width") ? std::max(1., print_config.opt_float("belt_purge_tower_width")) : 35.; - const double layer_h = print_config.has("layer_height") ? print_config.opt_float("layer_height") : 0.2; - - // Belt geometry. The rotation axis is the gantry tilt axis; the belt - // travels along the *other* horizontal axis (X-rotation -> belt along Y, - // the CR-30 default). The purge prism is a long bar laid along the belt - // travel direction, beside the parts. For no/Z rotation we fall back to - // vertical slicing geometry (theta = 90 deg). - const auto *axis_opt = printer_config.option>("belt_slice_rotation"); - const auto *angle_opt = printer_config.option("belt_slice_rotation_angle"); - const BeltRotationAxis rot = axis_opt != nullptr ? axis_opt->value : BeltRotationAxis::X; - const bool belt_is_y = (rot != BeltRotationAxis::Y); // X / None / Z -> belt along Y - double theta = M_PI / 2.; - if ((rot == BeltRotationAxis::X || rot == BeltRotationAxis::Y) && angle_opt != nullptr && std::abs(angle_opt->value) > EPSILON) - theta = std::clamp(Geometry::deg2rad(std::abs(angle_opt->value)), Geometry::deg2rad(5.), M_PI / 2.); - const double sin_t = std::sin(theta); - const double cot_t = std::cos(theta) / sin_t; - - // Parts' extent along the belt-travel axis and the lateral (across-belt) axis. - const double belt_min = belt_is_y ? y_min : x_min; - const double belt_max = belt_is_y ? y_max : x_max; - const double lat_min = belt_is_y ? x_min : y_min; - const double lat_max = belt_is_y ? x_max : y_max; - - // Worst-case purge volume of one layer: up to (filament count - 1) - // toolchanges, each needing the worst flush matrix entry (mirrors the - // volume selection in Print::_plan_belt_purge()). - // NOTE: both purge_in_prime_tower and single_extruder_multi_material are - // PRINTER options (Preset.cpp s_Preset_printer_options) — read them from the - // printer preset. Reading purge_in_prime_tower from the print preset returns - // has()==false, collapsing use_matrix to false and sizing the tower for the - // small prime_volume instead of the real color-change flush. This must match - // the backend Print::_plan_belt_purge() which reads both from the merged config. - const bool use_matrix = (printer_config.has("purge_in_prime_tower") && printer_config.opt_bool("purge_in_prime_tower")) - && (printer_config.has("single_extruder_multi_material") && printer_config.opt_bool("single_extruder_multi_material")); - double max_flush = print_config.has("prime_volume") ? print_config.opt_float("prime_volume") : 45.; - if (use_matrix) { - const size_t extruder_nums = wxGetApp().preset_bundle->get_printer_extruder_count(); - const std::vector matrix = get_flush_volumes_matrix( - project_config.option("flush_volumes_matrix")->values, 0, extruder_nums); - const auto * multi_opt = project_config.option("flush_multiplier"); - const double multiplier = multi_opt != nullptr && !multi_opt->values.empty() ? multi_opt->get_at(0) : 1.; - const int n_total = (int) (std::sqrt(double(matrix.size())) + 0.5); - double m = 0.; - for (int i : filaments) - for (int j : filaments) - if (i != j && i <= n_total && j <= n_total) - m = std::max(m, matrix[size_t(i - 1) * n_total + size_t(j - 1)]); - if (m > 0.) - max_flush = m * multiplier; - } - const double v_layer = double(filaments.size() - 1) * max_flush; - - // Height from the per-layer purge demand. A tilted slicing plane cuts a - // width x (height/sin) rectangle out of the bar, so one layer slab absorbs - // width * (height/sin) * layer_height of purge. Solve for the height that - // holds the worst-case per-layer purge, with: - // eta - infill/perimeter packing (not all of the cross-section is solid) - // safety - margin for the tilt ramp at the bar ends and layer-grid - // alignment slop, where a layer cuts less than the full section - // and a minimum so the tower is a real printable body rather than a sliver. - const double eta = 0.85; - const double safety = 1.6; - const double printable_height = printer_config.has("printable_height") ? printer_config.opt_float("printable_height") : 250.; - double height = safety * v_layer * sin_t / (width * layer_h * eta); - height = std::clamp(height, 8.0, std::max(8.0, printable_height)); - - // --- Idempotence (input-keyed) ---------------------------------------- - // Key on the generation inputs, NOT the prism's resulting bbox: plate - // assignment nudges the prism after creation, so a bbox comparison sees a - // "change" every tick and regenerates the prism, which re-invalidates any - // freshly sliced result and makes the G-code preview unreachable. - auto q = [](double v) { return std::lround(v * 10.0); }; // 0.1 mm quantization - BeltPurgeSignature sig; - sig.valid = true; - sig.filament_count = (int) filaments.size(); - sig.key[0] = q(width); - sig.key[1] = q(layer_h); - sig.key[2] = q(height); - sig.key[3] = q(belt_min); - sig.key[4] = q(belt_max); - sig.key[5] = q(lat_min); - sig.key[6] = q(z_max); - if (prism_idxs.size() == 1 && sig == m_belt_purge_sig) - return false; // already up to date — do not touch the model - - // --- Position ---------------------------------------------------------- - // Belt-travel axis. With the mesh rotated by theta before slicing, a machine - // point (y,z) maps to slicing-Z = y*sin(theta) + z*cos(theta). The parts - // occupy slicing-Z in [y_min*sin, y_max*sin + z_max*cos], and the bar's - // FULL-cross-section region (the part not in a triangular end ramp) spans - // slicing-Z [belt_start*sin + H*cos, belt_end*sin]. Covering the parts' - // whole band therefore needs: - // - leading edge belt_start <= y_min - H*cot(theta) (bar's own ramp) - // - trailing edge belt_end >= y_max + z_max*cot(theta) (parts' top - // features print further up the belt, so the bar must reach there). - // The leading side uses the bar-ramp allowance height/sin (>= H*cot, slight - // over-cover); the trailing side adds the parts' height projection - // z_max*cot, which dominates the bar's own ramp. - const double margin = 5.; - const double ramp_compensation = height / sin_t; - const double belt_start = std::max(0.0, belt_min - ramp_compensation); // leading ramp, toward Y=0 - const double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach - const double length = std::max(belt_end - belt_start, 10.); - const double belt_center = 0.5 * (belt_start + belt_end); - - // Across-belt: flush against the bed's maximum edge, inset by half the bar - // width so the bar's far edge sits on the boundary and the whole bar stays - // on the bed. The bed (printable_area) is plate-local but model instances - // live in the plate's world frame, so add the plate origin's lateral - // component (plate-origin compensation is needed throughout the belt - // pipeline). lat_min/lat_max come from instance_bounding_box (world frame). - const Vec3d plate_origin = plate->get_origin(); - const double lat_origin = plate_origin[belt_is_y ? 0 : 1]; - const double inset = 1.; - double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts - BoundingBoxf bed_ext_dbg; - if (const auto *bed_opt = printer_config.option("printable_area"); - bed_opt != nullptr && !bed_opt->values.empty()) { - const BoundingBoxf bed_ext = get_extents(bed_opt->values); - bed_ext_dbg = bed_ext; - const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y(); - lat_center = lat_origin + bed_lat_max - inset - 0.5 * width; - } - - // Ground-truth diagnostic for placement frames (warning level so it lands - // in the GUI debug log): plate origin, bed extents, parts bbox, result. - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge place" - << " plate_origin=(" << plate_origin.x() << "," << plate_origin.y() << ")" - << " parts_x=[" << x_min << "," << x_max << "] parts_y=[" << y_min << "," << y_max << "] z_max=" << z_max - << " bed_ext=[" << bed_ext_dbg.min.x() << "," << bed_ext_dbg.min.y() - << " -> " << bed_ext_dbg.max.x() << "," << bed_ext_dbg.max.y() << "]" - << " lat_center=" << lat_center << " belt=[" << belt_start << "," << belt_end << "]"; - - // Orient the bar: long axis = belt travel, width = lateral, height = Z. - const double size_x = belt_is_y ? width : length; - const double size_y = belt_is_y ? length : width; - const Vec3d desired_center(belt_is_y ? lat_center : belt_center, - belt_is_y ? belt_center : lat_center, - 0.5 * height); - - // --- (Re)create --------------------------------------------------------- - if (!prism_idxs.empty()) - remove_prisms(prism_idxs); - - ModelObject *new_object = model.add_object(); - new_object->name = _u8L("Belt Purge Tower"); - new_object->add_instance(); - ModelVolume *new_volume = new_object->add_volume(make_cube(size_x, size_y, height)); - new_volume->name = new_object->name; - - auto &cfg = new_object->config; - cfg.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); - cfg.set_key_value("flush_into_objects", new ConfigOptionBool(true)); - cfg.set_key_value("extruder", new ConfigOptionInt(1)); - // Sacrificial solid prism: one wall, no shells, dense rectilinear infill — - // every extrusion is overriddable, so the absorbed volume matches the - // cross-section x layer-height estimate used for the height above. - cfg.set_key_value("wall_loops", new ConfigOptionInt(1)); - cfg.set_key_value("top_shell_layers", new ConfigOptionInt(0)); - cfg.set_key_value("bottom_shell_layers", new ConfigOptionInt(0)); - cfg.set_key_value("sparse_infill_density", new ConfigOptionPercent(100)); - cfg.set_key_value("sparse_infill_pattern", new ConfigOptionEnum(ipRectilinear)); - cfg.set_key_value("enable_support", new ConfigOptionBool(false)); - cfg.set_key_value("brim_type", new ConfigOptionEnum(btNoBrim)); - cfg.set_key_value("seam_slope_type", new ConfigOptionEnum(SeamScarfType::None)); - cfg.set_key_value("precise_z_height", new ConfigOptionBool(false)); - - // Position by the belt-calibration pattern: drop to the bed, then translate - // the instance by the delta between the object's ACTUAL bbox center and the - // target. Setting the instance offset directly is unreliable here — the - // freshly added cube's local frame is not centered, so set_offset() lands - // the min corner (not the center) on the target, leaving the bar centered - // on the bed edge with half of it hanging off. - new_object->invalidate_bounding_box(); - new_object->ensure_on_bed(); - const BoundingBoxf3 cur = new_object->bounding_box_exact(); - new_object->translate_instances(Vec3d(desired_center.x() - cur.center().x(), - desired_center.y() - cur.center().y(), - 0.0)); - new_object->instances.front()->set_assemble_transformation(new_object->instances.front()->get_transformation()); - - const size_t obj_idx = model.objects.size() - 1; - // Registers the object in the sidebar and notifies the part plates; - // selection is left untouched (auto-managed object). - sidebar->obj_list()->add_object_to_list(obj_idx, /*call_selection_changed=*/false); - - // Record the inputs that produced this prism so subsequent ticks are no-ops - // until the parts/config actually change. - m_belt_purge_sig = sig; - - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] belt purge tower generated" - << " belt_is_y=" << belt_is_y - << " W=" << width << " L=" << length << " H=" << height - << " v_layer=" << v_layer << " max_flush=" << max_flush - << " filaments=" << filaments.size() - << " theta_deg=" << Geometry::rad2deg(theta) - << " desired_center=(" << desired_center.x() << "," << desired_center.y() << "," << desired_center.z() << ")" - << " achieved_center=(" << new_object->bounding_box_exact().center().x() << "," - << new_object->bounding_box_exact().center().y() << ")"; - return true; + return GUI::ensure_belt_purge_tower(model, partplate_list, sidebar->obj_list(), m_belt_purge_sig); } + // Update background processing thread from the current config and Model. // Returns a bitmask of UpdateBackgroundProcessReturnState. unsigned int Plater::priv::update_background_process(bool force_validation, bool postpone_error_messages, bool switch_print) diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index 25287c79c3..bf0ce6e65e 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -5699,6 +5699,12 @@ if (is_marlin_flavor) optgroup->append_single_option_line("enable_filament_ramming", "printer_multimaterial_wipe_tower#enable-filament-ramming"); optgroup->append_single_option_line("tool_change_on_wipe_tower", "printer_multimaterial_wipe_tower#tool-change-on-wipe-tower"); + // Orca-Belt: belt printers replace the classic wipe tower with an + // auto-generated purge prism; this is its enable (gated to belt printers + // in toggle_options()). + optgroup = page->new_optgroup(L("Belt purge tower"), "param_tower"); + optgroup->append_single_option_line("enable_belt_purge_tower", "printer_multimaterial_wipe_tower"); + optgroup = page->new_optgroup(L("Single extruder multi-material parameters"), "param_settings"); optgroup->append_single_option_line("cooling_tube_retraction", "printer_multimaterial_semm_parameters#cooling-tube-position"); @@ -6233,8 +6239,14 @@ void TabPrinter::toggle_options() } if (m_active_page->title() == L("Multimaterial")) { + // Orca-Belt: belt printers use the belt purge tower instead of the classic + // wipe tower — show its enable only on belt printers, and hide the classic + // wipe-tower fields there (the classic tower's G-code bypasses the belt transform). + const bool is_belt_printer = m_config->opt_bool("belt_printer"); + toggle_line("enable_belt_purge_tower", is_belt_printer); + const bool supports_wipe_tower_2 = !is_BBL_printer && m_config->opt_enum("wipe_tower_type") == WipeTowerType::Type2; - toggle_line("wipe_tower_type", !is_BBL_printer); + toggle_line("wipe_tower_type", !is_BBL_printer && !is_belt_printer); // SoftFever: hide specific settings for BBL printer for (auto el : { "enable_filament_ramming", From 5ea6ccc56a7a5664783475603b4e9006e027afb2 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 14:35:00 -0500 Subject: [PATCH 12/15] cleanup, early purge tower stop if no longer necessary --- src/libslic3r/BeltPurge.cpp | 316 +++++++++++++++++++++++++++++ src/libslic3r/CMakeLists.txt | 1 + src/libslic3r/Print.cpp | 231 --------------------- src/libslic3r/PrintObjectSlice.cpp | 45 ---- 4 files changed, 317 insertions(+), 276 deletions(-) create mode 100644 src/libslic3r/BeltPurge.cpp diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp new file mode 100644 index 0000000000..a9c2ffceb8 --- /dev/null +++ b/src/libslic3r/BeltPurge.cpp @@ -0,0 +1,316 @@ +// ORCA-Belt: backend of the belt purge tower (the belt replacement for the +// classic wipe/prime tower). +// +// Kept in its own translation unit so the belt-purge logic stays out of the way +// of unrelated upstream changes to Print.cpp / PrintObjectSlice.cpp and carries +// no regression risk for normal printers: none of these methods do anything +// unless the print is a belt printer with the belt purge tower enabled. +// +// Print::has_belt_purge_tower() - is the belt purge tower active? +// Print::_align_belt_purge_layers() - snap the prism's layer grid onto the +// printed objects' grid +// Print::_plan_belt_purge() - route filament-change purging into the +// prism (flush-into-objects), no wipe tower +// PrintObject::belt_shift_layer_grid() - shift a sliced layer grid +// PrintObject::belt_truncate_layers_above() - cancel the prism past the last swap +// +// (Declarations live in Print.hpp alongside the rest of the Print interface.) + +#include "Print.hpp" +#include "PrintConfig.hpp" +#include "Exception.hpp" +#include "GCode/ToolOrdering.hpp" +#include "Layer.hpp" +#include "I18N.hpp" +#include "format.hpp" +#include "LocalesUtils.hpp" +#include "libslic3r.h" + +#include +#include +#include + +#include + +namespace Slic3r { + +// Belt purge prism: purging after filament changes is routed into a sliced +// prism object via the flush-into-objects machinery instead of a wipe tower. +bool Print::has_belt_purge_tower() const +{ + // Its own purge-tower "type", gated by the belt-only enable_belt_purge_tower + // option (not the classic enable_prime_tower). + return m_config.belt_printer.value + && m_config.enable_belt_purge_tower.value + && !m_config.spiral_mode.value + && m_config.filament_diameter.values.size() > 1; +} + +// Belt mode: snap the purge prism's layer grid onto the printed objects' grid. +// After belt slicing every object's layer print_z carries a per-object global +// z offset (mesh-vertex-scan belt_z_shift + instance-Y-dependent terms), so +// objects at different belt-Y positions do not share a layer grid. Purge +// marking looks absorbers up with get_layer_at_printz(lt.print_z, EPSILON), +// so the prism only absorbs purge at toolchange print_z values that coincide +// with one of its own layers. Shifting the prism by at most half a layer +// (a sub-layer-height displacement along the belt) makes its grid residue +// match the reference object's; both grids step by the same layer height +// (enforced by validate()), so matching residues means exact layer matches. +void Print::_align_belt_purge_layers() +{ + PrintObject *prism = nullptr; + for (PrintObject *po : m_objects) + if (po->config().belt_purge_tower_object.value) { + prism = po; + break; + } + if (prism == nullptr || prism->layers().empty()) + return; + + const double h = prism->config().layer_height.value; + if (h <= EPSILON) + return; + + // Grid residue of an object's layer grid: identical for all of an object's + // layers above the first since they step by h. + auto grid_offset = [h](const PrintObject *po) { + const double z = po->layers().front()->print_z; + return z - std::floor(z / h) * h; // in [0, h) + }; + + // Reference grid: the tallest non-prism object (proxy for the object with + // the most toolchange layers). + const PrintObject *ref = nullptr; + double ref_top = -std::numeric_limits::max(); + for (const PrintObject *po : m_objects) { + if (po->config().belt_purge_tower_object.value || po->layers().empty()) + continue; + const double top = po->layers().back()->print_z; + if (top > ref_top) { + ref_top = top; + ref = po; + } + } + if (ref == nullptr) + return; + + const double ref_offset = grid_offset(ref); + bool grids_mismatch = false; + for (const PrintObject *po : m_objects) { + if (po == ref || po->config().belt_purge_tower_object.value || po->layers().empty()) + continue; + double d = std::abs(grid_offset(po) - ref_offset); + d = std::min(d, h - d); + if (d > 5. * EPSILON) { + grids_mismatch = true; + break; + } + } + + // Shift normalized to (-h/2, h/2]. + double delta = ref_offset - grid_offset(prism); + if (delta > 0.5 * h) + delta -= h; + else if (delta <= -0.5 * h) + delta += h; + + BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] purge prism grid snap" + << " ref=" << ref->model_object()->name + << " ref_offset=" << ref_offset + << " prism_offset=" << grid_offset(prism) + << " delta=" << delta + << " grids_mismatch=" << grids_mismatch; + + prism->belt_shift_layer_grid(delta); + + if (grids_mismatch) + this->active_step_add_warning( + PrintStateBase::WarningLevel::NON_CRITICAL, + _u8L("Objects on the plate are sliced on different layer grids; the belt purge tower can only follow " + "one of them. Filament changes on layers of the other objects may not be fully purged.")); +} + +// Belt mode replacement for _make_wipe_tower(): plan filament-change purging +// into the belt purge prism (and any other flush_into_* object) using the +// flush-into-objects machinery, without generating classic wipe tower G-code. +// The toolchange itself is emitted by GCode::set_extruder() via the +// change_filament_gcode macro; the overrides marked here make the new +// filament's first extrusions land in the purge prism. +void Print::_plan_belt_purge() +{ + m_wipe_tower_data.clear(); + + // Must run before ToolOrdering is built: LayerTools merge per-object layer + // print_z values, and the prism only absorbs purge where its (snapped) + // layers coincide with the toolchange layers. + this->_align_belt_purge_layers(); + + const unsigned int number_of_extruders = (unsigned int) m_config.filament_colour.values.size(); + + // No initial priming extrusions: there is no tower to prime on. + m_wipe_tower_data.tool_ordering = ToolOrdering(*this, (unsigned int) -1, false); + m_wipe_tower_data.tool_ordering.sort_and_build_data(*this, (unsigned int) -1, false); + + if (m_wipe_tower_data.tool_ordering.empty() || m_wipe_tower_data.tool_ordering.last_extruder() == unsigned(-1)) + throw Slic3r::SlicingError("The print is empty. The model is not printable with current print settings."); + + if (!m_wipe_tower_data.tool_ordering.has_wipe_tower()) + // No toolchanges anywhere, nothing to purge. + return; + + this->throw_if_canceled(); + + // Flush volumes per filament pair, mirroring the generic wipe tower path: + // full flush matrix for single extruder multi material with purging enabled, + // plain prime volume otherwise. + std::vector flush_matrix(cast( + get_flush_volumes_matrix(m_config.flush_volumes_matrix.values, 0, m_config.nozzle_diameter.values.size()))); + std::vector> wipe_volumes; + for (unsigned int i = 0; i < number_of_extruders; ++i) + wipe_volumes.push_back(std::vector(flush_matrix.begin() + i * number_of_extruders, + flush_matrix.begin() + (i + 1) * number_of_extruders)); + const bool use_flush_matrix = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material; + const float flush_multiplier = (float) m_config.flush_multiplier.get_at(0); + + // Cancel the purge prism early: pre-scan the tool ordering for the highest + // print_z that actually has a toolchange, then drop the prism's layers above + // it so the tower stops at the last color swap (saves filament/time). This + // MUST happen before the marking loop below: ensure_perimeters_infills_order + // force-overrides the prism's extrusions on every layer (it is a dedicated + // flush object), so truncating afterwards would leave dangling overrides + // pointing into deleted layers. + { + double last_tc_z = -1.; + unsigned int cur_ext = m_wipe_tower_data.tool_ordering.first_extruder(); + for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools()) + for (const unsigned int e : lt.extruders) + if (e != cur_ext) { last_tc_z = lt.print_z; cur_ext = e; } + if (last_tc_z >= 0.) + for (PrintObject *po : m_objects) + if (po->config().belt_purge_tower_object.value) { + po->belt_truncate_layers_above(last_tc_z); + break; + } + } + + // Diagnostic: the prism only absorbs purge at toolchange layers whose + // print_z coincides with one of its own layers. Compare the prism's layer + // print_z range to the toolchange print_z range and count how many + // toolchange layers actually land on a prism layer. This distinguishes a + // range/grid-alignment failure (no coverage) from a capacity shortfall + // (covered but not enough cross-section). + const PrintObject *diag_prism = nullptr; + for (const PrintObject *po : m_objects) + if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { diag_prism = po; break; } + if (diag_prism != nullptr) + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge prism layer range print_z=[" + << diag_prism->layers().front()->print_z << ", " << diag_prism->layers().back()->print_z + << "] nlayers=" << diag_prism->layers().size(); + int tc_layers = 0, tc_layers_covered = 0; + + float total_leftover = 0.f; + float worst_layer_leftover = 0.f; + double worst_layer_z = 0.; + + unsigned int current_extruder_id = m_wipe_tower_data.tool_ordering.first_extruder(); + for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) { + float layer_leftover = 0.f; + bool layer_has_tc = false; + for (const unsigned int extruder_id : layer_tools.extruders) { + if (extruder_id == current_extruder_id) + continue; + if (!layer_has_tc) { + layer_has_tc = true; + ++tc_layers; + if (diag_prism != nullptr && diag_prism->get_layer_at_printz(layer_tools.print_z, EPSILON) != nullptr) + ++tc_layers_covered; + } + float volume_to_wipe = use_flush_matrix ? + wipe_volumes[current_extruder_id][extruder_id] * flush_multiplier : + (float) m_config.prime_volume; + float leftover = layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_extruder_id, extruder_id, + volume_to_wipe); + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] purge toolchange print_z=" << layer_tools.print_z + << " filament " << current_extruder_id << "->" << extruder_id + << " requested=" << volume_to_wipe + << " absorbed=" << volume_to_wipe - leftover + << " leftover=" << leftover; + layer_leftover += leftover; + current_extruder_id = extruder_id; + } + layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this); + if (layer_leftover > 0.f) { + total_leftover += layer_leftover; + if (layer_leftover > worst_layer_leftover) { + worst_layer_leftover = layer_leftover; + worst_layer_z = layer_tools.print_z; + } + } + this->throw_if_canceled(); + } + + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge coverage: " << tc_layers_covered << "/" << tc_layers + << " toolchange layers land on a prism layer" + << (tc_layers > 0 && tc_layers_covered == 0 ? " (RANGE/GRID MISALIGNMENT — prism absorbs nothing)" : + tc_layers_covered < tc_layers ? " (partial coverage)" : " (full coverage)"); + + if (total_leftover > 1.f) { + this->active_step_add_warning( + PrintStateBase::WarningLevel::CRITICAL, + Slic3r::format(_u8L("The belt purge tower cannot absorb the full purge volume: %1% mm³ in total could not " + "be purged (worst layer: %2% mm³ at height %3%). The print may show color bleeding. " + "Increase the belt purge tower width, or reduce flushing volumes."), + int(std::ceil(total_leftover)), int(std::ceil(worst_layer_leftover)), + Slic3r::float_to_string_decimal_point(worst_layer_z, 2))); + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge planning leftover total=" << total_leftover + << " worst_layer=" << worst_layer_leftover << " at print_z=" << worst_layer_z; + } +} + +// Belt mode: shift the sliced layer grid by delta. Mirrors the global_z_offset +// application in slice() — layer print_z and belt_floor_z_shift move together +// so belt floor clipping stays consistent with the shifted grid. Used by +// Print::_align_belt_purge_layers() to snap the purge prism onto the printed +// objects' layer grid; |delta| <= half a layer height, i.e. a sub-layer shift +// of the prism along the belt. +void PrintObject::belt_shift_layer_grid(double delta) +{ + if (std::abs(delta) < EPSILON) + return; + for (Layer *layer : m_layers) + layer->print_z += delta; + for (SupportLayer *layer : m_support_layers) + layer->print_z += delta; + m_slicing_params.belt_floor_z_shift += delta; + BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] belt_shift_layer_grid" + << " obj=" << this->model_object()->name + << " delta=" << delta + << " first_layer.print_z=" << (m_layers.empty() ? 0. : m_layers.front()->print_z); +} + +// Belt mode: drop layers strictly above z (used to cancel the purge prism early +// once there are no more toolchanges above z, so the tower stops at the last +// color swap instead of wasting filament up the rest of the belt). Each layer's +// cross-section is already sliced, so removing upper layers does not affect the +// last toolchange's coverage. Deletes the Layer objects and clears the new top +// layer's upper-layer link. Returns the number of layers removed. +size_t PrintObject::belt_truncate_layers_above(coordf_t z) +{ + size_t keep = m_layers.size(); + while (keep > 0 && m_layers[keep - 1]->print_z > z + EPSILON) + --keep; + if (keep >= m_layers.size()) + return 0; + const size_t removed = m_layers.size() - keep; + for (size_t i = keep; i < m_layers.size(); ++i) + delete m_layers[i]; + m_layers.resize(keep); + if (!m_layers.empty()) + m_layers.back()->upper_layer = nullptr; + BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] truncate purge prism above print_z=" << z + << " kept=" << keep << " removed=" << removed + << " new_top=" << (m_layers.empty() ? 0. : m_layers.back()->print_z); + return removed; +} + +} // namespace Slic3r diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index c8796265db..9ca113e958 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -86,6 +86,7 @@ set(lisbslic3r_sources BeltGCode.hpp BeltGCodeWriter.cpp BeltGCodeWriter.hpp + BeltPurge.cpp BeltSliceStrategy.cpp BeltSliceStrategy.hpp BeltTransform.cpp diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 4004cd81de..922168ccb8 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -4126,17 +4126,6 @@ bool Print::has_wipe_tower() const return false; } -// Belt purge prism: purging after filament changes is routed into a sliced -// prism object via the flush-into-objects machinery instead of a wipe tower. -bool Print::has_belt_purge_tower() const -{ - // Its own purge-tower "type", gated by the belt-only enable_belt_purge_tower - // option (not the classic enable_prime_tower). - return m_config.belt_printer.value - && m_config.enable_belt_purge_tower.value - && !m_config.spiral_mode.value - && m_config.filament_diameter.values.size() > 1; -} const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const { @@ -4224,226 +4213,6 @@ bool Print::enable_timelapse_print() const return m_config.timelapse_type.value == TimelapseType::tlSmooth; } -// Belt mode: snap the purge prism's layer grid onto the printed objects' grid. -// After belt slicing every object's layer print_z carries a per-object global -// z offset (mesh-vertex-scan belt_z_shift + instance-Y-dependent terms), so -// objects at different belt-Y positions do not share a layer grid. Purge -// marking looks absorbers up with get_layer_at_printz(lt.print_z, EPSILON), -// so the prism only absorbs purge at toolchange print_z values that coincide -// with one of its own layers. Shifting the prism by at most half a layer -// (a sub-layer-height displacement along the belt) makes its grid residue -// match the reference object's; both grids step by the same layer height -// (enforced by validate()), so matching residues means exact layer matches. -void Print::_align_belt_purge_layers() -{ - PrintObject *prism = nullptr; - for (PrintObject *po : m_objects) - if (po->config().belt_purge_tower_object.value) { - prism = po; - break; - } - if (prism == nullptr || prism->layers().empty()) - return; - - const double h = prism->config().layer_height.value; - if (h <= EPSILON) - return; - - // Grid residue of an object's layer grid: identical for all of an object's - // layers above the first since they step by h. - auto grid_offset = [h](const PrintObject *po) { - const double z = po->layers().front()->print_z; - return z - std::floor(z / h) * h; // in [0, h) - }; - - // Reference grid: the tallest non-prism object (proxy for the object with - // the most toolchange layers). - const PrintObject *ref = nullptr; - double ref_top = -std::numeric_limits::max(); - for (const PrintObject *po : m_objects) { - if (po->config().belt_purge_tower_object.value || po->layers().empty()) - continue; - const double top = po->layers().back()->print_z; - if (top > ref_top) { - ref_top = top; - ref = po; - } - } - if (ref == nullptr) - return; - - const double ref_offset = grid_offset(ref); - bool grids_mismatch = false; - for (const PrintObject *po : m_objects) { - if (po == ref || po->config().belt_purge_tower_object.value || po->layers().empty()) - continue; - double d = std::abs(grid_offset(po) - ref_offset); - d = std::min(d, h - d); - if (d > 5. * EPSILON) { - grids_mismatch = true; - break; - } - } - - // Shift normalized to (-h/2, h/2]. - double delta = ref_offset - grid_offset(prism); - if (delta > 0.5 * h) - delta -= h; - else if (delta <= -0.5 * h) - delta += h; - - BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] purge prism grid snap" - << " ref=" << ref->model_object()->name - << " ref_offset=" << ref_offset - << " prism_offset=" << grid_offset(prism) - << " delta=" << delta - << " grids_mismatch=" << grids_mismatch; - - prism->belt_shift_layer_grid(delta); - - if (grids_mismatch) - this->active_step_add_warning( - PrintStateBase::WarningLevel::NON_CRITICAL, - _u8L("Objects on the plate are sliced on different layer grids; the belt purge tower can only follow " - "one of them. Filament changes on layers of the other objects may not be fully purged.")); -} - -// Belt mode replacement for _make_wipe_tower(): plan filament-change purging -// into the belt purge prism (and any other flush_into_* object) using the -// flush-into-objects machinery, without generating classic wipe tower G-code. -// The toolchange itself is emitted by GCode::set_extruder() via the -// change_filament_gcode macro; the overrides marked here make the new -// filament's first extrusions land in the purge prism. -void Print::_plan_belt_purge() -{ - m_wipe_tower_data.clear(); - - // Must run before ToolOrdering is built: LayerTools merge per-object layer - // print_z values, and the prism only absorbs purge where its (snapped) - // layers coincide with the toolchange layers. - this->_align_belt_purge_layers(); - - const unsigned int number_of_extruders = (unsigned int) m_config.filament_colour.values.size(); - - // No initial priming extrusions: there is no tower to prime on. - m_wipe_tower_data.tool_ordering = ToolOrdering(*this, (unsigned int) -1, false); - m_wipe_tower_data.tool_ordering.sort_and_build_data(*this, (unsigned int) -1, false); - - if (m_wipe_tower_data.tool_ordering.empty() || m_wipe_tower_data.tool_ordering.last_extruder() == unsigned(-1)) - throw Slic3r::SlicingError("The print is empty. The model is not printable with current print settings."); - - if (!m_wipe_tower_data.tool_ordering.has_wipe_tower()) - // No toolchanges anywhere, nothing to purge. - return; - - this->throw_if_canceled(); - - // Flush volumes per filament pair, mirroring the generic wipe tower path: - // full flush matrix for single extruder multi material with purging enabled, - // plain prime volume otherwise. - std::vector flush_matrix(cast( - get_flush_volumes_matrix(m_config.flush_volumes_matrix.values, 0, m_config.nozzle_diameter.values.size()))); - std::vector> wipe_volumes; - for (unsigned int i = 0; i < number_of_extruders; ++i) - wipe_volumes.push_back(std::vector(flush_matrix.begin() + i * number_of_extruders, - flush_matrix.begin() + (i + 1) * number_of_extruders)); - const bool use_flush_matrix = m_config.purge_in_prime_tower && m_config.single_extruder_multi_material; - const float flush_multiplier = (float) m_config.flush_multiplier.get_at(0); - - // Cancel the purge prism early: pre-scan the tool ordering for the highest - // print_z that actually has a toolchange, then drop the prism's layers above - // it so the tower stops at the last color swap (saves filament/time). This - // MUST happen before the marking loop below: ensure_perimeters_infills_order - // force-overrides the prism's extrusions on every layer (it is a dedicated - // flush object), so truncating afterwards would leave dangling overrides - // pointing into deleted layers. - { - double last_tc_z = -1.; - unsigned int cur_ext = m_wipe_tower_data.tool_ordering.first_extruder(); - for (const auto < : m_wipe_tower_data.tool_ordering.layer_tools()) - for (const unsigned int e : lt.extruders) - if (e != cur_ext) { last_tc_z = lt.print_z; cur_ext = e; } - if (last_tc_z >= 0.) - for (PrintObject *po : m_objects) - if (po->config().belt_purge_tower_object.value) { - po->belt_truncate_layers_above(last_tc_z); - break; - } - } - - // Diagnostic: the prism only absorbs purge at toolchange layers whose - // print_z coincides with one of its own layers. Compare the prism's layer - // print_z range to the toolchange print_z range and count how many - // toolchange layers actually land on a prism layer. This distinguishes a - // range/grid-alignment failure (no coverage) from a capacity shortfall - // (covered but not enough cross-section). - const PrintObject *diag_prism = nullptr; - for (const PrintObject *po : m_objects) - if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { diag_prism = po; break; } - if (diag_prism != nullptr) - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge prism layer range print_z=[" - << diag_prism->layers().front()->print_z << ", " << diag_prism->layers().back()->print_z - << "] nlayers=" << diag_prism->layers().size(); - int tc_layers = 0, tc_layers_covered = 0; - - float total_leftover = 0.f; - float worst_layer_leftover = 0.f; - double worst_layer_z = 0.; - - unsigned int current_extruder_id = m_wipe_tower_data.tool_ordering.first_extruder(); - for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) { - float layer_leftover = 0.f; - bool layer_has_tc = false; - for (const unsigned int extruder_id : layer_tools.extruders) { - if (extruder_id == current_extruder_id) - continue; - if (!layer_has_tc) { - layer_has_tc = true; - ++tc_layers; - if (diag_prism != nullptr && diag_prism->get_layer_at_printz(layer_tools.print_z, EPSILON) != nullptr) - ++tc_layers_covered; - } - float volume_to_wipe = use_flush_matrix ? - wipe_volumes[current_extruder_id][extruder_id] * flush_multiplier : - (float) m_config.prime_volume; - float leftover = layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_extruder_id, extruder_id, - volume_to_wipe); - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] purge toolchange print_z=" << layer_tools.print_z - << " filament " << current_extruder_id << "->" << extruder_id - << " requested=" << volume_to_wipe - << " absorbed=" << volume_to_wipe - leftover - << " leftover=" << leftover; - layer_leftover += leftover; - current_extruder_id = extruder_id; - } - layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this); - if (layer_leftover > 0.f) { - total_leftover += layer_leftover; - if (layer_leftover > worst_layer_leftover) { - worst_layer_leftover = layer_leftover; - worst_layer_z = layer_tools.print_z; - } - } - this->throw_if_canceled(); - } - - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge coverage: " << tc_layers_covered << "/" << tc_layers - << " toolchange layers land on a prism layer" - << (tc_layers > 0 && tc_layers_covered == 0 ? " (RANGE/GRID MISALIGNMENT — prism absorbs nothing)" : - tc_layers_covered < tc_layers ? " (partial coverage)" : " (full coverage)"); - - if (total_leftover > 1.f) { - this->active_step_add_warning( - PrintStateBase::WarningLevel::CRITICAL, - Slic3r::format(_u8L("The belt purge tower cannot absorb the full purge volume: %1% mm³ in total could not " - "be purged (worst layer: %2% mm³ at height %3%). The print may show color bleeding. " - "Increase the belt purge tower width, or reduce flushing volumes."), - int(std::ceil(total_leftover)), int(std::ceil(worst_layer_leftover)), - Slic3r::float_to_string_decimal_point(worst_layer_z, 2))); - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge planning leftover total=" << total_leftover - << " worst_layer=" << worst_layer_leftover << " at print_z=" << worst_layer_z; - } -} void Print::_make_wipe_tower() { diff --git a/src/libslic3r/PrintObjectSlice.cpp b/src/libslic3r/PrintObjectSlice.cpp index f13b208a3a..b65c79d0eb 100644 --- a/src/libslic3r/PrintObjectSlice.cpp +++ b/src/libslic3r/PrintObjectSlice.cpp @@ -1392,51 +1392,6 @@ void apply_fuzzy_skin_segmentation(PrintObject &print_object, ThrowOnCancel thro }); // end of parallel_for } -// Belt mode: shift the sliced layer grid by delta. Mirrors the global_z_offset -// application in slice() — layer print_z and belt_floor_z_shift move together -// so belt floor clipping stays consistent with the shifted grid. Used by -// Print::_align_belt_purge_layers() to snap the purge prism onto the printed -// objects' layer grid; |delta| <= half a layer height, i.e. a sub-layer shift -// of the prism along the belt. -void PrintObject::belt_shift_layer_grid(double delta) -{ - if (std::abs(delta) < EPSILON) - return; - for (Layer *layer : m_layers) - layer->print_z += delta; - for (SupportLayer *layer : m_support_layers) - layer->print_z += delta; - m_slicing_params.belt_floor_z_shift += delta; - BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] belt_shift_layer_grid" - << " obj=" << this->model_object()->name - << " delta=" << delta - << " first_layer.print_z=" << (m_layers.empty() ? 0. : m_layers.front()->print_z); -} - -// Belt mode: drop layers strictly above z (used to cancel the purge prism early -// once there are no more toolchanges above z, so the tower stops at the last -// color swap instead of wasting filament up the rest of the belt). Each layer's -// cross-section is already sliced, so removing upper layers does not affect the -// last toolchange's coverage. Deletes the Layer objects and clears the new top -// layer's upper-layer link. Returns the number of layers removed. -size_t PrintObject::belt_truncate_layers_above(coordf_t z) -{ - size_t keep = m_layers.size(); - while (keep > 0 && m_layers[keep - 1]->print_z > z + EPSILON) - --keep; - if (keep >= m_layers.size()) - return 0; - const size_t removed = m_layers.size() - keep; - for (size_t i = keep; i < m_layers.size(); ++i) - delete m_layers[i]; - m_layers.resize(keep); - if (!m_layers.empty()) - m_layers.back()->upper_layer = nullptr; - BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] truncate purge prism above print_z=" << z - << " kept=" << keep << " removed=" << removed - << " new_top=" << (m_layers.empty() ? 0. : m_layers.back()->print_z); - return removed; -} // 1) Decides Z positions of the layers, // 2) Initializes layers and their regions From 3d270c2aa7eae0e7d7438f077985d4810c2d0d7e Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Wed, 17 Jun 2026 20:59:33 -0500 Subject: [PATCH 13/15] workable belt purge, via N-1 individual "purge objects" --- src/libslic3r/BeltPurge.cpp | 114 +++++++++++++++++++----------- src/slic3r/GUI/BeltPurgeTower.cpp | 62 ++++++++++++++-- 2 files changed, 126 insertions(+), 50 deletions(-) diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp index a9c2ffceb8..c69987c719 100644 --- a/src/libslic3r/BeltPurge.cpp +++ b/src/libslic3r/BeltPurge.cpp @@ -21,6 +21,8 @@ #include "Exception.hpp" #include "GCode/ToolOrdering.hpp" #include "Layer.hpp" +#include "ExtrusionEntity.hpp" +#include "ExtrusionEntityCollection.hpp" #include "I18N.hpp" #include "format.hpp" #include "LocalesUtils.hpp" @@ -46,16 +48,28 @@ bool Print::has_belt_purge_tower() const && m_config.filament_diameter.values.size() > 1; } -// Belt mode: snap the purge prism's layer grid onto the printed objects' grid. -// After belt slicing every object's layer print_z carries a per-object global -// z offset (mesh-vertex-scan belt_z_shift + instance-Y-dependent terms), so -// objects at different belt-Y positions do not share a layer grid. Purge -// marking looks absorbers up with get_layer_at_printz(lt.print_z, EPSILON), -// so the prism only absorbs purge at toolchange print_z values that coincide -// with one of its own layers. Shifting the prism by at most half a layer -// (a sub-layer-height displacement along the belt) makes its grid residue -// match the reference object's; both grids step by the same layer height -// (enforced by validate()), so matching residues means exact layer matches. +// Belt mode: align ALL objects on the plate (the printed objects AND the purge +// prism) onto one common layer grid, so the prism can absorb every toolchange. +// +// After belt slicing each object's layer print_z carries a per-object global z +// offset (mesh-vertex-scan belt_z_shift + instance-Y-dependent terms), so +// objects at different belt-Y positions get layer grids with DIFFERENT residues +// (mod layer height). Purge marking looks absorbers up with +// get_layer_at_printz(lt.print_z, EPSILON), so a toolchange on object B only +// absorbs into the prism if the prism has a layer at B's print_z. Snapping only +// the prism to one object therefore worked for a single (assembled) multi-color +// object but failed with multiple separate objects — the prism could follow only +// one grid, and toolchanges on the others went unabsorbed ("multiple layer +// grids" warning). +// +// Fix: pick one reference grid (the tallest object) and shift every object onto +// it. Each shift is at most half a layer height — a sub-100µm move along the +// belt, the very same mechanism the per-object global_z_offset already uses, and +// it keeps each object internally consistent (belt_shift_layer_grid moves the +// object's layers, its support layers, and its belt floor together). Equal layer +// height across objects is enforced by Print::validate(), so once residues match +// every object steps on the same lattice {ref_offset + k*h} and every toolchange +// layer coincides with a prism layer. void Print::_align_belt_purge_layers() { PrintObject *prism = nullptr; @@ -73,13 +87,15 @@ void Print::_align_belt_purge_layers() // Grid residue of an object's layer grid: identical for all of an object's // layers above the first since they step by h. - auto grid_offset = [h](const PrintObject *po) { + auto grid_offset = [h](const PrintObject *po) -> double { + if (po->layers().empty()) + return 0.; const double z = po->layers().front()->print_z; return z - std::floor(z / h) * h; // in [0, h) }; // Reference grid: the tallest non-prism object (proxy for the object with - // the most toolchange layers). + // the most toolchange layers — minimizes how far the rest must move). const PrintObject *ref = nullptr; double ref_top = -std::numeric_limits::max(); for (const PrintObject *po : m_objects) { @@ -94,40 +110,23 @@ void Print::_align_belt_purge_layers() if (ref == nullptr) return; - const double ref_offset = grid_offset(ref); - bool grids_mismatch = false; - for (const PrintObject *po : m_objects) { - if (po == ref || po->config().belt_purge_tower_object.value || po->layers().empty()) + const double ref_offset = grid_offset(ref); + + // Snap every object (printed objects AND the prism) onto the reference grid. + for (PrintObject *po : m_objects) { + if (po->layers().empty()) continue; - double d = std::abs(grid_offset(po) - ref_offset); - d = std::min(d, h - d); - if (d > 5. * EPSILON) { - grids_mismatch = true; - break; - } + double delta = ref_offset - grid_offset(po); + if (delta > 0.5 * h) + delta -= h; + else if (delta <= -0.5 * h) + delta += h; + po->belt_shift_layer_grid(delta); // no-op for the reference object (delta ~ 0) } - // Shift normalized to (-h/2, h/2]. - double delta = ref_offset - grid_offset(prism); - if (delta > 0.5 * h) - delta -= h; - else if (delta <= -0.5 * h) - delta += h; - - BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] purge prism grid snap" - << " ref=" << ref->model_object()->name - << " ref_offset=" << ref_offset - << " prism_offset=" << grid_offset(prism) - << " delta=" << delta - << " grids_mismatch=" << grids_mismatch; - - prism->belt_shift_layer_grid(delta); - - if (grids_mismatch) - this->active_step_add_warning( - PrintStateBase::WarningLevel::NON_CRITICAL, - _u8L("Objects on the plate are sliced on different layer grids; the belt purge tower can only follow " - "one of them. Filament changes on layers of the other objects may not be fully purged.")); + BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] purge grid align: snapped " << m_objects.size() + << " objects onto ref grid offset=" << ref_offset + << " (ref=" << ref->model_object()->name << ")"; } // Belt mode replacement for _make_wipe_tower(): plan filament-change purging @@ -228,8 +227,37 @@ void Print::_plan_belt_purge() float volume_to_wipe = use_flush_matrix ? wipe_volumes[current_extruder_id][extruder_id] * flush_multiplier : (float) m_config.prime_volume; + // DIAGNOSTIC (first dozen toolchanges, warning level): how much + // extrudable volume does the prism actually have at this layer, and + // how much of the requested purge did it absorb? Distinguishes "prism + // layer found but tiny/empty" from "found with geometry but marking + // didn't consume it". + const int swap_idx_on_layer = [&]{ int n = 0; for (unsigned int e : layer_tools.extruders) { if (e == extruder_id) break; ++n; } return n; }(); + double prism_vol = -1.; + { + if (const Layer *pl = diag_prism != nullptr ? diag_prism->get_layer_at_printz(layer_tools.print_z, EPSILON) : nullptr) { + prism_vol = 0.; + for (const LayerRegion *lr : pl->regions()) { + for (const ExtrusionEntity *ee : lr->fills.entities) prism_vol += ee->total_volume(); + for (const ExtrusionEntity *ee : lr->perimeters.entities) prism_vol += ee->total_volume(); + } + } + } float leftover = layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_extruder_id, extruder_id, volume_to_wipe); + // DIAGNOSTIC: log the toolchanges that FAIL to fully absorb (warning, + // capped) — these are the ones causing the leftover. + static thread_local int dbg_fail = 0; + if (leftover > 0.01f && dbg_fail < 20) { + ++dbg_fail; + BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge UNABSORBED print_z=" << layer_tools.print_z + << " filament " << current_extruder_id << "->" << extruder_id + << " swap#" << swap_idx_on_layer << " of " << layer_tools.extruders.size() << " extruders" + << " requested=" << volume_to_wipe + << " absorbed=" << volume_to_wipe - leftover + << " leftover=" << leftover + << " prism_layer_extrudable_vol=" << prism_vol; + } BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] purge toolchange print_z=" << layer_tools.print_z << " filament " << current_extruder_id << "->" << extruder_id << " requested=" << volume_to_wipe diff --git a/src/slic3r/GUI/BeltPurgeTower.cpp b/src/slic3r/GUI/BeltPurgeTower.cpp index 47ff6b6bd4..3be4fee3ff 100644 --- a/src/slic3r/GUI/BeltPurgeTower.cpp +++ b/src/slic3r/GUI/BeltPurgeTower.cpp @@ -55,9 +55,34 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object } }; - const auto &printer_config = wxGetApp().preset_bundle->printers.get_edited_preset().config; - const auto &print_config = wxGetApp().preset_bundle->prints.get_edited_preset().config; - auto &project_config = wxGetApp().preset_bundle->project_config; + // Cheap early-out for non-belt printers: only belt printers ever get a belt + // purge tower, and this runs on every background-process tick — so avoid the + // full_config() merge below unless this is actually a belt printer. (Also + // tidies up a stale prism if the user switched away from a belt printer.) + { + const auto *belt_pre = wxGetApp().preset_bundle->printers.get_edited_preset().config.option("belt_printer"); + if (belt_pre == nullptr || !belt_pre->value) { + sig = BeltPurgeSignature{}; + if (prism_idxs.empty()) + return false; + remove_prisms(prism_idxs); + return true; + } + } + + // Read every sizing input from the MERGED full config — the exact same + // config the backend slices with (BackgroundSlicingProcess::apply uses + // preset_bundle->full_config()). Reading from the individual presets caused + // GUI/backend mismatches: e.g. purge_in_prime_tower or a populated + // flush_volumes_matrix present in the merged config but false/empty in the + // preset the GUI happened to read, so the tower was sized for the small + // prime_volume instead of the real color-change flush and could not absorb + // it. The three names below alias the one merged config so the rest of the + // function is unchanged. + const DynamicPrintConfig full_cfg = wxGetApp().preset_bundle->full_config(); + const DynamicPrintConfig &printer_config = full_cfg; + const DynamicPrintConfig &print_config = full_cfg; + const DynamicPrintConfig &project_config = full_cfg; const auto *belt_opt = printer_config.option("belt_printer"); const bool belt = belt_opt != nullptr && belt_opt->value; @@ -238,21 +263,44 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object << " -> " << bed_ext_dbg.max.x() << "," << bed_ext_dbg.max.y() << "]" << " lat_center=" << lat_center << " belt=[" << belt_start << "," << belt_end << "]"; - // Orient the bar: long axis = belt travel, width = lateral, height = Z. - const double size_x = belt_is_y ? width : length; - const double size_y = belt_is_y ? length : width; const Vec3d desired_center(belt_is_y ? lat_center : belt_center, belt_is_y ? belt_center : lat_center, 0.5 * height); + // Build the prism as N DISCONNECTED sub-bars side by side across the belt, + // N = (filaments - 1) = the worst-case number of toolchanges on one layer. + // Why: mark_wiping_extrusions overrides whole extrusion-entity COLLECTIONS, + // and each disconnected island slices into its own infill collection. With a + // single solid box there is one collection per layer, so the FIRST toolchange + // on a layer grabs the entire collection (consuming all of it for one swap) + // and any further swaps on that layer find nothing left and go unabsorbed + // (the classic wipe tower hides this by spilling leftover into the real + // tower; the belt prism has no fallback). One island per simultaneous swap + // lets each swap claim its own island. Total lateral footprint stays `width` + // (each island width/N wide, separated by a small gap), so per-layer capacity + // per island ~= max_flush, matching the height sizing. + const int n_islands = std::max(1, (int) filaments.size() - 1); + const double gap = 2.0; + const double w_sub = std::max(1.0, (width - (n_islands - 1) * gap) / n_islands); + // --- (Re)create --------------------------------------------------------- if (!prism_idxs.empty()) remove_prisms(prism_idxs); + TriangleMesh prism_mesh; + for (int i = 0; i < n_islands; ++i) { + const double lat_off = i * (w_sub + gap); + // Box dims: lateral = w_sub, along-belt = length, vertical = height. + TriangleMesh box = belt_is_y ? make_cube(w_sub, length, height) // X = lateral, Y = belt + : make_cube(length, w_sub, height); // X = belt, Y = lateral + box.translate(belt_is_y ? Vec3f((float) lat_off, 0.f, 0.f) : Vec3f(0.f, (float) lat_off, 0.f)); + prism_mesh.merge(box); + } + ModelObject *new_object = model.add_object(); new_object->name = _u8L("Belt Purge Tower"); new_object->add_instance(); - ModelVolume *new_volume = new_object->add_volume(make_cube(size_x, size_y, height)); + ModelVolume *new_volume = new_object->add_volume(std::move(prism_mesh)); new_volume->name = new_object->name; auto &cfg = new_object->config; From ee3e014f02249cb3244fb1dae21d2ac915c97349 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Thu, 18 Jun 2026 09:35:30 -0500 Subject: [PATCH 14/15] allow belt purge to skip unnecessary purge volume --- src/libslic3r/BeltPurge.cpp | 63 ++++++++++++++-------------- src/libslic3r/GCode/ToolOrdering.hpp | 14 ++++--- src/slic3r/GUI/BeltPurgeTower.cpp | 8 +++- 3 files changed, 46 insertions(+), 39 deletions(-) diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp index c69987c719..c51c5511c6 100644 --- a/src/libslic3r/BeltPurge.cpp +++ b/src/libslic3r/BeltPurge.cpp @@ -198,9 +198,10 @@ void Print::_plan_belt_purge() // toolchange layers actually land on a prism layer. This distinguishes a // range/grid-alignment failure (no coverage) from a capacity shortfall // (covered but not enough cross-section). - const PrintObject *diag_prism = nullptr; - for (const PrintObject *po : m_objects) - if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { diag_prism = po; break; } + PrintObject *prism_po = nullptr; + for (PrintObject *po : m_objects) + if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { prism_po = po; break; } + const PrintObject *diag_prism = prism_po; if (diag_prism != nullptr) BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge prism layer range print_z=[" << diag_prism->layers().front()->print_z << ", " << diag_prism->layers().back()->print_z @@ -227,37 +228,8 @@ void Print::_plan_belt_purge() float volume_to_wipe = use_flush_matrix ? wipe_volumes[current_extruder_id][extruder_id] * flush_multiplier : (float) m_config.prime_volume; - // DIAGNOSTIC (first dozen toolchanges, warning level): how much - // extrudable volume does the prism actually have at this layer, and - // how much of the requested purge did it absorb? Distinguishes "prism - // layer found but tiny/empty" from "found with geometry but marking - // didn't consume it". - const int swap_idx_on_layer = [&]{ int n = 0; for (unsigned int e : layer_tools.extruders) { if (e == extruder_id) break; ++n; } return n; }(); - double prism_vol = -1.; - { - if (const Layer *pl = diag_prism != nullptr ? diag_prism->get_layer_at_printz(layer_tools.print_z, EPSILON) : nullptr) { - prism_vol = 0.; - for (const LayerRegion *lr : pl->regions()) { - for (const ExtrusionEntity *ee : lr->fills.entities) prism_vol += ee->total_volume(); - for (const ExtrusionEntity *ee : lr->perimeters.entities) prism_vol += ee->total_volume(); - } - } - } float leftover = layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_extruder_id, extruder_id, volume_to_wipe); - // DIAGNOSTIC: log the toolchanges that FAIL to fully absorb (warning, - // capped) — these are the ones causing the leftover. - static thread_local int dbg_fail = 0; - if (leftover > 0.01f && dbg_fail < 20) { - ++dbg_fail; - BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge UNABSORBED print_z=" << layer_tools.print_z - << " filament " << current_extruder_id << "->" << extruder_id - << " swap#" << swap_idx_on_layer << " of " << layer_tools.extruders.size() << " extruders" - << " requested=" << volume_to_wipe - << " absorbed=" << volume_to_wipe - leftover - << " leftover=" << leftover - << " prism_layer_extrudable_vol=" << prism_vol; - } BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] purge toolchange print_z=" << layer_tools.print_z << " filament " << current_extruder_id << "->" << extruder_id << " requested=" << volume_to_wipe @@ -266,6 +238,33 @@ void Print::_plan_belt_purge() layer_leftover += leftover; current_extruder_id = extruder_id; } + + // Plastic saving: drop the prism's infill that no toolchange on this + // layer claimed. At this point (after the real-purge marking, before the + // force-override pass below) the prism's OVERRIDDEN fills are exactly the + // purge; the rest is infill that would otherwise print as the prism's own + // filament for nothing — which is most of the prism on layers with little + // or no purge. We keep perimeters so the bar's wall shell stays + // continuous along the belt. (Must run before ensure_perimeters_infills_ + // order, which force-overrides every remaining fill and would make them + // all look "claimed".) + if (prism_po != nullptr) { + if (Layer *pl = prism_po->get_layer_at_printz(layer_tools.print_z, EPSILON)) { + const auto &we = layer_tools.wiping_extrusions(); + for (LayerRegion *lr : pl->regions()) { + auto &ents = lr->fills.entities; + size_t keep = 0; + for (size_t r = 0; r < ents.size(); ++r) { + if (we.is_entity_overridden(ents[r], prism_po, 0)) + ents[keep++] = ents[r]; + else + delete ents[r]; + } + ents.resize(keep); + } + } + } + layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this); if (layer_leftover > 0.f) { total_leftover += layer_leftover; diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index 0d7526c72e..f2071e75b7 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -45,6 +45,14 @@ public: void ensure_perimeters_infills_order(const Print& print); + // Returns true if entity is not printed with its usual extruder for a given copy + // (i.e. it was claimed as a wiping/purge extrusion). Used by the belt purge + // tower to tell which prism fills carry purge vs. which are unclaimed waste. + bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const { + auto it = entity_map.find(std::make_tuple(entity, object)); + return it == entity_map.end() ? false : it->second[copy_id] != -1; + } + bool is_overriddable(const ExtrusionEntityCollection& ee, const PrintConfig& print_config, const PrintObject& object, const PrintRegion& region) const; bool is_overriddable_and_mark(const ExtrusionEntityCollection& ee, const PrintConfig& print_config, const PrintObject& object, const PrintRegion& region) { bool out = this->is_overriddable(ee, print_config, object, region); @@ -80,12 +88,6 @@ private: void set_support_extruder_override(const PrintObject* object, size_t copy_id, int extruder, size_t num_of_copies); void set_support_interface_extruder_override(const PrintObject* object, size_t copy_id, int extruder, size_t num_of_copies); - // Returns true in case that entity is not printed with its usual extruder for a given copy: - bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const { - auto it = entity_map.find(std::make_tuple(entity, object)); - return it == entity_map.end() ? false : it->second[copy_id] != -1; - } - std::map, ExtruderPerCopy> entity_map; // to keep track of who prints what // BBS std::map support_map; diff --git a/src/slic3r/GUI/BeltPurgeTower.cpp b/src/slic3r/GUI/BeltPurgeTower.cpp index 3be4fee3ff..f04f311d6b 100644 --- a/src/slic3r/GUI/BeltPurgeTower.cpp +++ b/src/slic3r/GUI/BeltPurgeTower.cpp @@ -280,7 +280,13 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object // (each island width/N wide, separated by a small gap), so per-layer capacity // per island ~= max_flush, matching the height sizing. const int n_islands = std::max(1, (int) filaments.size() - 1); - const double gap = 2.0; + // Minimal gap between sub-bars: they must stay just-separated so the slicer + // keeps them as distinct islands (hence distinct infill collections, one per + // simultaneous swap). Zero gap would union them into one collection and + // reintroduce the multi-swap-per-layer absorption bug; a hair over ~2 line + // widths also keeps gap-fill from bridging them. 1 mm is about as close as + // they can butt up while staying individually purgeable. + const double gap = 1.0; const double w_sub = std::max(1.0, (width - (n_islands - 1) * gap) / n_islands); // --- (Re)create --------------------------------------------------------- From 99ee8893cd4dea6c313be5c6f78f940717163072 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 8 Aug 2026 17:14:26 -0500 Subject: [PATCH 15/15] Fix belt purge tower activation and placement safety --- result.json | 10 -------- src/libslic3r/BeltPurge.cpp | 13 +++++++---- src/libslic3r/GCode/ToolOrdering.hpp | 2 +- src/libslic3r/Print.cpp | 4 ++-- src/slic3r/GUI/BeltPurgeTower.cpp | 16 +++++++++---- src/slic3r/GUI/BeltPurgeTower.hpp | 9 +++---- tests/fff_print/test_print.cpp | 35 ++++++++++++++++++++++++++++ 7 files changed, 62 insertions(+), 27 deletions(-) delete mode 100644 result.json diff --git a/result.json b/result.json deleted file mode 100644 index 517f6bffa5..0000000000 --- a/result.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "error_string": "Success.", - "export_time": 0, - "layer_height": 0.0, - "plate_index": 0, - "prepare_time": 0, - "return_code": 0, - "sparse_infill_density": 0.0, - "wall_loops": 0 -} diff --git a/src/libslic3r/BeltPurge.cpp b/src/libslic3r/BeltPurge.cpp index c51c5511c6..3e261a1882 100644 --- a/src/libslic3r/BeltPurge.cpp +++ b/src/libslic3r/BeltPurge.cpp @@ -42,10 +42,15 @@ bool Print::has_belt_purge_tower() const { // Its own purge-tower "type", gated by the belt-only enable_belt_purge_tower // option (not the classic enable_prime_tower). - return m_config.belt_printer.value - && m_config.enable_belt_purge_tower.value - && !m_config.spiral_mode.value - && m_config.filament_diameter.values.size() > 1; + if (!(m_config.belt_printer.value + && m_config.enable_belt_purge_tower.value + && !m_config.spiral_mode.value + && m_config.filament_diameter.values.size() > 1)) + return false; + + return std::any_of(m_objects.begin(), m_objects.end(), [](const PrintObject *object) { + return object->config().belt_purge_tower_object.value; + }); } // Belt mode: align ALL objects on the plate (the printed objects AND the purge diff --git a/src/libslic3r/GCode/ToolOrdering.hpp b/src/libslic3r/GCode/ToolOrdering.hpp index f2071e75b7..52778690bb 100644 --- a/src/libslic3r/GCode/ToolOrdering.hpp +++ b/src/libslic3r/GCode/ToolOrdering.hpp @@ -50,7 +50,7 @@ public: // tower to tell which prism fills carry purge vs. which are unclaimed waste. bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const { auto it = entity_map.find(std::make_tuple(entity, object)); - return it == entity_map.end() ? false : it->second[copy_id] != -1; + return it != entity_map.end() && copy_id < it->second.size() && it->second[copy_id] != -1; } bool is_overriddable(const ExtrusionEntityCollection& ee, const PrintConfig& print_config, const PrintObject& object, const PrintRegion& region) const; diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 922168ccb8..7cec3de5eb 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -1477,13 +1477,13 @@ StringObjectException Print::validate(std::vector *warnin if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value && m_config.print_sequence == PrintSequence::ByObject - && extruders.size() > 1 && warning != nullptr) { + && extruders.size() > 1) { StringObjectException warningtemp; warningtemp.string = L("The belt purge tower is not generated in \"By object\" print sequence; " "filament changes will not be purged."); warningtemp.opt_key = "enable_belt_purge_tower"; warningtemp.is_warning = true; - *warning = warningtemp; + add_warning(warningtemp); } if (m_config.enable_prime_tower) { diff --git a/src/slic3r/GUI/BeltPurgeTower.cpp b/src/slic3r/GUI/BeltPurgeTower.cpp index f04f311d6b..cf1489d6a0 100644 --- a/src/slic3r/GUI/BeltPurgeTower.cpp +++ b/src/slic3r/GUI/BeltPurgeTower.cpp @@ -142,7 +142,14 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object } // --- Sizing ----------------------------------------------------------- - const double width = print_config.has("belt_purge_tower_width") ? std::max(1., print_config.opt_float("belt_purge_tower_width")) : 35.; + const int n_islands = std::max(1, (int) filaments.size() - 1); + const double gap = 1.0; + // Every disconnected island needs at least 1 mm of printable width. Honor + // the configured total width whenever possible, but never let the island + // layout silently grow past the footprint used for placement. + const double min_width = n_islands + (n_islands - 1) * gap; + const double width = std::max(min_width, + print_config.has("belt_purge_tower_width") ? print_config.opt_float("belt_purge_tower_width") : 35.); const double layer_h = print_config.has("layer_height") ? print_config.opt_float("layer_height") : 0.2; // Belt geometry. The rotation axis is the gantry tilt axis; the belt @@ -219,6 +226,9 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object new_sig.key[4] = q(belt_max); new_sig.key[5] = q(lat_min); new_sig.key[6] = q(z_max); + new_sig.key[7] = static_cast(rot); + new_sig.key[8] = std::lround(theta * 10000.0); + new_sig.key[9] = q(lat_max); if (prism_idxs.size() == 1 && new_sig == sig) return false; // already up to date — do not touch the model @@ -279,15 +289,13 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object // lets each swap claim its own island. Total lateral footprint stays `width` // (each island width/N wide, separated by a small gap), so per-layer capacity // per island ~= max_flush, matching the height sizing. - const int n_islands = std::max(1, (int) filaments.size() - 1); // Minimal gap between sub-bars: they must stay just-separated so the slicer // keeps them as distinct islands (hence distinct infill collections, one per // simultaneous swap). Zero gap would union them into one collection and // reintroduce the multi-swap-per-layer absorption bug; a hair over ~2 line // widths also keeps gap-fill from bridging them. 1 mm is about as close as // they can butt up while staying individually purgeable. - const double gap = 1.0; - const double w_sub = std::max(1.0, (width - (n_islands - 1) * gap) / n_islands); + const double w_sub = (width - (n_islands - 1) * gap) / n_islands; // --- (Re)create --------------------------------------------------------- if (!prism_idxs.empty()) diff --git a/src/slic3r/GUI/BeltPurgeTower.hpp b/src/slic3r/GUI/BeltPurgeTower.hpp index b1735303c5..5e5b614834 100644 --- a/src/slic3r/GUI/BeltPurgeTower.hpp +++ b/src/slic3r/GUI/BeltPurgeTower.hpp @@ -1,5 +1,4 @@ -#ifndef slic3r_GUI_BeltPurgeTower_hpp_ -#define slic3r_GUI_BeltPurgeTower_hpp_ +#pragma once // ORCA-Belt: auto-managed purge prism for belt printers. // @@ -23,12 +22,12 @@ struct BeltPurgeSignature { bool valid = false; int filament_count = 0; - long key[7] = {0}; // rounded geometry inputs (0.1 mm units) + long key[10] = {0}; // rounded geometry inputs (0.1 mm units) bool operator==(const BeltPurgeSignature &o) const { if (valid != o.valid || filament_count != o.filament_count) return false; - for (int i = 0; i < 7; ++i) + for (int i = 0; i < 10; ++i) if (key[i] != o.key[i]) return false; return true; @@ -44,5 +43,3 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object } // namespace GUI } // namespace Slic3r - -#endif // slic3r_GUI_BeltPurgeTower_hpp_ diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 9cb085f78b..ba3432b4ad 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -300,6 +300,41 @@ TEST_CASE("Print::validate tolerates a null warnings pointer", "[Print][validate CHECK(err.string.empty()); } +TEST_CASE("Purge tower selection keeps ordinary printers on the classic path", "[Print][PurgeTower][Regression]") +{ + DynamicPrintConfig config = multifilament_config(2, { + { "belt_printer", 0 }, + { "enable_prime_tower", 1 }, + { "enable_belt_purge_tower", 1 } + }); + config.set_key_value("timelapse_type", new ConfigOptionEnum(TimelapseType::tlSmooth)); + + Model model; + Print print; + build_cubes(model, print, config, /*n=*/1, /*overlap=*/false); + + CHECK(print.has_wipe_tower()); + CHECK_FALSE(print.has_belt_purge_tower()); +} + +TEST_CASE("Belt purge planning requires its managed purge object", "[Print][PurgeTower][Regression]") +{ + DynamicPrintConfig config = multifilament_config(2, { + { "belt_printer", 1 }, + { "enable_belt_purge_tower", 1 } + }); + + Model model; + Print print; + build_cubes(model, print, config, /*n=*/1, /*overlap=*/false); + CHECK_FALSE(print.has_belt_purge_tower()); + + model.objects.front()->config.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true)); + print.apply(model, config); + CHECK(print.has_belt_purge_tower()); + CHECK_FALSE(print.has_wipe_tower()); +} + TEST_CASE("A default slice emits perimeter, infill, and skirt", "[Print]") { const std::string gcode = slice({ cube(20) }, {