pull purge tower into its own files, make purge tower semi-transparent like other purge towers

This commit is contained in:
harrierpigeon
2026-08-08 16:35:55 -05:00
parent c80f1ab312
commit bcfb09481c
11 changed files with 443 additions and 327 deletions
+1
View File
@@ -1412,6 +1412,7 @@ static std::vector<std::string> s_Preset_printer_options {
"belt_preslice_global", "belt_preslice_global",
"first_layer_plane", "first_layer_plane_offset", "first_layer_plane_thickness", "first_layer_plane", "first_layer_plane_offset", "first_layer_plane_thickness",
"belt_support_floor_offset", "belt_support_floor_mode", "belt_support_z_offset_mode", "belt_support_floor_offset", "belt_support_floor_mode", "belt_support_z_offset_mode",
"enable_belt_purge_tower",
"gcode_flavor", "gcode_flavor",
"fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs", "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", "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",
+9 -4
View File
@@ -359,6 +359,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|| opt_key == "hot_plate_temp" || opt_key == "hot_plate_temp"
|| opt_key == "textured_plate_temp" || opt_key == "textured_plate_temp"
|| opt_key == "enable_prime_tower" || opt_key == "enable_prime_tower"
|| opt_key == "enable_belt_purge_tower"
|| opt_key == "enable_wrapping_detection" || opt_key == "enable_wrapping_detection"
|| opt_key == "prime_tower_enable_framework" || opt_key == "prime_tower_enable_framework"
|| opt_key == "prime_tower_width" || opt_key == "prime_tower_width"
@@ -1474,16 +1475,18 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
add_warning(layer_warning); add_warning(layer_warning);
} }
if (m_config.enable_prime_tower) { if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value
if (m_config.belt_printer.value && m_config.print_sequence == PrintSequence::ByObject && m_config.print_sequence == PrintSequence::ByObject
&& extruders.size() > 1 && warning != nullptr) { && extruders.size() > 1 && warning != nullptr) {
StringObjectException warningtemp; StringObjectException warningtemp;
warningtemp.string = L("The belt purge tower is not generated in \"By object\" print sequence; " warningtemp.string = L("The belt purge tower is not generated in \"By object\" print sequence; "
"filament changes will not be purged."); "filament changes will not be purged.");
warningtemp.opt_key = "enable_prime_tower"; warningtemp.opt_key = "enable_belt_purge_tower";
warningtemp.is_warning = true; warningtemp.is_warning = true;
*warning = warningtemp; *warning = warningtemp;
} }
if (m_config.enable_prime_tower) {
for (const PrintObject* object : m_objects) { for (const PrintObject* object : m_objects) {
if (object->config().precise_z_height.value) { 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"); 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. // prism object via the flush-into-objects machinery instead of a wipe tower.
bool Print::has_belt_purge_tower() const 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 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.spiral_mode.value
&& m_config.filament_diameter.values.size() > 1; && m_config.filament_diameter.values.size() > 1;
} }
+11
View File
@@ -7321,6 +7321,17 @@ void PrintConfigDef::init_fff_params()
def->set_default_value(new ConfigOptionEnum<BeltSupportZOffsetMode>(BeltSupportZOffsetMode::Unconditional)); def->set_default_value(new ConfigOptionEnum<BeltSupportZOffsetMode>(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 = this->add("belt_purge_tower_width", coFloat);
def->label = L("Belt purge tower width"); def->label = L("Belt purge tower width");
def->category = L("Printable space"); def->category = L("Printable space");
+3
View File
@@ -1807,6 +1807,9 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionEnum<BeltSupportZOffsetMode>, belt_support_z_offset_mode)) ((ConfigOptionEnum<BeltSupportZOffsetMode>, belt_support_z_offset_mode))
// Width (machine X, across the belt) of the auto-generated belt purge prism. // Width (machine X, across the belt) of the auto-generated belt purge prism.
((ConfigOptionFloat, belt_purge_tower_width)) ((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 //BBS
((ConfigOptionInts, additional_cooling_fan_speed)) ((ConfigOptionInts, additional_cooling_fan_speed))
((ConfigOptionInts, close_additional_fan_first_x_layers)) ((ConfigOptionInts, close_additional_fan_first_x_layers))
+2
View File
@@ -397,6 +397,8 @@ set(SLIC3R_GUI_SOURCES
GUI/ParamsPanel.hpp GUI/ParamsPanel.hpp
GUI/PartPlate.cpp GUI/PartPlate.cpp
GUI/PartPlate.hpp GUI/PartPlate.hpp
GUI/BeltPurgeTower.cpp
GUI/BeltPurgeTower.hpp
GUI/FilamentGroupPopup.hpp GUI/FilamentGroupPopup.hpp
GUI/FilamentGroupPopup.cpp GUI/FilamentGroupPopup.cpp
GUI/PhysicalPrinterDialog.cpp GUI/PhysicalPrinterDialog.cpp
+311
View File
@@ -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 <algorithm>
#include <cmath>
#include <limits>
#include <set>
#include <vector>
#include <boost/log/trivial.hpp>
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<int> 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<int> &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<ConfigOptionBool>("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<ConfigOptionEnum<PrintSequence>>("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<int> filaments;
double x_min = std::numeric_limits<double>::max();
double x_max = -std::numeric_limits<double>::max();
double y_min = std::numeric_limits<double>::max();
double y_max = -std::numeric_limits<double>::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<ConfigOptionEnum<BeltRotationAxis>>("belt_slice_rotation");
const auto *angle_opt = printer_config.option<ConfigOptionFloat>("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<double> matrix = get_flush_volumes_matrix(
project_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values, 0, extruder_nums);
const auto * multi_opt = project_config.option<ConfigOptionFloats>("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<ConfigOptionPoints>("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<InfillPattern>(ipRectilinear));
cfg.set_key_value("enable_support", new ConfigOptionBool(false));
cfg.set_key_value("brim_type", new ConfigOptionEnum<BrimType>(btNoBrim));
cfg.set_key_value("seam_slope_type", new ConfigOptionEnum<SeamScarfType>(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
+48
View File
@@ -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_
+7 -4
View File
@@ -972,10 +972,13 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
bool have_prime_tower = config->opt_bool("enable_prime_tower"); bool have_prime_tower = config->opt_bool("enable_prime_tower");
// ORCA-Belt: belt printers replace the classic wipe tower with the // ORCA-Belt: belt printers replace the classic wipe tower with the
// auto-generated belt purge prism — only its width is tunable; the // auto-generated belt purge prism, enabled by the belt-only printer option
// classic tower geometry options do not apply. (is_belt_printer is // enable_belt_purge_tower. Its width (a process option) is only relevant
// computed at the top of this function.) // then. (is_belt_printer is computed at the top of this function.)
toggle_line("belt_purge_tower_width", have_prime_tower && is_belt_printer); 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"}) 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); toggle_line(el, have_prime_tower && !is_belt_printer);
+22
View File
@@ -2948,6 +2948,28 @@ void GLCanvas3D::reload_scene(bool refresh_immediately, bool force_full_scene_re
} }
update_volumes_colors_by_extruder(); 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. // Update selection indices based on the old/new GLVolumeCollection.
if (m_selection.get_mode() == Selection::Instance) if (m_selection.get_mode() == Selection::Instance)
m_selection.instances_changed(instance_ids_selected); m_selection.instances_changed(instance_ids_selected);
+9 -311
View File
@@ -105,6 +105,7 @@
#include "GUI_Preview.hpp" #include "GUI_Preview.hpp"
#include "3DBed.hpp" #include "3DBed.hpp"
#include "PartPlate.hpp" #include "PartPlate.hpp"
#include "BeltPurgeTower.hpp"
#include "Camera.hpp" #include "Camera.hpp"
#include "Mouse3DController.hpp" #include "Mouse3DController.hpp"
#include "Tab.hpp" #include "Tab.hpp"
@@ -5476,24 +5477,10 @@ struct Plater::priv
void remove(size_t obj_idx); void remove(size_t obj_idx);
bool delete_object_from_model(size_t obj_idx, bool refresh_immediately = true); //BBS 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 // ORCA-Belt: keep the auto-generated belt purge prism in sync with the
// config and plate contents. Returns true when the model was mutated // config and plate contents (thin wrapper over GUI::ensure_belt_purge_tower
// (caller should refresh the scene). // in BeltPurgeTower.cpp). Returns true when the model was mutated.
bool ensure_belt_purge_tower(); bool ensure_belt_purge_tower();
// Inputs of the last belt purge prism generation. Idempotence is keyed on BeltPurgeSignature m_belt_purge_sig;
// these (not the prism's resulting bbox): the prism gets nudged by plate
// assignment after creation, so comparing its bbox would falsely detect a
// change and regenerate it on every background-process tick — which would
// re-invalidate a freshly sliced result and make the preview unreachable.
struct BeltPurgeSignature {
bool valid = false;
int filament_count = 0;
long key[7] = {0}; // rounded geometry inputs (0.1 mm units)
bool operator==(const BeltPurgeSignature &o) const {
if (valid != o.valid || filament_count != o.filament_count) return false;
for (int i = 0; i < 7; ++i) if (key[i] != o.key[i]) return false;
return true;
}
} m_belt_purge_sig;
void delete_all_objects_from_model(); void delete_all_objects_from_model();
void reset(bool apply_presets_change = false); void reset(bool apply_presets_change = false);
void center_selection(); void center_selection();
@@ -8898,304 +8885,15 @@ void Plater::priv::process_validation_warnings(const std::vector<StringObjectExc
} }
// ORCA-Belt: auto-managed purge prism for belt printers. The classic wipe // ORCA-Belt: the actual implementation lives in BeltPurgeTower.cpp (kept out of
// tower is disabled in belt mode (its G-code bypasses the belt transform), so // this large, frequently-touched file so it stays clear of unrelated upstream
// filament-change purging is routed into this prism via flush_into_objects // changes and carries no regression risk for normal printers). This is a thin
// (see Print::_plan_belt_purge()). The prism is a real model object so it is // wrapper that hands it the model, plates, object list, and cached signature.
// 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 tilt lead-in/lead-out). Runs on every
// background-process update, so it must be idempotent: it only mutates the
// model when the desired prism differs from the existing one beyond coarse
// tolerances.
bool Plater::priv::ensure_belt_purge_tower() bool Plater::priv::ensure_belt_purge_tower()
{ {
auto is_prism = [](const ModelObject *mo) { return GUI::ensure_belt_purge_tower(model, partplate_list, sidebar->obj_list(), m_belt_purge_sig);
const ConfigOption *opt = mo->config.option("belt_purge_tower_object");
return opt != nullptr && opt->getBool();
};
std::vector<int> 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<int> &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<ConfigOptionBool>("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<ConfigOptionEnum<PrintSequence>>("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<int> filaments;
double x_min = std::numeric_limits<double>::max();
double x_max = -std::numeric_limits<double>::max();
double y_min = std::numeric_limits<double>::max();
double y_max = -std::numeric_limits<double>::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<ConfigOptionEnum<BeltRotationAxis>>("belt_slice_rotation");
const auto *angle_opt = printer_config.option<ConfigOptionFloat>("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<double> matrix = get_flush_volumes_matrix(
project_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values, 0, extruder_nums);
const auto * multi_opt = project_config.option<ConfigOptionFloats>("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<ConfigOptionPoints>("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<InfillPattern>(ipRectilinear));
cfg.set_key_value("enable_support", new ConfigOptionBool(false));
cfg.set_key_value("brim_type", new ConfigOptionEnum<BrimType>(btNoBrim));
cfg.set_key_value("seam_slope_type", new ConfigOptionEnum<SeamScarfType>(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;
}
// Update background processing thread from the current config and Model. // Update background processing thread from the current config and Model.
// Returns a bitmask of UpdateBackgroundProcessReturnState. // Returns a bitmask of UpdateBackgroundProcessReturnState.
+13 -1
View File
@@ -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("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"); 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 = 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"); 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")) { 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<WipeTowerType>("wipe_tower_type") == WipeTowerType::Type2; const bool supports_wipe_tower_2 = !is_BBL_printer && m_config->opt_enum<WipeTowerType>("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 // SoftFever: hide specific settings for BBL printer
for (auto el : { for (auto el : {
"enable_filament_ramming", "enable_filament_ramming",