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;