From 449cb0b27c1525449bd7b0883adf6e56d2f0f950 Mon Sep 17 00:00:00 2001 From: andrewboktor Date: Sat, 16 Dec 2023 21:48:21 -0800 Subject: [PATCH] ENH: Full re-write of spiral vase Github: 2744 * Practically full re-write of spiral vase - Adds transition out to prevent sharp edge at the top of spiral vase. - Adds XY interpolation - Adds option to turn XY interpolation on/off * - Increasing E to 5 decimal digits (I observed uneven flow with less than that) - Excluding all travel moves (I saw a bug where somehow we ended up with travel moves within the print so excluding all travel moves) * - max_xy_smoothing is now configurable, default is 200% of nozzle_diameter - fixed no-op travel moves in the middle of spiral that now show up as defects when Smooth Spiral is enabled! * - Avoiding namespace pollution - Fixing dist_XY == 0 bug --------- Co-authored-by: Andrew Boktor Co-authored-by: SoftFever Change-Id: I4e982b6192f730037ff497389454313af3905e82 --- src/libslic3r/GCode.cpp | 24 +++-- src/libslic3r/GCode/SpiralVase.cpp | 147 +++++++++++++++++++++++--- src/libslic3r/GCode/SpiralVase.hpp | 24 ++++- src/libslic3r/Preset.cpp | 2 +- src/libslic3r/PrintConfig.cpp | 19 ++++ src/libslic3r/PrintConfig.hpp | 2 + src/slic3r/GUI/ConfigManipulation.cpp | 2 + src/slic3r/GUI/Tab.cpp | 2 + 8 files changed, 195 insertions(+), 27 deletions(-) diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 8e724f668..0bc7268d7 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -2456,10 +2456,16 @@ void GCode::process_layers( return this->process_layer(print, layer.second, layer_tools, &layer == &layers_to_print.back(), &print_object_instances_ordering, size_t(-1)); } }); - const auto spiral_mode = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [&spiral_mode = *this->m_spiral_vase.get()](GCode::LayerResult in) -> GCode::LayerResult { + if (m_spiral_vase) { + float nozzle_diameter = EXTRUDER_CONFIG(nozzle_diameter); + float max_xy_smoothing = m_config.get_abs_value("spiral_mode_max_xy_smoothing", nozzle_diameter); + this->m_spiral_vase->set_max_xy_smoothing(max_xy_smoothing); + } + const auto spiral_mode = tbb::make_filter( + slic3r_tbb_filtermode::serial_in_order, [&spiral_mode = *this->m_spiral_vase.get(), & layers_to_print](GCode::LayerResult in) -> GCode::LayerResult { spiral_mode.enable(in.spiral_vase_enable); - return { spiral_mode.process_layer(std::move(in.gcode)), in.layer_id, in.spiral_vase_enable, in.cooling_buffer_flush }; + bool last_layer = in.layer_id == layers_to_print.size() - 1; + return { spiral_mode.process_layer(std::move(in.gcode), last_layer), in.layer_id, in.spiral_vase_enable, in.cooling_buffer_flush}; }); const auto cooling = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, [&cooling_buffer = *this->m_cooling_buffer.get()](GCode::LayerResult in) -> std::string { @@ -2504,10 +2510,16 @@ void GCode::process_layers( return this->process_layer(print, { std::move(layer) }, tool_ordering.tools_for_layer(layer.print_z()), &layer == &layers_to_print.back(), nullptr, single_object_idx, prime_extruder); } }); - const auto spiral_mode = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [&spiral_mode = *this->m_spiral_vase.get()](GCode::LayerResult in)->GCode::LayerResult { + if (m_spiral_vase) { + float nozzle_diameter = EXTRUDER_CONFIG(nozzle_diameter); + float max_xy_smoothing = m_config.get_abs_value("spiral_mode_max_xy_smoothing", nozzle_diameter); + this->m_spiral_vase->set_max_xy_smoothing(max_xy_smoothing); + } + const auto spiral_mode = tbb::make_filter( + slic3r_tbb_filtermode::serial_in_order, [&spiral_mode = *this->m_spiral_vase.get(), &layers_to_print](GCode::LayerResult in) -> GCode::LayerResult { spiral_mode.enable(in.spiral_vase_enable); - return { spiral_mode.process_layer(std::move(in.gcode)), in.layer_id, in.spiral_vase_enable, in.cooling_buffer_flush }; + bool last_layer = in.layer_id == layers_to_print.size() - 1; + return { spiral_mode.process_layer(std::move(in.gcode), last_layer), in.layer_id, in.spiral_vase_enable, in.cooling_buffer_flush }; }); const auto cooling = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, [&cooling_buffer = *this->m_cooling_buffer.get()](GCode::LayerResult in)->std::string { diff --git a/src/libslic3r/GCode/SpiralVase.cpp b/src/libslic3r/GCode/SpiralVase.cpp index c3caee2dc..5f0e77300 100644 --- a/src/libslic3r/GCode/SpiralVase.cpp +++ b/src/libslic3r/GCode/SpiralVase.cpp @@ -1,10 +1,77 @@ #include "SpiralVase.hpp" #include "GCode.hpp" #include +#include +#include namespace Slic3r { -std::string SpiralVase::process_layer(const std::string &gcode) +namespace SpiralVaseHelpers { +/** == Smooth Spiral Helpers == */ +/** Distance between a and b */ +float distance(SpiralVase::SpiralPoint a, SpiralVase::SpiralPoint b) { + return sqrt(pow(a.x - b.x, 2) + pow(a.y - b.y, 2)); +} + +SpiralVase::SpiralPoint subtract(SpiralVase::SpiralPoint a, SpiralVase::SpiralPoint b) +{ + return SpiralVase::SpiralPoint(a.x - b.x, a.y - b.y); +} + +SpiralVase::SpiralPoint add(SpiralVase::SpiralPoint a, SpiralVase::SpiralPoint b) { + return SpiralVase::SpiralPoint(a.x + b.x, a.y + b.y); +} + +SpiralVase::SpiralPoint scale(SpiralVase::SpiralPoint a, float factor) { + return SpiralVase::SpiralPoint(a.x * factor, a.y * factor); +} + +/** dot product */ +float dot(SpiralVase::SpiralPoint a, SpiralVase::SpiralPoint b) { + return a.x * b.x + a.y * b.y; +} + +/** Find the point on line ab closes to point c */ +SpiralVase::SpiralPoint nearest_point_on_line(SpiralVase::SpiralPoint c, SpiralVase::SpiralPoint a, SpiralVase::SpiralPoint b, float& dist) +{ + SpiralVase::SpiralPoint ab = subtract(b, a); + SpiralVase::SpiralPoint ac = subtract(c, a); + float t = dot(ac, ab) / dot(ab, ab); + t = t > 1 ? 1 : t; + t = t < 0 ? 0 : t; + SpiralVase::SpiralPoint closest = SpiralVase::SpiralPoint(add(a, scale(ab, t))); + dist = distance(c, closest); + return closest; +} + +/** Given a set of lines defined by points such as line[n] is the line from points[n] to points[n+1], + * find the closest point to p that falls on any of the lines */ +SpiralVase::SpiralPoint nearest_point_on_lines(SpiralVase::SpiralPoint p, + std::shared_ptr> points, + bool& found, + float& dist) +{ + if (points->size() < 2) { + found = false; + return SpiralVase::SpiralPoint(0, 0); + } + float min = std::numeric_limits::max(); + SpiralVase::SpiralPoint closest(0, 0); + for (unsigned long i = 0; i < points->size() - 1; i++) { + float currentDist = 0; + SpiralVase::SpiralPoint current = nearest_point_on_line(p, points->at(i), points->at(i + 1), currentDist); + if (currentDist < min) { + min = currentDist; + closest = current; + found = true; + } + } + dist = min; + return closest; +} +} // namespace SpiralVase + +std::string SpiralVase::process_layer(const std::string &gcode, bool last_layer) { /* This post-processor relies on several assumptions: - all layers are processed through it, including those that are not supposed @@ -45,19 +112,29 @@ std::string SpiralVase::process_layer(const std::string &gcode) } }); } - + // Remove layer height from initial Z. z -= layer_height; - + + std::shared_ptr> current_layer = std::make_shared>(); + std::shared_ptr> previous_layer = m_previous_layer; + + bool smooth_spiral = m_smooth_spiral; std::string new_gcode; + std::string transition_gcode; + float max_xy_dist_for_smoothing = m_max_xy_smoothing; //FIXME Tapering of the transition layer only works reliably with relative extruder distances. // For absolute extruder distances it will be switched off. // Tapering the absolute extruder distances requires to process every extrusion value after the first transition // layer. - bool transition = m_transition_layer && m_config.use_relative_e_distances.value; - float layer_height_factor = layer_height / total_layer_length; + bool transition_in = m_transition_layer && m_config.use_relative_e_distances.value; + bool transition_out = last_layer && m_config.use_relative_e_distances.value; float len = 0.f; - m_reader.parse_buffer(gcode, [&new_gcode, &z, total_layer_length, layer_height_factor, transition, &len] + //set initial point + SpiralVase::SpiralPoint last_point = previous_layer != NULL && previous_layer->size() > 0 ? previous_layer->at(previous_layer->size()-1): SpiralVase::SpiralPoint(0,0); + + m_reader.parse_buffer(gcode, [&new_gcode, &z, total_layer_length, layer_height, transition_in, &len, ¤t_layer, &previous_layer, &transition_gcode, transition_out, + smooth_spiral, &max_xy_dist_for_smoothing, &last_point] (GCodeReader &reader, GCodeReader::GCodeLine line) { if (line.cmd_is("G1")) { if (line.has_z()) { @@ -69,29 +146,67 @@ std::string SpiralVase::process_layer(const std::string &gcode) } else { float dist_XY = line.dist_XY(reader); if (dist_XY > 0) { - // horizontal move - if (line.extruding(reader)) { + if (line.extruding(reader)) { // Exclude wipe and retract len += dist_XY; - line.set(reader, Z, z + len * layer_height_factor); - if (transition && line.has(E)) - // Transition layer, modulate the amount of extrusion from zero to the final value. - line.set(reader, E, line.value(E) * len / total_layer_length); + float factor = len / total_layer_length; + if (transition_in) + // Transition layer, interpolate the amount of extrusion from zero to the final value. + line.set(reader, E, line.e() * factor, 5 /*decimal_digits*/); + else if (transition_out) { + // We want the last layer to ramp down extrusion, but without changing z height! + // So clone the line before we mess with its Z and duplicate it into a new layer that ramps down E + // We add this new layer at the very end + GCodeReader::GCodeLine transitionLine(line); + transitionLine.set(reader, E, line.e() * (1 - factor), 5 /*decimal_digits*/); + transition_gcode += transitionLine.raw() + '\n'; + } + // This line is the core of Spiral Vase mode, ramp up the Z smoothly + line.set(reader, Z, z + factor * layer_height); + if (smooth_spiral) { + // Now we also need to try to interpolate X and Y + SpiralVase::SpiralPoint p(line.x(), line.y()); // Get current x/y coordinates + current_layer->push_back(p); // Store that point for later use on the next layer + if (previous_layer != NULL) { + bool found = false; + float dist = 0; + SpiralVase::SpiralPoint nearestp = SpiralVaseHelpers::nearest_point_on_lines(p, previous_layer, found, dist); + if (found && dist < max_xy_dist_for_smoothing) { + // Interpolate between the point on this layer and the point on the previous layer + SpiralVase::SpiralPoint target = SpiralVaseHelpers::add(SpiralVaseHelpers::scale(nearestp, 1 - factor), SpiralVaseHelpers::scale(p, factor)); + line.set(reader, X, target.x); + line.set(reader, Y, target.y); + // We need to figure out the distance of this new line! + float modified_dist_XY = SpiralVaseHelpers::distance(last_point, target); + // Scale the extrusion amount according to change in length + line.set(reader, E, line.e() * modified_dist_XY / dist_XY, 5 /*decimal_digits*/); + last_point = target; + } else { + last_point = p; + } + } + } new_gcode += line.raw() + '\n'; } return; - /* Skip travel moves: the move to first perimeter point will cause a visible seam when loops are not aligned in XY; by skipping it we blend the first loop move in the XY plane (although the smoothness of such blend depend on how long the first segment is; maybe we should - enforce some minimum length?). */ + enforce some minimum length?). + When smooth_spiral is enabled, we're gonna end up exactly where the next layer should + start anyway, so we don't need the travel move */ } } } new_gcode += line.raw() + '\n'; + if(transition_out) { + transition_gcode += line.raw() + '\n'; + } }); - - return new_gcode; + + m_previous_layer = current_layer; + + return new_gcode + transition_gcode; } } diff --git a/src/libslic3r/GCode/SpiralVase.hpp b/src/libslic3r/GCode/SpiralVase.hpp index 1e29b4992..ba9692b95 100644 --- a/src/libslic3r/GCode/SpiralVase.hpp +++ b/src/libslic3r/GCode/SpiralVase.hpp @@ -6,14 +6,25 @@ namespace Slic3r { -class SpiralVase { +class SpiralVase +{ public: + class SpiralPoint + { + public: + SpiralPoint(float paramx, float paramy) : x(paramx), y(paramy) {} + + public: + float x, y; + }; SpiralVase(const PrintConfig &config) : m_config(config) { //BBS //m_reader.z() = (float)m_config.z_offset; m_reader.z() = 0.0f; m_reader.apply_config(m_config); + m_previous_layer = NULL; + m_smooth_spiral = config.spiral_mode_smooth; }; void enable(bool en) { @@ -21,17 +32,22 @@ public: m_enabled = en; } - std::string process_layer(const std::string &gcode); - + std::string process_layer(const std::string &gcode, bool last_layer); + void set_max_xy_smoothing(float max) { + m_max_xy_smoothing = max; + } private: const PrintConfig &m_config; GCodeReader m_reader; + float m_max_xy_smoothing = 0.f; bool m_enabled = false; // First spiral vase layer. Layer height has to be ramped up from zero to the target layer height. bool m_transition_layer = false; + // Whether to interpolate XY coordinates with the previous layer. Results in no seam at layer changes + bool m_smooth_spiral = false; + std::shared_ptr> m_previous_layer; }; - } #endif // slic3r_SpiralVase_hpp_ diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 84ca29e5d..2c3900daa 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -792,7 +792,7 @@ bool Preset::has_cali_lines(PresetBundle* preset_bundle) } static std::vector s_Preset_print_options { - "layer_height", "initial_layer_print_height", "wall_loops", "slice_closing_radius", "spiral_mode", "slicing_mode", + "layer_height", "initial_layer_print_height", "wall_loops", "slice_closing_radius", "spiral_mode", "spiral_mode_smooth", "spiral_mode_max_xy_smoothing", "slicing_mode", "top_shell_layers", "top_shell_thickness", "bottom_shell_layers", "bottom_shell_thickness", "ensure_vertical_shell_thickness", "reduce_crossing_wall", "detect_thin_wall", "detect_overhang_wall", "seam_position", "wall_sequence", "is_infill_first", "sparse_infill_density", "sparse_infill_pattern", "sparse_infill_anchor", "sparse_infill_anchor_max", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index ddd9ebd33..40f133e59 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -3014,6 +3014,25 @@ void PrintConfigDef::init_fff_params() def->mode = comSimple; def->set_default_value(new ConfigOptionBool(false)); + def = this->add("spiral_mode_smooth", coBool); + def->label = L("Smooth Spiral"); + def->tooltip = L("Smooth Spiral smoothes out X and Y moves as well" + "resulting in no visible seam at all, even in the XY directions on walls that are not vertical"); + def->mode = comSimple; + def->set_default_value(new ConfigOptionBool(false)); + + def = this->add("spiral_mode_max_xy_smoothing", coFloatOrPercent); + def->label = L("Max XY Smoothing"); + def->tooltip = L("Maximum distance to move points in XY to try to achieve a smooth spiral" + "If expressed as a %, it will be computed over nozzle diameter"); + def->sidetext = L("mm or %"); + def->ratio_over = "nozzle_diameter"; + def->min = 0; + def->max = 1000; + def->max_literal = 10; + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionFloatOrPercent(200, true)); + def = this->add("timelapse_type", coEnum); def->label = L("Timelapse"); def->tooltip = L("If smooth or traditional mode is selected, a timelapse video will be generated for each print. " diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index ff853730b..fcb637ea1 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -1043,6 +1043,8 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionInt, skirt_loops)) ((ConfigOptionInts, slow_down_layer_time)) ((ConfigOptionBool, spiral_mode)) + ((ConfigOptionBool, spiral_mode_smooth)) + ((ConfigOptionFloatOrPercent, spiral_mode_max_xy_smoothing)) ((ConfigOptionInt, standby_temperature_delta)) ((ConfigOptionInts, nozzle_temperature)) ((ConfigOptionInts, chamber_temperatures)) diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index b0b88f700..62ff416ca 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -602,6 +602,8 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, co toggle_line("sparse_infill_anchor", has_infill_anchors); bool has_spiral_vase = config->opt_bool("spiral_mode"); + toggle_line("spiral_mode_smooth", has_spiral_vase); + toggle_line("spiral_mode_max_xy_smoothing", config->opt_bool("spiral_mode_smooth")); bool has_top_solid_infill = config->opt_int("top_shell_layers") > 0; bool has_bottom_solid_infill = config->opt_int("bottom_shell_layers") > 0; bool has_solid_infill = has_top_solid_infill || has_bottom_solid_infill; diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp index c45ba4e8a..f7ada5b89 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -2149,6 +2149,8 @@ void TabPrint::build() optgroup->append_single_option_line("slicing_mode"); optgroup->append_single_option_line("print_sequence", "sequent-print"); optgroup->append_single_option_line("spiral_mode", "spiral-vase"); + optgroup->append_single_option_line("spiral_mode_smooth", "spiral-vase#smooth"); + optgroup->append_single_option_line("spiral_mode_max_xy_smoothing", "spiral-vase#max-xy-smoothing"); optgroup->append_single_option_line("timelapse_type", "Timelapse"); optgroup->append_single_option_line("fuzzy_skin", "parameter/fuzzy-skin");