From 88726d76e84547ff9613b847323ffd53836399e3 Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Sat, 13 Jun 2026 04:24:41 -0500 Subject: [PATCH] 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");