SPE-2111: Rewrite the spiral vase with interpolation between layers.

- Adds transition out to prevent a sharp edge at the top of the spiral vase.
- Interpolation in the X and Y axes is used whether vase mode is enabled.
- Maximum smoothing is automatically calculated from the nozzle diameter.

This commit comes from https://github.com/prusa3d/PrusaSlicer/pull/12142 with some refactoring.

Co-authored-by: Andrew Boktor <aboktor@microsoft.com>
Co-authored-by: SoftFever <softfeverever@gmail.com>
Co-authored-by: Vovodroid <vovodroid@users.noreply.github.com>
Co-authored-by: Tom Glastonbury <t@tg73.net>
Co-authored-by: Lukáš Hejl <hejl.lukas@gmail.com>
This commit is contained in:
andrewboktor 2024-03-14 19:06:02 +01:00 committed by Lukas Matena
parent b1f5ef354b
commit 122d876bca
3 changed files with 109 additions and 50 deletions

View File

@ -1509,11 +1509,12 @@ void GCodeGenerator::process_layers(
}); });
// The pipeline is variable: The vase mode filter is optional. // The pipeline is variable: The vase mode filter is optional.
const auto spiral_vase = tbb::make_filter<LayerResult, LayerResult>(slic3r_tbb_filtermode::serial_in_order, const auto spiral_vase = tbb::make_filter<LayerResult, LayerResult>(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) if (in.nop_layer_result)
return in; return in;
spiral_vase->enable(in.spiral_vase_enable); 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<LayerResult, LayerResult>(slic3r_tbb_filtermode::serial_in_order, const auto pressure_equalizer = tbb::make_filter<LayerResult, LayerResult>(slic3r_tbb_filtermode::serial_in_order,
[pressure_equalizer = this->m_pressure_equalizer.get()](LayerResult in) -> LayerResult { [pressure_equalizer = this->m_pressure_equalizer.get()](LayerResult in) -> LayerResult {
@ -1602,11 +1603,12 @@ void GCodeGenerator::process_layers(
}); });
// The pipeline is variable: The vase mode filter is optional. // The pipeline is variable: The vase mode filter is optional.
const auto spiral_vase = tbb::make_filter<LayerResult, LayerResult>(slic3r_tbb_filtermode::serial_in_order, const auto spiral_vase = tbb::make_filter<LayerResult, LayerResult>(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) if (in.nop_layer_result)
return in; return in;
spiral_vase->enable(in.spiral_vase_enable); 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<LayerResult, LayerResult>(slic3r_tbb_filtermode::serial_in_order, const auto pressure_equalizer = tbb::make_filter<LayerResult, LayerResult>(slic3r_tbb_filtermode::serial_in_order,
[pressure_equalizer = this->m_pressure_equalizer.get()](LayerResult in) -> LayerResult { [pressure_equalizer = this->m_pressure_equalizer.get()](LayerResult in) -> LayerResult {

View File

@ -9,10 +9,21 @@
#include "SpiralVase.hpp" #include "SpiralVase.hpp"
#include "GCode.hpp" #include "GCode.hpp"
#include <sstream> #include <sstream>
#include <cmath>
#include <limits>
namespace Slic3r { namespace Slic3r {
std::string SpiralVase::process_layer(const std::string &gcode) static AABBTreeLines::LinesDistancer<Linef> get_layer_distancer(const std::vector<Vec2f> &layer_points)
{
Linesf lines;
for (size_t idx = 1; idx < layer_points.size(); ++idx)
lines.emplace_back(layer_points[idx - 1].cast<double>(), layer_points[idx].cast<double>());
return AABBTreeLines::LinesDistancer{std::move(lines)};
}
std::string SpiralVase::process_layer(const std::string &gcode, bool last_layer)
{ {
/* This post-processor relies on several assumptions: /* This post-processor relies on several assumptions:
- all layers are processed through it, including those that are not supposed - all layers are processed through it, including those that are not supposed
@ -30,8 +41,8 @@ std::string SpiralVase::process_layer(const std::string &gcode)
} }
// Get total XY length for this layer by summing all extrusion moves. // Get total XY length for this layer by summing all extrusion moves.
float total_layer_length = 0; float total_layer_length = 0.f;
float layer_height = 0; float layer_height = 0.f;
float z = 0.f; float z = 0.f;
{ {
@ -54,18 +65,24 @@ std::string SpiralVase::process_layer(const std::string &gcode)
}); });
} }
// Remove layer height from initial Z. // Remove layer height from initial Z.
z -= layer_height; z -= layer_height;
std::string new_gcode;
// FIXME Tapering of the transition layer only works reliably with relative extruder distances. // FIXME Tapering of the transition layer only works reliably with relative extruder distances.
// For absolute extruder distances it will be switched off. // For absolute extruder distances it will be switched off.
// Tapering the absolute extruder distances requires to process every extrusion value after the first transition // Tapering the absolute extruder distances requires to process every extrusion value after the first transition
// layer. // layer.
bool transition = m_transition_layer && m_config.use_relative_e_distances.value; const bool transition_in = m_transition_layer && m_config.use_relative_e_distances.value;
float layer_height_factor = layer_height / total_layer_length; const bool transition_out = last_layer && m_config.use_relative_e_distances.value;
const AABBTreeLines::LinesDistancer previous_layer_distancer = get_layer_distancer(m_previous_layer);
Vec2f last_point = m_previous_layer.empty() ? Vec2f::Zero() : m_previous_layer.back();
float len = 0.f; float len = 0.f;
m_reader.parse_buffer(gcode, [&new_gcode, &z, total_layer_length, layer_height_factor, transition, &len]
std::string new_gcode, transition_gcode;
std::vector<Vec2f> current_layer;
m_reader.parse_buffer(gcode, [z, total_layer_length, layer_height, transition_in, transition_out, smooth_spiral = m_smooth_spiral, max_xy_smoothing = m_max_xy_smoothing,
&len, &last_point, &new_gcode, &transition_gcode, &current_layer, &previous_layer_distancer]
(GCodeReader &reader, GCodeReader::GCodeLine line) { (GCodeReader &reader, GCodeReader::GCodeLine line) {
if (line.cmd_is("G1")) { if (line.cmd_is("G1")) {
if (line.has_z()) { if (line.has_z()) {
@ -74,32 +91,63 @@ std::string SpiralVase::process_layer(const std::string &gcode)
line.set(reader, Z, z); line.set(reader, Z, z);
new_gcode += line.raw() + '\n'; new_gcode += line.raw() + '\n';
return; return;
} else { } else if (line.has_x() || line.has_y()) { // Sometimes lines have X/Y but the move is to the last position.
float dist_XY = line.dist_XY(reader); if (const float dist_XY = line.dist_XY(reader); dist_XY > 0 && line.extruding(reader)) { // Exclude wipe and retract
if (dist_XY > 0) {
// horizontal move
if (line.extruding(reader)) {
len += dist_XY; len += dist_XY;
line.set(reader, Z, z + len * layer_height_factor); const float factor = len / total_layer_length;
if (transition && line.has(E)) if (transition_in)
// Transition layer, modulate the amount of extrusion from zero to the final value. // Transition layer, interpolate the amount of extrusion from zero to the final value.
line.set(reader, E, line.value(E) * len / total_layer_length); line.set(reader, E, line.e() * factor, 5);
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 transition_line(line);
transition_line.set(reader, E, line.e() * (1.f - factor), 5);
transition_gcode += transition_line.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
Vec2f p(line.x(), line.y()); // Get current x/y coordinates
current_layer.emplace_back(p); // Store that point for later use on the next layer
auto [nearest_distance, idx, nearest_pt] = previous_layer_distancer.distance_from_lines_extra<false>(p.cast<double>());
if (nearest_distance < max_xy_smoothing) {
// Interpolate between the point on this layer and the point on the previous layer
Vec2f target = nearest_pt.cast<float>() * (1.f - factor) + 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 = (last_point - target).norm();
// Scale the extrusion amount according to change in length
line.set(reader, E, line.e() * modified_dist_XY / dist_XY, 5);
last_point = target;
} else {
last_point = p;
}
}
new_gcode += line.raw() + '\n'; new_gcode += line.raw() + '\n';
} }
return; return;
/* Skip travel moves: the move to first perimeter point will /* Skip travel moves: the move to first perimeter point will
cause a visible seam when loops are not aligned in XY; by skipping 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 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 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'; new_gcode += line.raw() + '\n';
if (transition_out)
transition_gcode += line.raw() + '\n';
}); });
return new_gcode; m_previous_layer = std::move(current_layer);
return new_gcode + transition_gcode;
} }
} }

View File

@ -14,31 +14,40 @@
namespace Slic3r { namespace Slic3r {
class SpiralVase
class SpiralVase { {
public: public:
SpiralVase(const PrintConfig &config) : m_config(config) SpiralVase() = delete;
explicit 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_reader.apply_config(m_config);
const double max_nozzle_diameter = *std::max_element(config.nozzle_diameter.values.begin(), config.nozzle_diameter.values.end());
m_max_xy_smoothing = float(2. * max_nozzle_diameter);
}; };
void enable(bool en) { void enable(bool enable)
m_transition_layer = en && ! m_enabled; {
m_enabled = en; m_transition_layer = enable && !m_enabled;
m_enabled = enable;
} }
std::string process_layer(const std::string &gcode); std::string process_layer(const std::string &gcode, bool last_layer);
private: private:
const PrintConfig &m_config; const PrintConfig &m_config;
GCodeReader m_reader; 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. // 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 = true;
std::vector<Vec2f> m_previous_layer;
}; };
} }
#endif // slic3r_SpiralVase_hpp_ #endif // slic3r_SpiralVase_hpp_