mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-13 02:41:16 +00:00
Merge upstream/hanif/belt-printer-fixes into belt/final-round
Brings in upstream/belt-printer (the Sept 14 main merge) plus Hanif Koh's 21 review-fix commits from PR #15685, on top of the MachineKinematics refactor and the purge-prism / tree-support / first-layer-speed fixes. Conflict resolution: - BeltGCodeWriter is gone (kinematics refactor), so Hanif's plate-offset fix for it is ported into GCodeWriter: the first-layer-plane checks in travel_to_xy / travel_to_xyz / _travel_to_z now evaluate the plate-local point, and BeltGCode::init_belt_writer hands the stored plate origin to the writer it installs. - init_belt_writer(Print&) takes Hanif's signature; the BBL flag is set on the surviving writer by GCode::_do_export. - The shared emit_belt_brim_bands() loop keeps the BeltFloorObjectGuard the local branch added, so apron bands classify first-layer height against their own object. - eager_lift keeps effective_type: it now carries set_force_normal_lift(). - GCodeWriter's initializer list follows Hanif's member order with m_kinematics in its declared position. - TreeSupport::detect_overhangs uses Hanif's clamped build_plate_tilt_slope() for the non-belt path and the belt shear for the belt path.
This commit is contained in:
@@ -1041,28 +1041,29 @@ std::string CoolingBuffer::apply_layer_cooldown(
|
||||
}
|
||||
|
||||
if (need_set_fan) {
|
||||
const auto set_fan = [&](int speed) {
|
||||
if (m_current_fan_speed != speed) {
|
||||
new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, speed, part_cooling_fan_min_pwm);
|
||||
m_current_fan_speed = speed;
|
||||
}
|
||||
};
|
||||
if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){
|
||||
new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, overhang_fan_speed, part_cooling_fan_min_pwm);
|
||||
m_current_fan_speed = overhang_fan_speed;
|
||||
set_fan(overhang_fan_speed);
|
||||
} else if (fan_speed_change_requests[CoolingLine::TYPE_INTERNAL_BRIDGE_FAN_START]){ // ORCA: Add support for separate internal bridge fan speed control
|
||||
new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, internal_bridge_fan_speed, part_cooling_fan_min_pwm);
|
||||
m_current_fan_speed = internal_bridge_fan_speed;
|
||||
set_fan(internal_bridge_fan_speed);
|
||||
}
|
||||
else if (fan_speed_change_requests[CoolingLine::TYPE_SUPPORT_INTERFACE_FAN_START]){
|
||||
new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, supp_interface_fan_speed, part_cooling_fan_min_pwm);
|
||||
m_current_fan_speed = supp_interface_fan_speed;
|
||||
set_fan(supp_interface_fan_speed);
|
||||
}
|
||||
else if (fan_speed_change_requests[CoolingLine::TYPE_IRONING_FAN_START]){
|
||||
new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, ironing_fan_speed, part_cooling_fan_min_pwm);
|
||||
m_current_fan_speed = ironing_fan_speed;
|
||||
set_fan(ironing_fan_speed);
|
||||
}
|
||||
else if(fan_speed_change_requests[CoolingLine::TYPE_FORCE_RESUME_FAN] && m_current_fan_speed != -1){
|
||||
new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_current_fan_speed, part_cooling_fan_min_pwm);
|
||||
fan_speed_change_requests[CoolingLine::TYPE_FORCE_RESUME_FAN] = false;
|
||||
}
|
||||
else {
|
||||
new_gcode += GCodeWriter::set_fan(m_config.gcode_flavor, m_fan_speed, part_cooling_fan_min_pwm);
|
||||
m_current_fan_speed = m_fan_speed;
|
||||
set_fan(m_fan_speed);
|
||||
}
|
||||
need_set_fan = false;
|
||||
}
|
||||
|
||||
@@ -170,14 +170,13 @@ void FanMover::_put_in_middle_G1(std::list<BufferData>::iterator item_to_split,
|
||||
|
||||
void FanMover::_print_in_middle_G1(BufferData& line_to_split, float nb_sec, const std::string &line_to_write) {
|
||||
if (nb_sec < line_to_split.time * 0.1) {
|
||||
// doesn't really need to be split, print it after
|
||||
m_process_output += line_to_split.raw + "\n";
|
||||
// Doesn't need to be split: the insertion point is at the start.
|
||||
m_process_output += line_to_write + (line_to_write.back() == '\n'?"":"\n");
|
||||
} else if (nb_sec > line_to_split.time * 0.9) {
|
||||
// doesn't really need to be split, print it before
|
||||
//will also print before if line_to_split.time == 0
|
||||
m_process_output += line_to_write + (line_to_write.back() == '\n' ? "" : "\n");
|
||||
m_process_output += line_to_split.raw + "\n";
|
||||
} else if (nb_sec > line_to_split.time * 0.9) {
|
||||
// Doesn't need to be split: the insertion point is at the end.
|
||||
m_process_output += line_to_split.raw + "\n";
|
||||
m_process_output += line_to_write + (line_to_write.back() == '\n' ? "" : "\n");
|
||||
}else if(line_to_split.raw.size() > 2
|
||||
&& line_to_split.raw[0] == 'G' && line_to_split.raw[1] == '1' && line_to_split.raw[2] == ' ') {
|
||||
float percent = nb_sec / line_to_split.time;
|
||||
|
||||
@@ -32,7 +32,8 @@ class FanMover
|
||||
private:
|
||||
const std::regex regex_fan_speed;
|
||||
const float nb_seconds_delay;
|
||||
const bool with_D_option;
|
||||
// Set from fan_speedup_time at the call site, but nothing here reads it.
|
||||
[[maybe_unused]] const bool with_D_option;
|
||||
const bool relative_e;
|
||||
const bool only_overhangs;
|
||||
const float kickstart;
|
||||
|
||||
@@ -532,7 +532,7 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
|
||||
const float height = interpolate ? lerp(prev_move.height, curr_move.height, t) : curr_move.height;
|
||||
// ORCA: Fix issue with flow rate changes being visualized incorrectly
|
||||
const float mm3_per_mm = curr_move.mm3_per_mm;
|
||||
const float fan_speed = interpolate ? lerp(prev_move.fan_speed, curr_move.fan_speed, t) : curr_move.fan_speed;
|
||||
const float fan_speed = curr_move.fan_speed;
|
||||
const float temperature = interpolate ? lerp(prev_move.temperature, curr_move.temperature, t) : curr_move.temperature;
|
||||
actual_speed_moves.push_back({
|
||||
block.move_id,
|
||||
@@ -563,7 +563,7 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
|
||||
const float height = interpolate ? lerp(prev_move.height, curr_move.height, t) : curr_move.height;
|
||||
// ORCA: Fix issue with flow rate changes being visualized incorrectly
|
||||
const float mm3_per_mm = curr_move.mm3_per_mm;
|
||||
const float fan_speed = interpolate ? lerp(prev_move.fan_speed, curr_move.fan_speed, t) : curr_move.fan_speed;
|
||||
const float fan_speed = curr_move.fan_speed;
|
||||
const float temperature = interpolate ? lerp(prev_move.temperature, curr_move.temperature, t) : curr_move.temperature;
|
||||
actual_speed_moves.push_back({
|
||||
block.move_id,
|
||||
@@ -1273,7 +1273,7 @@ void GCodeProcessor::run_post_process()
|
||||
// add lines M73 to exported gcode
|
||||
auto process_line_move = [
|
||||
// Lambdas, mostly for string formatting, all with an empty capture block.
|
||||
time_in_minutes, format_time_float, format_line_M73_main, format_line_M73_stop_int, format_line_M73_stop_float, time_in_last_minute,format_line_exhaust_fan_control,
|
||||
time_in_minutes, format_time_float, format_line_M73_main, format_line_M73_stop_int, format_line_M73_stop_float, time_in_last_minute,
|
||||
&self = std::as_const(m_time_processor),
|
||||
// Caches, to be modified
|
||||
&g1_times_cache_it, &last_exported_main, &last_exported_stop,
|
||||
@@ -1468,9 +1468,11 @@ void GCodeProcessor::run_post_process()
|
||||
|
||||
// Append a per-filament usage block at a filament change.
|
||||
auto handle_filament_change = [&](int filament_id, int cur_line_id, int nozzle_id) {
|
||||
// skip filament changes emitted inside the machine start / end gcode
|
||||
if (m_machine_start_gcode_end_line_id == (unsigned int) (-1) && (unsigned int) (cur_line_id) < m_machine_start_gcode_end_line_id ||
|
||||
m_machine_end_gcode_start_line_id != (unsigned int) (-1) && (unsigned int) (cur_line_id) > m_machine_end_gcode_start_line_id)
|
||||
// Skip filament changes emitted inside the machine start / end gcode. One forward pass assigns
|
||||
// the tag ids and tests them in the same loop, so inside the start gcode the end tag is unseen
|
||||
// and the id still holds the sentinel. That is why the first clause tests == and the second !=.
|
||||
if ((m_machine_start_gcode_end_line_id == (unsigned int) (-1) && (unsigned int) (cur_line_id) < m_machine_start_gcode_end_line_id) ||
|
||||
(m_machine_end_gcode_start_line_id != (unsigned int) (-1) && (unsigned int) (cur_line_id) > m_machine_end_gcode_start_line_id))
|
||||
return;
|
||||
if (!m_filament_blocks.empty())
|
||||
m_filament_blocks.back().upper_gcode_id = cur_line_id;
|
||||
@@ -3584,6 +3586,7 @@ void GCodeProcessor::reset()
|
||||
m_zero_layer_height = 0.0f;
|
||||
m_first_layer_height = 0.0f;
|
||||
m_processing_start_custom_gcode = false;
|
||||
m_in_config_block = false;
|
||||
m_g1_line_id = 0;
|
||||
m_layer_id = 0;
|
||||
m_cp_color.reset();
|
||||
@@ -4189,9 +4192,20 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers
|
||||
return;
|
||||
}
|
||||
|
||||
if (boost::starts_with(comment, " CONFIG_BLOCK_START")) {
|
||||
m_in_config_block = true;
|
||||
return;
|
||||
}
|
||||
if (boost::starts_with(comment, " CONFIG_BLOCK_END")) {
|
||||
m_in_config_block = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Belt printer: derive the physical tilt magnitude from the slicing-rotation
|
||||
// angle header comment (used to enable the preview's belt view).
|
||||
if (boost::starts_with(comment, " belt_slice_rotation_angle = ")) {
|
||||
// angle header comment (used to enable the preview's belt view). Only the belt
|
||||
// header carries it outside the config block; the config block lists the key
|
||||
// for every printer, belt or not.
|
||||
if (!m_in_config_block && boost::starts_with(comment, " belt_slice_rotation_angle = ")) {
|
||||
try {
|
||||
m_result.belt_tilt_angle = std::abs(std::stof(std::string(comment.substr(29))));
|
||||
} catch (...) {}
|
||||
@@ -5375,7 +5389,7 @@ void GCodeProcessor::process_VG1(const GCodeReader::GCodeLine& line)
|
||||
float filament_radius = 0.5f * filament_diameter;
|
||||
float area_filament_cross_section = static_cast<float>(M_PI) * sqr(filament_radius);
|
||||
|
||||
auto absolute_position = [this, area_filament_cross_section](Axis axis, const GCodeReader::GCodeLine& lineG1) {
|
||||
auto absolute_position = [this](Axis axis, const GCodeReader::GCodeLine& lineG1) {
|
||||
bool is_relative = (m_global_positioning_type == EPositioningType::Relative);
|
||||
if (axis == E)
|
||||
is_relative |= (m_e_local_positioning_type == EPositioningType::Relative);
|
||||
@@ -5826,7 +5840,7 @@ void GCodeProcessor::process_G2_G3(const GCodeReader::GCodeLine& line, bool cloc
|
||||
if (travel_length < 0.001)
|
||||
return;
|
||||
|
||||
auto adjust_target = [this, area_filament_cross_section](const AxisCoords& target, const AxisCoords& prev_position) {
|
||||
auto adjust_target = [this](const AxisCoords& target, const AxisCoords& prev_position) {
|
||||
AxisCoords ret = target;
|
||||
if (m_global_positioning_type == EPositioningType::Relative) {
|
||||
for (unsigned char a = X; a <= E; ++a) {
|
||||
@@ -7084,7 +7098,7 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type,
|
||||
get_acceleration(normal_mode));
|
||||
const float junction_deviation = get_option_value(m_time_processor.machine_limits.machine_max_junction_deviation, normal_mode_id);
|
||||
const bool use_jd_jerk = (m_flavor == gcfMarlinFirmware && junction_deviation > 0.0f);
|
||||
const auto axis_jerk_for_preview = [this, normal_mode, use_jd_jerk, move_acceleration](Axis axis) {
|
||||
const auto axis_jerk_for_preview = [this, use_jd_jerk, move_acceleration](Axis axis) {
|
||||
return use_jd_jerk ? get_axis_max_jerk_with_jd(normal_mode, axis, move_acceleration) : get_axis_max_jerk(normal_mode, axis);
|
||||
};
|
||||
const float jerk_x = axis_jerk_for_preview(X);
|
||||
|
||||
@@ -1194,6 +1194,7 @@ class Print;
|
||||
float m_first_layer_height; // mm
|
||||
float m_zero_layer_height; // mm
|
||||
bool m_processing_start_custom_gcode;
|
||||
bool m_in_config_block;
|
||||
unsigned int m_g1_line_id;
|
||||
unsigned int m_layer_id;
|
||||
CpColor m_cp_color;
|
||||
|
||||
@@ -627,7 +627,7 @@ void compute_global_occlusion(GlobalModelInfo &result, const PrintObject *po,
|
||||
SeamPosition seam_position = spAligned) {
|
||||
BOOST_LOG_TRIVIAL(debug)
|
||||
<< "SeamPlacer: gather occlusion meshes: start";
|
||||
auto obj_transform = po->trafo_centered();
|
||||
auto obj_transform = po->trafo_sliced();
|
||||
indexed_triangle_set triangle_set;
|
||||
indexed_triangle_set negative_volumes_set;
|
||||
//add all parts
|
||||
@@ -712,7 +712,7 @@ void gather_enforcers_blockers(GlobalModelInfo &result, const PrintObject *po) {
|
||||
BOOST_LOG_TRIVIAL(debug)
|
||||
<< "SeamPlacer: build AABB trees for raycasting enforcers/blockers: start";
|
||||
|
||||
auto obj_transform = po->trafo_centered();
|
||||
auto obj_transform = po->trafo_sliced();
|
||||
|
||||
for (const ModelVolume *mv : po->model_object()->volumes) {
|
||||
if (mv->is_seam_painted()) {
|
||||
|
||||
@@ -78,7 +78,7 @@ struct PlateBBoxData
|
||||
int first_extruder = 0;
|
||||
float nozzle_diameter = 0.4;
|
||||
std::string bed_type;
|
||||
float first_layer_time;
|
||||
float first_layer_time = 0.0f;
|
||||
// version 1: use view type ColorPrint (filament color)
|
||||
// version 2: use view type FilamentId (filament id)
|
||||
int version = 2;
|
||||
|
||||
@@ -910,7 +910,7 @@ namespace Slic3r
|
||||
|
||||
unsigned int iterations = (1 << all_extruders.size());
|
||||
unsigned int final_state = iterations - 1;
|
||||
std::vector<std::vector<float>>cache(iterations, std::vector<float>(all_extruders.size(), 0x7fffffff));
|
||||
std::vector<std::vector<float>>cache(iterations, std::vector<float>(all_extruders.size(), std::numeric_limits<float>::max()));
|
||||
std::vector<std::vector<int>>prev(iterations, std::vector<int>(all_extruders.size(), -1));
|
||||
cache[1][0] = 0.;
|
||||
for (unsigned int state = 0; state < iterations; ++state) {
|
||||
|
||||
@@ -3204,7 +3204,7 @@ void ToolOrdering::assign_custom_gcodes(const Print &print)
|
||||
// Skip all custom G-codes above this layer and skip all extruder switches.
|
||||
for (; custom_gcode_it != custom_gcode_per_print_z.gcodes.rend() && (
|
||||
(print_z_above > lt.print_z && custom_gcode_it->print_z > 0.5 * (lt.print_z + print_z_above))
|
||||
|| custom_gcode_it->type == CustomGCode::ToolChange); ++ custom_gcode_it);
|
||||
|| custom_gcode_it->type == CustomGCode::ToolChange); ++ custom_gcode_it) {}
|
||||
print_z_above = lt.print_z;
|
||||
if (custom_gcode_it == custom_gcode_per_print_z.gcodes.rend())
|
||||
// Custom G-codes were processed.
|
||||
|
||||
@@ -1630,6 +1630,94 @@ float WipeTower::get_auto_brim_by_height(float max_height) {
|
||||
return 8.f;
|
||||
}
|
||||
|
||||
float WipeTower::estimate_brim_real_width(float brim_width, float nozzle_diameter, float first_layer_height, bool type2)
|
||||
{
|
||||
if (brim_width <= 0.f)
|
||||
return brim_width;
|
||||
const float spacing = nozzle_diameter * 1.25f - first_layer_height * float(1. - M_PI_4); // Width_To_Nozzle_Ratio
|
||||
if (spacing <= EPSILON)
|
||||
return brim_width;
|
||||
const int loops_num = int((brim_width + spacing / 2.f) / spacing);
|
||||
return loops_num * spacing + (type2 ? 0.f : spacing / 2.f);
|
||||
}
|
||||
|
||||
float WipeTower::get_wrapping_detection_depth()
|
||||
{
|
||||
return float(wrapping_wipe_tower_depth);
|
||||
}
|
||||
|
||||
float WipeTower::nozzle_change_perimeter_width(float nozzle_diameter)
|
||||
{
|
||||
auto it = nozzle_diameter_to_nozzle_change_width.find(nozzle_diameter);
|
||||
return it != nozzle_diameter_to_nozzle_change_width.end() ? it->second : 2.f * nozzle_diameter * 1.25f;
|
||||
}
|
||||
|
||||
float WipeTower::estimate_tower_blocks_depth(const std::vector<PurgeEstimate> &purges, float width, float layer_height, float nozzle_diameter, float extra_spacing)
|
||||
{
|
||||
if (purges.empty() || layer_height < EPSILON || nozzle_diameter < EPSILON)
|
||||
return 0.f;
|
||||
const float pw = nozzle_diameter * 1.25f; // Width_To_Nozzle_Ratio
|
||||
const float ncpw = nozzle_change_perimeter_width(nozzle_diameter);
|
||||
const float line_width = width - 2.f * pw;
|
||||
if (line_width <= EPSILON)
|
||||
return 0.f;
|
||||
// Line cross-section as volume_to_length() sees it; the infill gap stretches the perimeter
|
||||
// width by the configured ratio and nozzle-change lines keep their own width
|
||||
// (calc_block_infill_gap).
|
||||
auto line_area = [layer_height](float w) { return layer_height * (w - layer_height * float(1. - M_PI_4)); };
|
||||
const float extra_width = (extra_spacing - 1.f) * pw;
|
||||
const float gap = pw + extra_width;
|
||||
const float nc_gap = ncpw + extra_width;
|
||||
// A layer purges into at most (filaments - 1) targets, so a category holding every filament
|
||||
// never sees its smallest purge (the layer's first filament) in its worst layer.
|
||||
struct Block { float depth = 0.f; float min_purge = 0.f; size_t filaments = 0; };
|
||||
std::map<int, Block> blocks;
|
||||
for (const PurgeEstimate &purge : purges) {
|
||||
Block &block = blocks[purge.category];
|
||||
const float purge_depth = std::ceil(purge.prime_volume / line_area(pw) / line_width) * gap;
|
||||
block.min_purge = block.filaments == 0 ? purge_depth : std::min(block.min_purge, purge_depth);
|
||||
block.depth += purge_depth;
|
||||
++block.filaments;
|
||||
if (purge.filament_change_length > EPSILON) {
|
||||
// The leaving filament is rammed over the nozzle-change flow, again in whole lines.
|
||||
const float filament_area = float(M_PI) * purge.filament_diameter * purge.filament_diameter / 4.f;
|
||||
const float nc_length = purge.filament_change_length * filament_area / line_area(ncpw);
|
||||
block.depth += std::ceil(nc_length / (width - ncpw - pw)) * nc_gap;
|
||||
}
|
||||
}
|
||||
float depth = pw; // plan_tower_new starts the first block one perimeter width in
|
||||
for (const auto &[category, block] : blocks)
|
||||
depth += block.filaments == purges.size() ? block.depth - block.min_purge : block.depth;
|
||||
return depth;
|
||||
}
|
||||
|
||||
float WipeTower::rib_footprint_side(float width, float depth, float rib_width, float extra_rib_length, float max_height)
|
||||
{
|
||||
if (width < EPSILON || depth < EPSILON)
|
||||
return 0.f;
|
||||
// Ribs run the diagonal; below the height-based minimum they are extended rather than the
|
||||
// body, then by the extra length, never ending up shorter than the diagonal.
|
||||
const float diagonal = std::sqrt(width * width + depth * depth);
|
||||
float rib_length = diagonal;
|
||||
if (depth + EPSILON < get_limit_depth_by_height(max_height))
|
||||
rib_length = std::max(rib_length, get_limit_depth_by_height(max_height) * float(std::sqrt(2.)));
|
||||
rib_length = std::max(diagonal, rib_length + extra_rib_length);
|
||||
// Half the extension at each end of the diagonal plus half the rib width, projected onto the axes.
|
||||
const float rib_w = std::min(rib_width, std::min(width, depth) / 2.f);
|
||||
const float per_side = ((rib_length - diagonal) / 2.f + rib_w / 2.f) / float(std::sqrt(2.));
|
||||
return std::max(width, depth) + 2.f * per_side;
|
||||
}
|
||||
|
||||
float WipeTower::estimate_rib_tower_bbox_side(const std::vector<PurgeEstimate> &purges, float width, float layer_height, float nozzle_diameter, float extra_spacing, float rib_width, float extra_rib_length, float max_height)
|
||||
{
|
||||
if (purges.empty() || width < EPSILON || layer_height < EPSILON || nozzle_diameter < EPSILON)
|
||||
return 0.f;
|
||||
const float pw = nozzle_diameter * 1.25f; // Width_To_Nozzle_Ratio
|
||||
const float square = align_ceil(std::sqrt(estimate_tower_blocks_depth(purges, width, layer_height, nozzle_diameter, extra_spacing) * width), pw);
|
||||
const float depth = estimate_tower_blocks_depth(purges, square, layer_height, nozzle_diameter, extra_spacing);
|
||||
return rib_footprint_side(square, depth, rib_width, extra_rib_length, max_height);
|
||||
}
|
||||
|
||||
Vec2f WipeTower::move_box_inside_polygon(const BoundingBox &box, const Polygons &polygons, coord_t offset)
|
||||
{
|
||||
if (polygons.empty()) return Vec2f{0.f, 0.f};
|
||||
@@ -4083,7 +4171,7 @@ void WipeTower::toolchange_wipe_new(WipeTowerWriter &writer, const box_coordinat
|
||||
}
|
||||
return time * 60.f;
|
||||
};
|
||||
auto estimate_wipe_time = [&estimate_time_kernel, & cleaning_box, &target_speed, &x_to_wipe, &xr, &xl, &dy, &WipeSpeedMap, &solid_tool_toolchange](int begin_line) -> float {
|
||||
auto estimate_wipe_time = [&estimate_time_kernel, & cleaning_box, &x_to_wipe, &xr, &xl, &dy, &solid_tool_toolchange](int begin_line) -> float {
|
||||
int n = std::ceil(x_to_wipe / (xr - xl));
|
||||
if (solid_tool_toolchange) n = (cleaning_box.lu[1] - cleaning_box.ld[1]) / dy;
|
||||
float total_time = estimate_time_kernel(n);
|
||||
@@ -4883,12 +4971,8 @@ void WipeTower::generate_new(std::vector<std::vector<WipeTower::ToolChangeResult
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_inserted) {
|
||||
if (finish_block_tcr.gcode.empty())
|
||||
finish_block_tcr = finish_block_tcr;
|
||||
else
|
||||
finish_layer_tcr = merge_tcr(finish_layer_tcr, finish_block_tcr);
|
||||
}
|
||||
if (!has_inserted && !finish_block_tcr.gcode.empty())
|
||||
finish_layer_tcr = merge_tcr(finish_layer_tcr, finish_block_tcr);
|
||||
}
|
||||
}
|
||||
// record the contact layers of different categories
|
||||
@@ -5111,7 +5195,7 @@ Polygon WipeTower::generate_rib_polygon(const box_coordinates &wt_box)
|
||||
|
||||
Polygon WipeTower::generate_support_wall_new(WipeTowerWriter &writer, const box_coordinates &wt_box, double feedrate, bool first_layer,bool rib_wall, bool extrude_perimeter, bool skip_points)
|
||||
{
|
||||
auto get_closet_idx = [this, &writer](Polylines &pls) -> std::pair<int,int> {
|
||||
auto get_closet_idx = [&writer](Polylines &pls) -> std::pair<int,int> {
|
||||
Vec2f anchor{writer.x(), writer.y()};
|
||||
int closestIndex = -1;
|
||||
int closestPl = -1;
|
||||
|
||||
@@ -42,9 +42,36 @@ public:
|
||||
static const std::map<float, float> min_depth_per_height;
|
||||
static float get_limit_depth_by_height(float max_height);
|
||||
static float get_auto_brim_by_height(float max_height);
|
||||
// Both generators lay the brim in whole loops one line spacing apart, so the printed width
|
||||
// differs from the configured one. WipeTower reports it with half a spacing of line width
|
||||
// added, WipeTower2 reports the loops alone; an estimate has to round like the generator
|
||||
// whose G-code it stands in for.
|
||||
static float estimate_brim_real_width(float brim_width, float nozzle_diameter, float first_layer_height, bool type2);
|
||||
// Depth a Type1 tower reserves once nothing but wrapping detection asks for one.
|
||||
static float get_wrapping_detection_depth();
|
||||
// Line width of the nozzle-change purge lines at this nozzle diameter.
|
||||
static float nozzle_change_perimeter_width(float nozzle_diameter);
|
||||
static TriangleMesh its_make_rib_tower(float width, float depth, float height, float rib_length, float rib_width, bool fillet_wall);
|
||||
static TriangleMesh its_make_rib_brim(const Polygon& brim, float layer_height);
|
||||
static Polygon rib_section(float width, float depth, float rib_length, float rib_width, bool fillet_wall);
|
||||
// One filament's share of a Type1 tower layer, as plan_tower_new() reserves it.
|
||||
struct PurgeEstimate
|
||||
{
|
||||
float prime_volume = 0.f; // mm3 wiped after changing to this filament
|
||||
int category = 0; // filament_adhesiveness_category; one purge block per category
|
||||
float filament_change_length = 0.f; // mm of filament rammed when it leaves its nozzle; 0 when no nozzle change is planned
|
||||
float filament_diameter = 1.75f;
|
||||
};
|
||||
// Depth of the Type1 purge stack at the given width (also the rectangle-wall depth): each
|
||||
// purge is whole lines at the block infill gap, one block per adhesiveness category sized by
|
||||
// its worst layer, stacked behind one perimeter width.
|
||||
static float estimate_tower_blocks_depth(const std::vector<PurgeEstimate> &purges, float width, float layer_height, float nozzle_diameter, float extra_spacing);
|
||||
// Side of the square bounding a rib-wall tower's first layer, brim excluded: the body plus the
|
||||
// rib bulge, with the ribs extended to the height-based minimum as both generators do.
|
||||
static float rib_footprint_side(float width, float depth, float rib_width, float extra_rib_length, float max_height);
|
||||
// Type1 rib tower: plan_tower_new() squares the tower from the depth at the configured width,
|
||||
// then re-plans the depth at the squared width.
|
||||
static float estimate_rib_tower_bbox_side(const std::vector<PurgeEstimate> &purges, float width, float layer_height, float nozzle_diameter, float extra_spacing, float rib_width, float extra_rib_length, float max_height);
|
||||
// Translation that brings a footprint inside the printable outline, padded by offset. The prime
|
||||
// tower is validated against the real outline (see layered_print_cleareance_valid), so clamping
|
||||
// against the bounding box alone would leave it off a delta or hexagonal bed. box and polygons
|
||||
|
||||
@@ -2129,6 +2129,23 @@ std::pair<double, double> WipeTower2::get_wipe_tower_cone_base(double width, dou
|
||||
return std::make_pair(R, support_scale);
|
||||
}
|
||||
|
||||
Polygon WipeTower2::cone_base_polygon(double width, double depth, double height, double angle_deg)
|
||||
{
|
||||
Polygon box({Point::new_scale(Vec2d(0., 0.)), Point::new_scale(Vec2d(width, 0.)),
|
||||
Point::new_scale(Vec2d(width, depth)), Point::new_scale(Vec2d(0., depth))});
|
||||
if (angle_deg <= EPSILON || height <= EPSILON || width <= EPSILON || depth <= EPSILON)
|
||||
return box;
|
||||
const auto [R, x_scale] = get_wipe_tower_cone_base(width, height, depth, angle_deg);
|
||||
if (R <= EPSILON)
|
||||
return box;
|
||||
const Vec2d center(width / 2., depth / 2.);
|
||||
Polygon ellipse;
|
||||
for (double alpha = 0.; alpha < 2. * M_PI; alpha += M_PI / 20.)
|
||||
ellipse.points.push_back(Point::new_scale(center + R * Vec2d(std::cos(alpha) / x_scale, std::sin(alpha))));
|
||||
Polygons u = union_({box, ellipse});
|
||||
return u.empty() ? box : u.front();
|
||||
}
|
||||
|
||||
// Static method to extract wipe_volumes[from][to] from the configuration.
|
||||
// Takes a ConfigBase so the GUI's wipe tower size estimate can pass the plate's
|
||||
// DynamicPrintConfig directly instead of materializing a full PrintConfig per call.
|
||||
|
||||
@@ -27,6 +27,10 @@ public:
|
||||
// in WipeTowerIntegration::append_tcr2 does not strip it.
|
||||
static const std::string wait_for_temp_tag() { return ";_WAIT_FOR_TEMP_ON_WIPE_TOWER"; }
|
||||
static std::pair<double, double> get_wipe_tower_cone_base(double width, double height, double depth, double angle_deg);
|
||||
// First-layer outline of a cone-wall tower in tower-local (scaled) coordinates: body box
|
||||
// unioned with the cone's base ellipse — the model first_layer_wipe_tower_corners uses,
|
||||
// and generate_support_cone_wall stays within it. Brim not included.
|
||||
static Polygon cone_base_polygon(double width, double depth, double height, double angle_deg);
|
||||
static std::vector<std::vector<float>> extract_wipe_volumes(const ConfigBase& config);
|
||||
// Estimated total flush volume of a SEMM print with the given number of filaments,
|
||||
// used to reserve wipe tower space before the tower is generated.
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
#include "WipeTowerEstimate.hpp"
|
||||
|
||||
#include "WipeTower.hpp"
|
||||
#include "WipeTower2.hpp"
|
||||
#include "../Config.hpp"
|
||||
#include "../PrintConfig.hpp"
|
||||
#include "../libslic3r.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <set>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Every caller today declares all these keys, but the signature accepts any ConfigBase: fall
|
||||
// back to the key's declared default, never to a hand-copied constant.
|
||||
static const ConfigOption *option_of(const ConfigBase &config, const char *key)
|
||||
{
|
||||
if (const ConfigOption *opt = config.option(key); opt != nullptr)
|
||||
return opt;
|
||||
if (const ConfigDef *def = config.def(); def != nullptr)
|
||||
if (const ConfigOptionDef *opt_def = def->get(key); opt_def != nullptr)
|
||||
return opt_def->default_value.get();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
WipeTowerType resolve_wipe_tower_type(const ConfigBase &config)
|
||||
{
|
||||
// printer_model is what the CLI keys its Bambu Lab detection on; the GUI's vendor flag
|
||||
// agrees for every shipped profile.
|
||||
if (const auto *model = dynamic_cast<const ConfigOptionString *>(config.option("printer_model"));
|
||||
model != nullptr && model->value.compare(0, 9, "Bambu Lab") == 0)
|
||||
return WipeTowerType::Type1;
|
||||
// By value, not by concrete type: a static PrintConfig holds ConfigOptionEnum<T>, a
|
||||
// DynamicConfig built from presets holds ConfigOptionEnumGeneric, and both answer getInt().
|
||||
const ConfigOption *type = option_of(config, "wipe_tower_type");
|
||||
return type != nullptr ? WipeTowerType(type->getInt()) : WipeTowerType::Type2;
|
||||
}
|
||||
|
||||
Polygon estimate_wipe_tower_first_layer_outline(const ConfigBase &config, WipeTowerType tower_type, double width, double depth, double height)
|
||||
{
|
||||
// Type1 ignores the cone option. The wall type is read by value: a preset-shaped config
|
||||
// holds it as ConfigOptionEnumGeneric, which a cast to ConfigOptionEnum<T> cannot see.
|
||||
const ConfigOption *wall_type = option_of(config, "wipe_tower_wall_type");
|
||||
const ConfigOption *cone_angle = option_of(config, "wipe_tower_cone_angle");
|
||||
const bool cone = tower_type == WipeTowerType::Type2 && wall_type != nullptr &&
|
||||
wall_type->getInt() == int(WipeTowerWallType::wtwCone) && cone_angle != nullptr;
|
||||
return WipeTower2::cone_base_polygon(width, depth, height, cone ? cone_angle->getFloat() : 0.);
|
||||
}
|
||||
|
||||
WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config, WipeTowerType tower_type, const std::vector<unsigned int> &filament_ids, double layer_height, double max_object_height)
|
||||
{
|
||||
WipeTowerFootprint footprint;
|
||||
footprint.height = max_object_height;
|
||||
const size_t filaments_cnt = filament_ids.size();
|
||||
if (filaments_cnt == 0 || layer_height < EPSILON)
|
||||
return footprint;
|
||||
|
||||
auto opt_float = [&config](const char *key) {
|
||||
const ConfigOption *opt = option_of(config, key);
|
||||
return opt != nullptr ? opt->getFloat() : 0.;
|
||||
};
|
||||
auto opt_bool = [&config](const char *key) {
|
||||
const ConfigOption *opt = option_of(config, key);
|
||||
return opt != nullptr && opt->getBool();
|
||||
};
|
||||
auto opt_enum = [&config](const char *key, int fallback) {
|
||||
const ConfigOption *opt = option_of(config, key);
|
||||
return opt != nullptr ? opt->getInt() : fallback;
|
||||
};
|
||||
auto floats_of = [&config](const char *key) { return dynamic_cast<const ConfigOptionFloats *>(option_of(config, key)); };
|
||||
auto max_of = [&floats_of](const char *key, double fallback) {
|
||||
const auto *opt = floats_of(key);
|
||||
return (opt != nullptr && !opt->values.empty()) ? *std::max_element(opt->values.begin(), opt->values.end()) : fallback;
|
||||
};
|
||||
auto float_at = [&floats_of](const char *key, unsigned int id, double fallback) {
|
||||
const auto *opt = floats_of(key);
|
||||
return (opt != nullptr && !opt->values.empty()) ? opt->get_at(id) : fallback;
|
||||
};
|
||||
auto int_at = [&config](const char *key, unsigned int id, int fallback) {
|
||||
const auto *opt = dynamic_cast<const ConfigOptionInts *>(option_of(config, key));
|
||||
return (opt != nullptr && !opt->values.empty()) ? opt->get_at(id) : fallback;
|
||||
};
|
||||
|
||||
// Both planners size every layer, so the tower has to fit its thinnest one: the first layer
|
||||
// when it is printed thinner than the rest.
|
||||
const double first_layer_height = opt_float("initial_layer_print_height");
|
||||
if (first_layer_height > EPSILON)
|
||||
layer_height = std::min(layer_height, first_layer_height);
|
||||
|
||||
const bool type1 = tower_type == WipeTowerType::Type1;
|
||||
const double width = opt_float("prime_tower_width");
|
||||
const double prime_volume = opt_float("prime_volume");
|
||||
// Type1 spaces its purge lines by prime_tower_infill_gap, Type2 by wipe_tower_extra_spacing.
|
||||
// Type2's extra flow cancels out of the depth: the line length is divided by it and the row
|
||||
// pitch multiplied by it (WipeTower2::get_wipe_depth).
|
||||
const double extra_spacing = opt_float(type1 ? "prime_tower_infill_gap" : "wipe_tower_extra_spacing") / 100.;
|
||||
const double rib_width = opt_float("wipe_tower_rib_width");
|
||||
const double extra_rib_length = opt_float("wipe_tower_extra_rib_length");
|
||||
const auto *nozzle_opt = floats_of("nozzle_diameter");
|
||||
const double nozzle_diameter = (nozzle_opt != nullptr && !nozzle_opt->values.empty()) ? nozzle_opt->values.front() : 0.4;
|
||||
const bool dual_nozzle = nozzle_opt != nullptr && nozzle_opt->values.size() == 2;
|
||||
const bool rib_wall = opt_enum("wipe_tower_wall_type", int(WipeTowerWallType::wtwRectangle)) == int(WipeTowerWallType::wtwRib);
|
||||
const bool smooth_timelapse = opt_enum("timelapse_type", int(TimelapseType::tlTraditional)) == int(TimelapseType::tlSmooth);
|
||||
const bool wrapping = opt_bool("enable_wrapping_detection");
|
||||
// Reasons a tower is printed with no tool change to purge for: the ones that stop
|
||||
// normalize_fdm_2 clearing enable_prime_tower. Its mixed-filament case is not modelled.
|
||||
const bool need_wipe_tower = smooth_timelapse || wrapping;
|
||||
|
||||
// A tower printed for one of the reasons above has no tool change to purge for; both
|
||||
// planners give it the idle depth below and nothing more.
|
||||
const size_t purge_count = filaments_cnt > 1 ? (dual_nozzle ? filaments_cnt : filaments_cnt - 1) : 0;
|
||||
|
||||
// Type2 purges one volume per tool change. Type1 plans per filament below; here the volume
|
||||
// only decides whether a tower exists.
|
||||
double volume = prime_volume * double(purge_count);
|
||||
if (dual_nozzle) {
|
||||
// Dual-nozzle printers also purge the filament change length on the tower.
|
||||
const double length = max_of("filament_change_length", 0.);
|
||||
const double diameter = max_of("filament_diameter", 1.75);
|
||||
volume += length * PI * diameter * diameter / 4. * double(filaments_cnt / 2);
|
||||
}
|
||||
// Single-extruder multi-material purges the flush matrix instead of the prime volume.
|
||||
const bool semm_flush = opt_bool("purge_in_prime_tower") && opt_bool("single_extruder_multi_material");
|
||||
if (semm_flush)
|
||||
volume = WipeTower2::estimate_semm_flush_volume(config, filaments_cnt);
|
||||
|
||||
// The Type1 planner wipes each filament's own prime volume after changing to it, in a block
|
||||
// per adhesiveness category. On a two-nozzle printer the leaving filament is also rammed at
|
||||
// every nozzle change; the tool order groups filaments by nozzle, so a layer crosses
|
||||
// (nozzles used - 1) times, charged here to the longest ramming.
|
||||
std::vector<WipeTower::PurgeEstimate> purges;
|
||||
if (type1 && filaments_cnt > 1) {
|
||||
const bool saving_mode = opt_enum("prime_volume_mode", int(PrimeVolumeMode::pvmDefault)) == int(PrimeVolumeMode::pvmSaving);
|
||||
std::set<int> nozzles;
|
||||
size_t longest_ramming = 0;
|
||||
for (size_t i = 0; i < filaments_cnt; ++i) {
|
||||
const unsigned int id = filament_ids[i];
|
||||
WipeTower::PurgeEstimate purge;
|
||||
purge.prime_volume = saving_mode ? 15.f : float(float_at("filament_prime_volume", id, prime_volume));
|
||||
purge.category = int_at("filament_adhesiveness_category", id, 0);
|
||||
purge.filament_diameter = float(float_at("filament_diameter", id, 1.75));
|
||||
purges.push_back(purge);
|
||||
if (dual_nozzle) {
|
||||
nozzles.insert(int_at("filament_map", id, 1));
|
||||
if (float_at("filament_change_length", id, 0.) > float_at("filament_change_length", filament_ids[longest_ramming], 0.))
|
||||
longest_ramming = i;
|
||||
}
|
||||
}
|
||||
if (nozzles.size() > 1)
|
||||
purges[longest_ramming].filament_change_length = float(float_at("filament_change_length", filament_ids[longest_ramming], 0.) * double(nozzles.size() - 1));
|
||||
}
|
||||
|
||||
// Both wall types decide this together: over-reserving only wastes bed area, but reporting
|
||||
// no tower for one that is built collapses the validation hull to a point.
|
||||
// A tool change is a reason on its own (see the base commit); Type1 already reserves
|
||||
// per filament, Type2 has only the volume, which can resolve to zero.
|
||||
const bool has_purge = type1 ? !purges.empty() : volume > EPSILON;
|
||||
if (!has_purge && filaments_cnt < 2 && !need_wipe_tower)
|
||||
return footprint;
|
||||
|
||||
const double min_depth = WipeTower::get_limit_depth_by_height(float(max_object_height));
|
||||
const float perimeter_width = float(nozzle_diameter) * 1.25f; // Width_To_Nozzle_Ratio
|
||||
// With nothing to purge, plan_tower_new sizes the tower for wrapping detection or the
|
||||
// stability minimum; WipeTower2 only knows the latter.
|
||||
const double idle_depth = (type1 && wrapping && !smooth_timelapse) ? WipeTower::get_wrapping_detection_depth() : min_depth;
|
||||
if (rib_wall) {
|
||||
// Both planners square the tower to the purge area and extend the ribs, not the body,
|
||||
// below the stability minimum.
|
||||
double side;
|
||||
if (!purges.empty())
|
||||
side = WipeTower::estimate_rib_tower_bbox_side(purges, float(width), float(layer_height), float(nozzle_diameter), float(extra_spacing), float(rib_width), float(extra_rib_length), float(max_object_height));
|
||||
else {
|
||||
const double square = has_purge ? std::sqrt(volume / layer_height * extra_spacing) : idle_depth;
|
||||
side = WipeTower::rib_footprint_side(float(square), float(square), float(rib_width), float(extra_rib_length), float(max_object_height));
|
||||
}
|
||||
footprint.width = footprint.depth = side;
|
||||
} else {
|
||||
double depth;
|
||||
if (type1) {
|
||||
// plan_tower_new stretches a short purge stack to the stability minimum behind its
|
||||
// leading perimeter width.
|
||||
depth = purges.empty() ? idle_depth : std::max(min_depth + perimeter_width, double(WipeTower::estimate_tower_blocks_depth(purges, float(width), float(layer_height), float(nozzle_diameter), float(extra_spacing))));
|
||||
} else {
|
||||
depth = volume / (layer_height * width);
|
||||
// The flush volumes already hold the spacing between wipes.
|
||||
if (!semm_flush)
|
||||
depth *= extra_spacing;
|
||||
depth = std::max(min_depth, depth);
|
||||
}
|
||||
footprint.width = width;
|
||||
footprint.depth = depth;
|
||||
}
|
||||
|
||||
footprint.brim_width = opt_float("prime_tower_brim_width");
|
||||
if (footprint.brim_width < 0)
|
||||
footprint.brim_width = WipeTower::get_auto_brim_by_height(float(max_object_height));
|
||||
footprint.brim_width = WipeTower::estimate_brim_real_width(float(footprint.brim_width), float(nozzle_diameter), float(first_layer_height > EPSILON ? first_layer_height : layer_height), !type1);
|
||||
return footprint;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "../Polygon.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
class ConfigBase;
|
||||
enum class WipeTowerType;
|
||||
|
||||
// Pre-slice footprint of the wipe tower, shared by validation (Print), the GUI's placement
|
||||
// clamp/preview/arrange and the CLI placement. The arithmetic is shared; the inputs below are
|
||||
// not, so a change to how one caller derives them has to be mirrored in the others.
|
||||
struct WipeTowerFootprint
|
||||
{
|
||||
double width = 0.; // effective width: equals depth for a rib wall, which squares the tower
|
||||
double depth = 0.; // 0 when these inputs imply no tower
|
||||
double height = 0.; // tallest object; drives the stability floor and the auto brim
|
||||
double brim_width = 0.; // printed width: auto (-1) resolved by height, laid in whole loops
|
||||
};
|
||||
|
||||
// Which planner builds the tower: Bambu Lab printers always get Type1, the rest follow
|
||||
// wipe_tower_type. The rule Print::wipe_tower_type() and the CLI apply, read off the config so
|
||||
// the GUI and CLI placement can resolve it without a Print.
|
||||
WipeTowerType resolve_wipe_tower_type(const ConfigBase &config);
|
||||
|
||||
// First-layer outline of an estimated tower in tower-local scaled coordinates, brim excluded:
|
||||
// the body box, or for a Type2 cone wall the box unioned with the cone's base. The preview,
|
||||
// the placement margin and validation all take the outline from here so they cannot disagree
|
||||
// about whether a cone exists.
|
||||
Polygon estimate_wipe_tower_first_layer_outline(const ConfigBase &config, WipeTowerType tower_type, double width, double depth, double height);
|
||||
|
||||
// filament_ids: 0-based filaments purged on the plate. The config cannot see custom G-code tool
|
||||
// changes, so ids derived from the model must include them
|
||||
// (Print::extruders(true)) or a real tower is sized as if it were never built.
|
||||
// layer_height: thinnest layer the objects are sliced at. The first layer is folded in here.
|
||||
//
|
||||
// A raft is deliberately not a reason: normalize_fdm_2 clears enable_prime_tower for a plate
|
||||
// purging one filament unless smooth timelapse or wrapping detection is on.
|
||||
WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase &config,
|
||||
WipeTowerType tower_type,
|
||||
const std::vector<unsigned int> &filament_ids,
|
||||
double layer_height,
|
||||
double max_object_height);
|
||||
|
||||
} // namespace Slic3r
|
||||
Reference in New Issue
Block a user