SPE-2301: Add a new option that makes a printer print loops clockwise instead of counterclockwise.

This commit is contained in:
Lukáš Hejl 2024-05-16 10:05:34 +02:00 committed by Lukas Matena
parent c2eac9502f
commit d6136a34e3
14 changed files with 92 additions and 44 deletions

View File

@ -508,13 +508,14 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
// apply half spacing using this flow's own spacing and generate infill
FillParams params;
params.density = float(0.01 * surface_fill.params.density);
params.dont_adjust = false; // surface_fill.params.dont_adjust;
params.anchor_length = surface_fill.params.anchor_length;
params.anchor_length_max = surface_fill.params.anchor_length_max;
params.resolution = resolution;
params.use_arachne = (perimeter_generator == PerimeterGeneratorType::Arachne && surface_fill.params.pattern == ipConcentric) || surface_fill.params.pattern == ipEnsuring;
params.layer_height = layerm.layer()->height;
params.density = float(0.01 * surface_fill.params.density);
params.dont_adjust = false; // surface_fill.params.dont_adjust;
params.anchor_length = surface_fill.params.anchor_length;
params.anchor_length_max = surface_fill.params.anchor_length_max;
params.resolution = resolution;
params.use_arachne = (perimeter_generator == PerimeterGeneratorType::Arachne && surface_fill.params.pattern == ipConcentric) || surface_fill.params.pattern == ipEnsuring;
params.layer_height = layerm.layer()->height;
params.prefer_clockwise_movements = this->object()->print()->config().prefer_clockwise_movements;
for (ExPolygon &expoly : surface_fill.expolygons) {
// Spacing is modified by the filler to indicate adjustments. Reset it for each expolygon.
@ -569,11 +570,12 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
thick_polylines.clear();
} else {
// When prefer_clockwise_movements is true, we have to ensure that extrusion paths will not be reversed during path planning.
extrusion_entities_append_paths(
eec->entities, std::move(polylines),
ExtrusionAttributes{ surface_fill.params.extrusion_role,
ExtrusionFlow{ flow_mm3_per_mm, float(flow_width), surface_fill.params.flow.height() }
});
}, !params.prefer_clockwise_movements);
layerm.m_fills.entities.push_back(eec);
}
insert_fills_into_islands(*this, uint32_t(surface_fill.region_id), fill_begin, uint32_t(layerm.fills().size()));

View File

@ -72,6 +72,9 @@ struct FillParams
bool use_arachne { false };
// Layer height for Concentric infill with Arachne.
coordf_t layer_height { 0.f };
// For infills that produce closed loops to force printing those loops clockwise.
bool prefer_clockwise_movements { false };
};
static_assert(IsTriviallyCopyable<FillParams>::value, "FillParams class is not POD (and it should be - see constructor).");

View File

@ -56,14 +56,19 @@ void FillConcentric::_fill_surface_single(
// clip the paths to prevent the extruder from getting exactly on the first point of the loop
// Keep valid paths only.
size_t j = iPathFirst;
for (size_t i = iPathFirst; i < polylines_out.size(); ++ i) {
for (size_t i = iPathFirst; i < polylines_out.size(); ++i) {
polylines_out[i].clip_end(this->loop_clipping);
if (polylines_out[i].is_valid()) {
if (params.prefer_clockwise_movements)
polylines_out[i].reverse();
if (j < i)
polylines_out[j] = std::move(polylines_out[i]);
++ j;
++j;
}
}
if (j < polylines_out.size())
polylines_out.erase(polylines_out.begin() + int(j), polylines_out.end());
//TODO: return ExtrusionLoop objects to get better chained paths,
@ -94,6 +99,7 @@ void FillConcentric::_fill_surface_single(const FillParams &params,
for (Arachne::VariableWidthLines &loop : loops) {
if (loop.empty())
continue;
for (const Arachne::ExtrusionLine &wall : loop)
all_extrusions.emplace_back(&wall);
}
@ -106,8 +112,14 @@ void FillConcentric::_fill_surface_single(const FillParams &params,
continue;
ThickPolyline thick_polyline = Arachne::to_thick_polyline(*extrusion);
if (extrusion->is_closed)
if (extrusion->is_closed) {
// Arachne produces contour with clockwise orientation and holes with counterclockwise orientation.
if (const bool extrusion_reverse = params.prefer_clockwise_movements ? !extrusion->is_contour() : extrusion->is_contour(); extrusion_reverse)
thick_polyline.reverse();
thick_polyline.start_at_index(nearest_point_index(thick_polyline.points, last_pos));
}
thick_polylines_out.emplace_back(std::move(thick_polyline));
last_pos = thick_polylines_out.back().last_point();
}

View File

@ -308,19 +308,25 @@ ThickPolylines make_fill_polylines(
for (Arachne::VariableWidthLines &loop : loops) {
if (loop.empty())
continue;
for (const Arachne::ExtrusionLine &wall : loop)
all_extrusions.emplace_back(&wall);
}
for (const Arachne::ExtrusionLine *extrusion : all_extrusions) {
if (extrusion->junctions.size() < 2) {
if (extrusion->junctions.size() < 2)
continue;
}
ThickPolyline thick_polyline = Arachne::to_thick_polyline(*extrusion);
if (extrusion->is_closed) {
// Arachne produces contour with clockwise orientation and holes with counterclockwise orientation.
if (const bool extrusion_reverse = params.prefer_clockwise_movements ? !extrusion->is_contour() : extrusion->is_contour(); extrusion_reverse)
thick_polyline.reverse();
thick_polyline.start_at_index(nearest_point_index(thick_polyline.points, ex_bb.min));
thick_polyline.clip_end(scaled_spacing * 0.5);
}
if (thick_polyline.is_valid() && thick_polyline.length() > 0 && thick_polyline.points.size() > 1) {
thick_polylines.push_back(thick_polyline);
}

View File

@ -2977,8 +2977,10 @@ static constexpr const double min_gcode_segment_length = 0.002;
std::string GCodeGenerator::extrude_loop(const ExtrusionLoop &loop_src, const GCode::SmoothPathCache &smooth_path_cache, const std::string_view description, double speed)
{
// Extrude all loops CCW.
bool is_hole = loop_src.is_clockwise();
// Extrude all loops CCW unless CW movements are prefered.
const bool is_hole = loop_src.is_clockwise();
const bool reverse_loop = m_config.prefer_clockwise_movements ? !is_hole : is_hole;
Point seam_point = this->last_position.has_value() ? *this->last_position : Point::Zero();
if (!m_config.spiral_vase && comment_is_perimeter(description)) {
assert(m_layer != nullptr);
@ -2986,8 +2988,7 @@ std::string GCodeGenerator::extrude_loop(const ExtrusionLoop &loop_src, const GC
}
// Because the G-code export has 1um resolution, don't generate segments shorter than 1.5 microns,
// thus empty path segments will not be produced by G-code export.
GCode::SmoothPath smooth_path = smooth_path_cache.resolve_or_fit_split_with_seam(
loop_src, is_hole, m_scaled_resolution, seam_point, scaled<double>(0.0015));
GCode::SmoothPath smooth_path = smooth_path_cache.resolve_or_fit_split_with_seam(loop_src, reverse_loop, m_scaled_resolution, seam_point, scaled<double>(0.0015));
// Clip the path to avoid the extruder to get exactly on the first point of the loop;
// if polyline was shorter than the clipping distance we'd get a null polyline, so
@ -3018,7 +3019,7 @@ std::string GCodeGenerator::extrude_loop(const ExtrusionLoop &loop_src, const GC
} else if (loop_src.paths.back().role().is_external_perimeter() && m_layer != nullptr && m_config.perimeters.value > 1) {
// Only wipe inside if the wipe along the perimeter is disabled.
// Make a little move inwards before leaving loop.
if (std::optional<Point> pt = wipe_hide_seam(smooth_path, is_hole, scale_(EXTRUDER_CONFIG(nozzle_diameter))); pt) {
if (std::optional<Point> pt = wipe_hide_seam(smooth_path, reverse_loop, scale_(EXTRUDER_CONFIG(nozzle_diameter))); pt) {
// Generate the seam hiding travel move.
gcode += m_writer.travel_to_xy(this->point_to_gcode(*pt), "move inwards before travel");
this->last_position = *pt;
@ -3033,9 +3034,10 @@ std::string GCodeGenerator::extrude_skirt(
const GCode::SmoothPathCache &smooth_path_cache, const std::string_view description, double speed)
{
assert(loop_src.is_counter_clockwise());
const bool reverse_loop = m_config.prefer_clockwise_movements;
Point seam_point = this->last_position.has_value() ? *this->last_position : Point::Zero();
GCode::SmoothPath smooth_path = smooth_path_cache.resolve_or_fit_split_with_seam(
loop_src, false, m_scaled_resolution, seam_point, scaled<double>(0.0015));
GCode::SmoothPath smooth_path = smooth_path_cache.resolve_or_fit_split_with_seam(loop_src, reverse_loop, m_scaled_resolution, seam_point, scaled<double>(0.0015));
// Clip the path to avoid the extruder to get exactly on the first point of the loop;
// if polyline was shorter than the clipping distance we'd get a null polyline, so

View File

@ -187,7 +187,7 @@ std::string Wipe::wipe(GCodeGenerator &gcodegen, bool toolchange)
// Make a little move inwards before leaving loop after path was extruded,
// thus the current extruder position is at the end of a path and the path
// may not be closed in case the loop was clipped to hide a seam.
std::optional<Point> wipe_hide_seam(const SmoothPath &path, bool is_hole, double wipe_length)
std::optional<Point> wipe_hide_seam(const SmoothPath &path, const bool path_reversed, const double wipe_length)
{
assert(! path.empty());
assert(path.front().path.size() >= 2);
@ -222,7 +222,7 @@ std::optional<Point> wipe_hide_seam(const SmoothPath &path, bool is_hole, double
double angle_inside = angle(p_next - p_current, p_prev - p_current);
assert(angle_inside >= -M_PI && angle_inside <= M_PI);
// 3rd of this angle will be taken, thus make the angle monotonic before interpolation.
if (is_hole) {
if (path_reversed) {
if (angle_inside > 0)
angle_inside -= 2.0 * M_PI;
} else {

View File

@ -506,7 +506,7 @@ static std::vector<std::string> s_Preset_machine_limits_options {
static std::vector<std::string> s_Preset_printer_options {
"printer_technology", "autoemit_temperature_commands",
"bed_shape", "bed_custom_texture", "bed_custom_model", "binary_gcode", "z_offset", "gcode_flavor", "use_relative_e_distances",
"use_firmware_retraction", "use_volumetric_e", "variable_layer_height",
"use_firmware_retraction", "use_volumetric_e", "variable_layer_height", "prefer_clockwise_movements",
//FIXME the print host keys are left here just for conversion from the Printer preset to Physical Printer preset.
"host_type", "print_host", "printhost_apikey", "printhost_cafile",
"single_extruder_multi_material", "start_gcode", "end_gcode", "before_layer_gcode", "layer_gcode", "toolchange_gcode",

View File

@ -211,7 +211,8 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
// Therefore toggling the Spiral Vase on / off requires complete reslicing.
|| opt_key == "spiral_vase"
|| opt_key == "filament_shrinkage_compensation_xy"
|| opt_key == "filament_shrinkage_compensation_z") {
|| opt_key == "filament_shrinkage_compensation_z"
|| opt_key == "prefer_clockwise_movements") {
osteps.emplace_back(posSlice);
} else if (
opt_key == "complete_objects"

View File

@ -3366,6 +3366,13 @@ void PrintConfigDef::init_fff_params()
def->mode = comExpert;
def->set_default_value(new ConfigOptionBool(true));
def = this->add("prefer_clockwise_movements", coBool);
def->label = L("Prefer clockwise movements");
def->tooltip = L("This setting makes the printer print loops clockwise instead of counterclockwise. "
"Most printers don't need to print loops clockwise.");
def->mode = comExpert;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("wipe", coBools);
def->label = L("Wipe while retracting");
def->tooltip = L("This flag will move the nozzle while retracting to minimize the possible blob "

View File

@ -864,6 +864,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionString, output_filename_format))
((ConfigOptionFloat, perimeter_acceleration))
((ConfigOptionStrings, post_process))
((ConfigOptionBool, prefer_clockwise_movements))
((ConfigOptionString, printer_model))
((ConfigOptionString, printer_notes))
((ConfigOptionFloat, resolution))

View File

@ -519,18 +519,19 @@ static inline void fill_expolygons_generate_paths(
fill_expolygons_generate_paths(dst, std::move(expolygons), filler, fill_params, density, role, flow);
}
static Polylines draw_perimeters(const ExPolygon &expoly, double clip_length)
{
static Polylines draw_perimeters(const ExPolygon &expoly, double clip_length, const bool prefer_clockwise_movements) {
// Draw the perimeters.
Polylines polylines;
polylines.reserve(expoly.holes.size() + 1);
for (size_t i = 0; i <= expoly.holes.size(); ++ i) {
Polyline pl(i == 0 ? expoly.contour.points : expoly.holes[i - 1].points);
pl.points.emplace_back(pl.points.front());
if (i > 0)
// It is a hole, reverse it.
// When prefer_clockwise_movements is true ensure that all loops are CW oriented (reverse contour),
// otherwise ensure that all loops are CCW oriented (reverse loops).
if (const bool loop_reverse = prefer_clockwise_movements ? i == 0 : i > 0; loop_reverse)
pl.reverse();
// so that all contours are CCW oriented.
pl.clip_end(clip_length);
polylines.emplace_back(std::move(pl));
}
@ -647,7 +648,7 @@ static inline void tree_supports_generate_paths(
ExPolygons level2 = offset2_ex({ expoly }, -1.5 * flow.scaled_width(), 0.5 * flow.scaled_width());
if (level2.size() == 1) {
Polylines polylines;
extrusion_entities_append_paths(eec->entities, draw_perimeters(expoly, clip_length), { ExtrusionRole::SupportMaterial, flow },
extrusion_entities_append_paths(eec->entities, draw_perimeters(expoly, clip_length, support_params.prefer_clockwise_movements), { ExtrusionRole::SupportMaterial, flow },
// Disable reversal of the path, always start with the anchor, always print CCW.
false);
expoly = level2.front();
@ -659,10 +660,14 @@ static inline void tree_supports_generate_paths(
// The anchor candidate points are annotated with an index of the source contour or with -1 if on intersection.
anchor_candidates.clear();
shrink_expolygon_with_contour_idx(expoly, flow.scaled_width(), DefaultJoinType, 1.2, anchor_candidates);
// Orient all contours CW.
for (auto &path : anchor_candidates)
if (ClipperLib_Z::Area(path) > 0)
// Based on the way how loops with anchors are constructed (we reverse orientation at the end),
// orient all loops CCW when prefer_clockwise_movements is true and orient all loops CW otherwise.
for (auto &path : anchor_candidates) {
const bool is_ccw = ClipperLib_Z::Area(path) > 0;
if (const bool path_reverse = support_params.prefer_clockwise_movements ? !is_ccw : is_ccw; path_reverse)
std::reverse(path.begin(), path.end());
}
// Draw the perimeters.
Polylines polylines;
@ -671,10 +676,12 @@ static inline void tree_supports_generate_paths(
// Open the loop with a seam.
const Polygon &loop = expoly.contour_or_hole(idx_loop);
Polyline pl(loop.points);
// Orient all contours CW, because the anchor will be added to the end of polyline while we want to start a loop with the anchor.
if (idx_loop == 0)
// It is an outer contour.
// When prefer_clockwise_movements is true, orient all loops CCW and orient all loops CW otherwise
// because the anchor will be added to the end of the polyline while we want to start a loop with the anchor.
if (const bool loop_reverse = support_params.prefer_clockwise_movements ? idx_loop != 0 : idx_loop == 0; loop_reverse)
pl.reverse();
pl.points.emplace_back(pl.points.front());
pl.clip_end(clip_length);
if (pl.size() < 2)
@ -767,11 +774,12 @@ static inline void fill_expolygons_with_sheath_generate_paths(
ExtrusionEntitiesPtr &dst,
const Polygons &polygons,
Fill *filler,
float density,
ExtrusionRole role,
const float density,
const ExtrusionRole role,
const Flow &flow,
bool with_sheath,
bool no_sort)
const bool with_sheath,
const bool no_sort,
const bool prefer_clockwise_movements)
{
if (polygons.empty())
return;
@ -797,7 +805,7 @@ static inline void fill_expolygons_with_sheath_generate_paths(
eec->no_sort = true;
}
ExtrusionEntitiesPtr &out = no_sort ? eec->entities : dst;
extrusion_entities_append_paths(out, draw_perimeters(expoly, clip_length), { ExtrusionRole::SupportMaterial, flow });
extrusion_entities_append_paths(out, draw_perimeters(expoly, clip_length, prefer_clockwise_movements), { ExtrusionRole::SupportMaterial, flow }, false);
// Fill in the rest.
fill_expolygons_generate_paths(out, offset_ex(expoly, float(-0.4 * spacing)), filler, fill_params, density, role, flow);
if (no_sort && ! eec->empty())
@ -1526,7 +1534,7 @@ void generate_support_toolpaths(
filler, float(support_params.support_density),
// Extrusion parameters
ExtrusionRole::SupportMaterial, flow,
support_params.with_sheath, false);
support_params.with_sheath, false, support_params.prefer_clockwise_movements);
}
if (! tree_polygons.empty())
tree_supports_generate_paths(support_layer.support_fills.entities, tree_polygons, flow, support_params);
@ -1561,7 +1569,7 @@ void generate_support_toolpaths(
// Extrusion parameters
(support_layer_id < slicing_params.base_raft_layers) ? ExtrusionRole::SupportMaterial : ExtrusionRole::SupportMaterialInterface, flow,
// sheath at first layer
support_layer_id == 0, support_layer_id == 0);
support_layer_id == 0, support_layer_id == 0, support_params.prefer_clockwise_movements);
}
});
@ -1802,7 +1810,7 @@ void generate_support_toolpaths(
filler, density,
// Extrusion parameters
ExtrusionRole::SupportMaterial, flow,
sheath, no_sort);
sheath, no_sort, support_params.prefer_clockwise_movements);
}
// Merge base_interface_layers to base_layers to avoid unneccessary retractions

View File

@ -143,6 +143,8 @@ SupportParameters::SupportParameters(const PrintObject &object)
}
this->tree_branch_diameter_double_wall_area_scaled = 0.25 * sqr(scaled<double>(object_config.support_tree_branch_diameter_double_wall.value)) * M_PI;
this->prefer_clockwise_movements = print_config.prefer_clockwise_movements;
}
} // namespace Slic3r

View File

@ -88,6 +88,9 @@ struct SupportParameters {
float raft_angle_base;
float raft_angle_interface;
// Print closed loop clockwise when it is equal to true.
bool prefer_clockwise_movements;
// Produce a raft interface angle for a given SupportLayer::interface_id()
float raft_interface_angle(size_t interface_id) const
{ return this->raft_angle_interface + ((interface_id & 1) ? float(- M_PI / 4.) : float(+ M_PI / 4.)); }

View File

@ -2785,6 +2785,7 @@ void TabPrinter::build_fff()
optgroup->append_single_option_line("use_firmware_retraction");
optgroup->append_single_option_line("use_volumetric_e");
optgroup->append_single_option_line("variable_layer_height");
optgroup->append_single_option_line("prefer_clockwise_movements");
const int gcode_field_height = 15; // 150
const int notes_field_height = 25; // 250