diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index bde9ffd1052..bfb25a3d35a 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -1,4 +1,5 @@ ///|/ Copyright (c) Prusa Research 2016 - 2023 Lukáš Matěna @lukasmatena, Vojtěch Bubník @bubnikv, Enrico Turri @enricoturri1966, Pavel Mikuš @Godrak, Oleksandra Iushchenko @YuSanka, Lukáš Hejl @hejllukas, Filip Sykala @Jony01, David Kocík @kocikdav +///|/ Copyright (c) OrcaSlicer 2023 Andrew Boktor @aboktor, @SoftFever ///|/ Copyright (c) SuperSlicer 2023 Remi Durand @supermerill ///|/ Copyright (c) 2021 Justin Schuh @jschuh ///|/ Copyright (c) 2020 Paul Arden @ardenpm @@ -1505,13 +1506,19 @@ void GCodeGenerator::process_layers( &layer == &layers_to_print.back(), &print_object_instances_ordering, size_t(-1)); } }); + if (m_spiral_vase) { + float nozzle_diameter = EXTRUDER_CONFIG(nozzle_diameter); + float max_xy_smoothing = m_config.get_abs_value("spiral_vase_max_xy_smoothing", nozzle_diameter); + this->m_spiral_vase->set_max_xy_smoothing(max_xy_smoothing); + } // The pipeline is variable: The vase mode filter is optional. const auto spiral_vase = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [spiral_vase = this->m_spiral_vase.get()](LayerResult in) -> LayerResult { + [spiral_vase = this->m_spiral_vase.get(), &layers_to_print](LayerResult in) -> LayerResult { if (in.nop_layer_result) return in; spiral_vase->enable(in.spiral_vase_enable); - return { spiral_vase->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_vase->process_layer(std::move(in.gcode), last_layer), in.layer_id, in.spiral_vase_enable, in.cooling_buffer_flush}; }); const auto pressure_equalizer = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, [pressure_equalizer = this->m_pressure_equalizer.get()](LayerResult in) -> LayerResult { @@ -1599,12 +1606,19 @@ void GCodeGenerator::process_layers( } }); // The pipeline is variable: The vase mode filter is optional. - const auto spiral_vase = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, - [spiral_vase = this->m_spiral_vase.get()](LayerResult in)->LayerResult { + if (m_spiral_vase) { + float nozzle_diameter = EXTRUDER_CONFIG(nozzle_diameter); + float max_xy_smoothing = m_config.get_abs_value("spiral_vase_max_xy_smoothing", nozzle_diameter); + this->m_spiral_vase->set_max_xy_smoothing(max_xy_smoothing); + } + const auto spiral_vase = tbb::make_filter( + slic3r_tbb_filtermode::serial_in_order, + [spiral_vase = this->m_spiral_vase.get(), &layers_to_print](LayerResult in)->LayerResult { if (in.nop_layer_result) return in; spiral_vase->enable(in.spiral_vase_enable); - return { spiral_vase->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_vase->process_layer(std::move(in.gcode), last_layer), in.layer_id, in.spiral_vase_enable, in.cooling_buffer_flush }; }); const auto pressure_equalizer = tbb::make_filter(slic3r_tbb_filtermode::serial_in_order, [pressure_equalizer = this->m_pressure_equalizer.get()](LayerResult in) -> LayerResult { diff --git a/src/libslic3r/GCode/SpiralVase.cpp b/src/libslic3r/GCode/SpiralVase.cpp index 4fb52443ac5..bf4f3a57a68 100644 --- a/src/libslic3r/GCode/SpiralVase.cpp +++ b/src/libslic3r/GCode/SpiralVase.cpp @@ -1,3 +1,4 @@ +///|/ Copyright (c) OrcaSlicer 2023 Andrew Boktor @aboktor ///|/ Copyright (c) Prusa Research 2017 - 2021 Vojtěch Bubník @bubnikv, Lukáš Matěna @lukasmatena ///|/ ///|/ ported from lib/Slic3r/GCode/SpiralVase.pm: @@ -5,14 +6,72 @@ ///|/ Copyright (c) Slic3r 2013 - 2014 Alessandro Ranellucci @alranel ///|/ ///|/ PrusaSlicer is released under the terms of the AGPLv3 or higher -///|/ #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 ca = subtract(c, a); + float t = dot(ca, 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::vector *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 SpiralVaseHelpers + +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 @@ -21,52 +80,61 @@ std::string SpiralVase::process_layer(const std::string &gcode) at the beginning - each layer is composed by suitable geometry (i.e. a single complete loop) - loops were not clipped before calling this method */ - + // If we're not going to modify G-code, just feed it to the reader // in order to update positions. - if (! m_enabled) { + if (!m_enabled) { m_reader.parse_buffer(gcode); return gcode; } - + // Get total XY length for this layer by summing all extrusion moves. float total_layer_length = 0; - float layer_height = 0; - float z = 0.f; - + float layer_height = 0; + float z = 0.f; + { - //FIXME Performance warning: This copies the GCodeConfig of the reader. - GCodeReader r = m_reader; // clone - bool set_z = false; - r.parse_buffer(gcode, [&total_layer_length, &layer_height, &z, &set_z] - (GCodeReader &reader, const GCodeReader::GCodeLine &line) { + // FIXME Performance warning: This copies the GCodeConfig of the reader. + GCodeReader r = m_reader; // clone + bool set_z = false; + r.parse_buffer(gcode, [&total_layer_length, &layer_height, &z, &set_z](GCodeReader &reader, const GCodeReader::GCodeLine &line) { if (line.cmd_is("G1")) { if (line.extruding(reader)) { total_layer_length += line.dist_XY(reader); } else if (line.has(Z)) { layer_height += line.dist_Z(reader); if (!set_z) { - z = line.new_Z(reader); + z = line.new_Z(reader); set_z = true; } } } }); } - - // Remove layer height from initial Z. + + //  Remove layer height from initial Z. z -= layer_height; - + + std::vector *current_layer = new std::vector(); + std::vector *previous_layer = m_previous_layer; + + bool smooth_spiral = m_smooth_spiral; std::string new_gcode; - //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; - float len = 0.f; - m_reader.parse_buffer(gcode, [&new_gcode, &z, total_layer_length, layer_height_factor, transition, &len] - (GCodeReader &reader, GCodeReader::GCodeLine line) { + 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_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; + 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()) { // If this is the initial Z move of the layer, replace it with a @@ -76,30 +144,70 @@ std::string SpiralVase::process_layer(const std::string &gcode) return; } else { float dist_XY = line.dist_XY(reader); - if (dist_XY > 0) { - // horizontal move - if (line.extruding(reader)) { + if (line.has_x() || line.has_y()) { // Sometimes lines have X/Y but the move is to the last position + if (dist_XY > 0 && 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; -} + delete m_previous_layer; + m_previous_layer = current_layer; + + return new_gcode + transition_gcode; } + +} // namespace Slic3r diff --git a/src/libslic3r/GCode/SpiralVase.hpp b/src/libslic3r/GCode/SpiralVase.hpp index 8829e8d22b8..3cfa60c04c0 100644 --- a/src/libslic3r/GCode/SpiralVase.hpp +++ b/src/libslic3r/GCode/SpiralVase.hpp @@ -1,3 +1,4 @@ +///|/ Copyright (c) OrcaSlicer 2023 Andrew Boktor @aboktor, @SoftFever ///|/ Copyright (c) Prusa Research 2017 - 2021 Vojtěch Bubník @bubnikv ///|/ ///|/ ported from lib/Slic3r/GCode/SpiralVase.pm: @@ -14,31 +15,46 @@ 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) { - m_reader.z() = (float)m_config.z_offset; + m_reader.z() = (float) m_config.z_offset; m_reader.apply_config(m_config); + m_previous_layer = NULL; + m_smooth_spiral = config.spiral_vase_smooth; }; - void enable(bool en) { - m_transition_layer = en && ! m_enabled; - m_enabled = en; + void enable(bool en) + { + m_transition_layer = en && !m_enabled; + 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; + const PrintConfig &m_config; + GCodeReader m_reader; + float m_max_xy_smoothing = 0.f; - bool m_enabled = false; + 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; + 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::vector *m_previous_layer; }; - -} +} // namespace Slic3r #endif // slic3r_SpiralVase_hpp_ diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp index 44c29c32c26..6fbd68cbc92 100644 --- a/src/libslic3r/Preset.cpp +++ b/src/libslic3r/Preset.cpp @@ -436,7 +436,7 @@ void Preset::set_visible_from_appconfig(const AppConfig &app_config) } static std::vector s_Preset_print_options { - "layer_height", "first_layer_height", "perimeters", "spiral_vase", "slice_closing_radius", "slicing_mode", + "layer_height", "first_layer_height", "perimeters", "spiral_vase", "spiral_vase_smooth", "spiral_vase_max_xy_smoothing", "slice_closing_radius", "slicing_mode", "top_solid_layers", "top_solid_min_thickness", "bottom_solid_layers", "bottom_solid_min_thickness", "extra_perimeters", "extra_perimeters_on_overhangs", "avoid_crossing_curled_overhangs", "avoid_crossing_perimeters", "thin_walls", "overhangs", "seam_position","staggered_inner_seams", "external_perimeters_first", "fill_density", "fill_pattern", "top_fill_pattern", "bottom_fill_pattern", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 88ba446eba2..3e666d8d7ce 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -1,4 +1,5 @@ ///|/ Copyright (c) Prusa Research 2016 - 2023 Vojtěch Bubník @bubnikv, Lukáš Matěna @lukasmatena, Lukáš Hejl @hejllukas, Tomáš Mészáros @tamasmeszaros, Oleksandra Iushchenko @YuSanka, Pavel Mikuš @Godrak, David Kocík @kocikdav, Enrico Turri @enricoturri1966, Filip Sykala @Jony01, Vojtěch Král @vojtechkral +///|/ Copyright (c) OrcaSlicer 2023 Andrew Boktor @aboktor ///|/ Copyright (c) 2023 Pedro Lamas @PedroLamas ///|/ Copyright (c) 2023 Mimoja @Mimoja ///|/ Copyright (c) 2020 - 2021 Sergey Kovalev @RandoMan70 @@ -2608,6 +2609,25 @@ void PrintConfigDef::init_fff_params() "It won't work when printing more than one single object."); def->set_default_value(new ConfigOptionBool(false)); + def = this->add("spiral_vase_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_vase_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("standby_temperature_delta", coInt); def->label = L("Temperature variation"); // TRN PrintSettings : "Ooze prevention" > "Temperature variation" diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 5c9bd4ee3cc..6267c5d274b 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -857,6 +857,8 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE( ((ConfigOptionInts, slowdown_below_layer_time)) ((ConfigOptionFloat, solid_infill_acceleration)) ((ConfigOptionBool, spiral_vase)) + ((ConfigOptionBool, spiral_vase_smooth)) + ((ConfigOptionFloatOrPercent, spiral_vase_max_xy_smoothing)) ((ConfigOptionInt, standby_temperature_delta)) ((ConfigOptionInts, temperature)) ((ConfigOptionInt, threads)) diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index e6abeda550d..d4245961c77 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -237,6 +237,8 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig* config) toggle_field("infill_anchor", has_infill_anchors); bool has_spiral_vase = config->opt_bool("spiral_vase"); + toggle_field("spiral_vase_smooth", has_spiral_vase); + toggle_field("spiral_vase_max_xy_smoothing", config->opt_bool("spiral_vase_smooth")); bool has_top_solid_infill = config->opt_int("top_solid_layers") > 0; bool has_bottom_solid_infill = config->opt_int("bottom_solid_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 fe15f36aea3..c4ab58dd051 100644 --- a/src/slic3r/GUI/Tab.cpp +++ b/src/slic3r/GUI/Tab.cpp @@ -1445,6 +1445,8 @@ void TabPrint::build() optgroup = page->new_optgroup(L("Vertical shells")); optgroup->append_single_option_line("perimeters", category_path + "perimeters"); optgroup->append_single_option_line("spiral_vase", category_path + "spiral-vase"); + optgroup->append_single_option_line("spiral_vase_smooth", category_path + "spiral-vase#smooth"); + optgroup->append_single_option_line("spiral_vase_max_xy_smoothing", category_path + "spiral-vase#max-xy-smoothing"); Line line { "", "" }; line.full_width = 1;