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",