Merge remote-tracking branch 'upstream/main' into haryr/aug25-rebase

# Conflicts:
#	resources/profiles/Custom.json
#	src/libslic3r/Brim.cpp
#	src/libslic3r/GCode.cpp
#	src/libslic3r/GCode.hpp
#	src/libslic3r/Preset.cpp
#	src/slic3r/GUI/3DScene.cpp
#	src/slic3r/GUI/ConfigManipulation.cpp
#	src/slic3r/GUI/GLCanvas3D.cpp
#	src/slic3r/GUI/Plater.cpp
This commit is contained in:
harrierpigeon
2026-08-25 06:50:46 -05:00
861 changed files with 39688 additions and 7012 deletions
+111 -71
View File
@@ -771,30 +771,31 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
return changes;
}
// Clearance the tower-approach router keeps around the tower: the avoid box is
// inflated by this much before routing, and the inflated corners must stay on the
// bed for a route to be generated at all.
static constexpr float wipe_tower_routing_clearance = 2.f;
// BBS
// start_pos refers to the last position before the wipe_tower.
// end_pos refers to the wipe tower's start_pos.
// using the print coordinate system
Polyline WipeTowerIntegration::generate_path_to_wipe_tower(const Point& start_pos,const Point &end_pos , const BoundingBox& avoid_polygon , const BoundingBox& printer_bbx) const
Polyline WipeTowerIntegration::generate_path_to_wipe_tower(const Point& start_pos,const Point &end_pos , const BoundingBox& avoid_polygon , const Polygons& bed_polygons) const
{
Polyline res;
coord_t alpha = scaled(2.f); // offset distance
coord_t alpha = scaled(wipe_tower_routing_clearance); // offset distance
BoundingBox avoid_polygon_inner = avoid_polygon;
avoid_polygon_inner.offset(alpha);
coord_t width = avoid_polygon_inner.max[0] - avoid_polygon_inner.min[0];
Polygon bed_polygon = printer_bbx.polygon();
Vec2f v(1, 0); // the first print direction of end_pos.
if (abs(end_pos[0] - avoid_polygon_inner.min[0]) < width / 2) v = -v; // judge whether the wipe tower's infill goes to the left or right.
// Judge whether the avoid_polygon_inner is outside the printer_bbx.
// Judge whether the avoid_polygon_inner is outside the bed. The real printable
// outline is tested (not its bounding box), so on circular/custom beds corners
// hanging off the bed are rejected.
// If so, do nothing and just go directly to the end_pos.
bool is_bbx_in_bed = true;
Points avoid_points = avoid_polygon_inner.polygon().points;
for (auto &wipe_tower_bbx_p : avoid_points) {
if (ClipperLib::PointInPolygon(wipe_tower_bbx_p, bed_polygon.points) != 1) {
is_bbx_in_bed = false;
break;
}
}
const bool is_bbx_in_bed = std::all_of(avoid_points.begin(), avoid_points.end(),
[&bed_polygons](const Point &pt) { return contains(bed_polygons, pt, /*border_result=*/false); });
if (!is_bbx_in_bed) {
res.points.push_back(end_pos);
return res;
@@ -901,27 +902,17 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
return Eigen::Rotation2Df(alpha) * (pt + m_rib_offset) + m_wipe_tower_pos;
}
// Printable-area bounds for tower-approach routing, in object coordinates (shared by
// the BBL avoid-perimeter path in append_tcr and the Type2 skip-points router).
// Multi-nozzle: clamp the travel bounds to the region every extruder can reach
// (get_extruder_shared_printable_polygon) instead of the full bed. Gated on the
// multi-nozzle predicate so every existing single/dual printer keeps the historic
// full-printable_area routing byte-identical.
BoundingBox WipeTowerIntegration::printer_travel_bounds(GCode &gcodegen) const
// Bed outline the tower-approach router plans against, in object coordinates. The real
// outline is returned, not its bounding box, so the router's containment tests fail off
// the bed on circular/custom shapes; the multi-nozzle narrowing lives in the accessor.
Polygons WipeTowerIntegration::shared_printable_area(GCode &gcodegen) const
{
const Vec2f plate_origin_2d(m_plate_origin(0), m_plate_origin(1));
BoundingBox printer_bbx;
if (is_multi_nozzle_printer(gcodegen.m_config)) {
printer_bbx = get_extents(gcodegen.m_print->get_extruder_shared_printable_polygon());
printer_bbx.min = wipe_tower_point_to_object_point(gcodegen, unscaled<float>(printer_bbx.min) + plate_origin_2d);
printer_bbx.max = wipe_tower_point_to_object_point(gcodegen, unscaled<float>(printer_bbx.max) + plate_origin_2d);
} else {
Points bed_points;
for (const auto& p : gcodegen.m_config.printable_area.values)
bed_points.push_back(wipe_tower_point_to_object_point(gcodegen, p.cast<float>() + plate_origin_2d));
printer_bbx = BoundingBox(bed_points);
}
return printer_bbx;
// The frame change is a pure translation, so transform the origin once.
const Point offset = wipe_tower_point_to_object_point(gcodegen, Vec2f(m_plate_origin(0), m_plate_origin(1)));
Polygons bed_polygons = gcodegen.m_print->get_extruder_shared_printable_polygon();
for (Polygon &poly : bed_polygons)
poly.translate(offset);
return bed_polygons;
}
// With skip points enabled the Type2 tower wall has an opening at each toolchange's
@@ -936,15 +927,37 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
if (!WipeTower2::use_gap_wall(gcodegen.m_config))
return {};
const Vec2f plate_origin_2d(m_plate_origin(0), m_plate_origin(1));
// Transform the tower-local bbx corners exactly like the tcr points; a rotated
// tower gets a conservative axis-aligned envelope.
Polygon avoid_points = scaled(m_wipe_tower_bbx).polygon();
for (auto& p : avoid_points.points)
p = wipe_tower_point_to_object_point(gcodegen, transform_wt2_pt(unscale(p).cast<float>()) + plate_origin_2d);
BoundingBox avoid_bbx(avoid_points.points);
if (avoid_bbx.contains(route_start))
// Transform tower-local corners exactly like the tcr points; a rotated tower gets a
// conservative axis-aligned envelope from the result.
auto tower_polygon = [&](const BoundingBoxf &bbx) {
Polygon poly = scaled(bbx).polygon();
for (Point &p : poly.points)
p = wipe_tower_point_to_object_point(gcodegen, transform_wt2_pt(unscale(p).cast<float>()) + plate_origin_2d);
return poly;
};
// The avoid envelope covers the first-layer brim (and rib flare), which a travel may
// cross freely: early-out only when the approach already starts over the tower body
// itself, so a start between the wall and the brim edge still gets routed in through
// the wall opening. Test the rotated polygon, not its bounding box — at angles off the
// axes the box's corner triangles cover most of the brim ring.
const float body_width = gcodegen.m_config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib ? m_wipe_tower_depth : m_right;
if (tower_polygon(BoundingBoxf(Vec2d(0., 0.), Vec2d(body_width, m_wipe_tower_depth))).contains(route_start))
return {};
Polyline travel_polyline = generate_path_to_wipe_tower(route_start, start_wipe_pos, avoid_bbx, printer_travel_bounds(gcodegen));
const Polygons bed = shared_printable_area(gcodegen);
BoundingBox avoid_bbx = get_extents(tower_polygon(m_wipe_tower_bbx));
// The inflated corners must stay on the bed for the router to generate a route at all:
// clamp the box against the bed shrunk by the clearance the router adds, so a tower
// parked near the bed edge is still routed along the clamped side instead of always
// travelling straight across the tower.
BoundingBox clamp_bbx = get_extents(bed);
clamp_bbx.offset(-(scaled(wipe_tower_routing_clearance) + SCALED_EPSILON));
avoid_bbx.min = avoid_bbx.min.cwiseMax(clamp_bbx.min);
avoid_bbx.max = avoid_bbx.max.cwiseMin(clamp_bbx.max);
if (avoid_bbx.min.x() >= avoid_bbx.max.x() || avoid_bbx.min.y() >= avoid_bbx.max.y())
return {};
Polyline travel_polyline = generate_path_to_wipe_tower(route_start, start_wipe_pos, avoid_bbx, bed);
std::string gcode;
// The polyline's last point is start_wipe_pos itself — emitted by the caller.
for (size_t i = 0; i + 1 < travel_polyline.points.size(); ++i)
@@ -1140,8 +1153,8 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
float old_retract_length = (old_filament_id != -1) ? full_config.retraction_length.get_at(old_fi) : 0;
float new_retract_length = full_config.retraction_length.get_at(new_fi);
float old_retract_length_toolchange = (old_filament_id != -1) ? full_config.retract_length_toolchange.get_at(old_filament_id) : 0;
float new_retract_length_toolchange = full_config.retract_length_toolchange.get_at(new_filament_id);
float old_retract_length_toolchange = (old_filament_id != -1) ? full_config.retract_length_toolchange.get_at(old_fi) : 0;
float new_retract_length_toolchange = full_config.retract_length_toolchange.get_at(new_fi);
int old_filament_temp = (old_filament_id != -1) ? (gcodegen.on_first_layer()? full_config.nozzle_temperature_initial_layer.get_at(old_fi) : full_config.nozzle_temperature.get_at(old_fi)) : 210;
int new_filament_temp = gcodegen.on_first_layer() ? full_config.nozzle_temperature_initial_layer.get_at(new_fi) : full_config.nozzle_temperature.get_at(new_fi);
Vec3d nozzle_pos = gcode_writer.get_position();
@@ -1325,7 +1338,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
Vec2f gcode_last_pos2d{gcode_last_pos[0], gcode_last_pos[1]};
Point gcode_last_pos2d_object = gcodegen.gcode_to_point(gcode_last_pos2d.cast<double>() + plate_origin_2d.cast<double>());
Point start_wipe_pos = wipe_tower_point_to_object_point(gcodegen, tool_change_start_pos + plate_origin_2d);
BoundingBox avoid_bbx, printer_bbx = printer_travel_bounds(gcodegen);
BoundingBox avoid_bbx;
{
// set avoid_bbx
avoid_bbx = scaled(m_wipe_tower_bbx);
@@ -1337,7 +1350,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
avoid_bbx = BoundingBox(avoid_points.points);
}
std::string travel_to_wipe_tower_gcode;
Polyline travel_polyline = generate_path_to_wipe_tower(gcode_last_pos2d_object, start_wipe_pos, avoid_bbx, printer_bbx);
Polyline travel_polyline = generate_path_to_wipe_tower(gcode_last_pos2d_object, start_wipe_pos, avoid_bbx, shared_printable_area(gcodegen));
for (size_t i = 0; i < travel_polyline.points.size(); ++i) {
const auto &p = travel_polyline.points[i];
@@ -1557,7 +1570,8 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
interface_temp = gcodegen.config().nozzle_temperature_range_high.get_at(new_extruder_id);
toolchange_temp_override = interface_temp;
}
toolchange_gcode_str = gcodegen.set_extruder(new_extruder_id, tcr.print_z, false, toolchange_temp_override); // TODO: toolchange_z vs print_z
toolchange_gcode_str = gcodegen.set_extruder(new_extruder_id, tcr.print_z, false, toolchange_temp_override,
WipeTower2::wait_for_temp_enabled(gcodegen.m_config)); // TODO: toolchange_z vs print_z
if (!travel_to_tower_now && !tcr.priming && WipeTower2::use_gap_wall(gcodegen.m_config)) {
// The tool changed in place (multi-tool printer without ramming), so the
// tower entry is the tcr's own positioning move — a straight line across
@@ -1708,7 +1722,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
std::string trimmed = line;
trimmed.erase(0, trimmed.find_first_not_of(" \t"));
bool skip_line = false;
if (boost::starts_with(trimmed, "M109")) {
if (boost::starts_with(trimmed, "M109") && trimmed.find(WipeTower2::wait_for_temp_tag()) == std::string::npos) {
bool matches_extruder = true;
if (trimmed.find('T') != std::string::npos)
matches_extruder = trimmed.find(t_token) != std::string::npos;
@@ -2932,6 +2946,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
DoExport::init_gcode_processor(print.config(), m_processor, m_silent_time_estimator_enabled,
print.get_layered_nozzle_group_result());
const bool is_bbl_printers = print.is_BBL_printer();
const bool skip_config_block = print.config().gcode_skip_config_block;
const WipeTowerType wipe_tower_type = print.wipe_tower_type();
m_calib_config.clear();
// resets analyzer's tracking data
@@ -3144,7 +3159,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// as configuration key / value pairs to be parsable by older versions of
// PrusaSlicer G-code viewer.
{
if (is_bbl_printers) {
if (is_bbl_printers && !skip_config_block) {
file.write("; CONFIG_BLOCK_START\n");
std::string full_config;
append_full_config(print, full_config);
@@ -4182,23 +4197,25 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder)
.c_str());
file.write("\n");
file.write("; CONFIG_BLOCK_START\n");
std::string full_config;
append_full_config(print, full_config);
if (!full_config.empty())
file.write(full_config);
if (!skip_config_block) {
file.write("; CONFIG_BLOCK_START\n");
std::string full_config;
append_full_config(print, full_config);
if (!full_config.empty())
file.write(full_config);
// SoftFever: write compatiple info
int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type);
file.write_format("; first_layer_bed_temperature = %d\n", first_layer_bed_temperature);
file.write_format("; bed_shape = %s\n", print.full_print_config().opt_serialize("printable_area").c_str());
file.write_format("; first_layer_temperature = %d\n", print.config().nozzle_temperature_initial_layer.get_at(0));
file.write_format("; first_layer_height = %.3f\n", print.config().initial_layer_print_height.value);
//SF TODO
// file.write_format("; variable_layer_height = %d\n", print.ad.adaptive_layer_height ? 1 : 0);
file.write("; CONFIG_BLOCK_END\n\n");
// SoftFever: write compatiple info
int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type);
file.write_format("; first_layer_bed_temperature = %d\n", first_layer_bed_temperature);
file.write_format("; bed_shape = %s\n", print.full_print_config().opt_serialize("printable_area").c_str());
file.write_format("; first_layer_temperature = %d\n", print.config().nozzle_temperature_initial_layer.get_at(0));
file.write_format("; first_layer_height = %.3f\n", print.config().initial_layer_print_height.value);
//SF TODO
// file.write_format("; variable_layer_height = %d\n", print.ad.adaptive_layer_height ? 1 : 0);
file.write("; CONFIG_BLOCK_END\n\n");
} // !skip_config_block
}
file.write("\n");
@@ -5781,7 +5798,7 @@ LayerResult GCode::process_layer(
// add tag for processor
gcode += ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Layer_Change) + "\n";
// export layer z
char buf[64];
char buf[80];
sprintf(buf, print.is_BBL_printer() ? "; Z_HEIGHT: %g\n" : ";Z:%g\n", print_z);
gcode += buf;
// export layer height
@@ -7146,6 +7163,7 @@ void GCode::append_full_config(const Print &print, std::string &str)
"farthest_point_timelapse"sv,
"compatible_printers"sv,
"compatible_prints"sv,
"filament_colour_type"sv,
"print_host"sv,
"print_host_webui"sv,
"printhost_apikey"sv,
@@ -8099,8 +8117,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
if (sloped) {
speed = std::min(speed, m_config.scarf_joint_speed.get_abs_value(speed));
}
}
else if(path.role() == erInternalBridgeInfill) {
} else if(path.role() == erInternalBridgeInfill) {
speed = m_config.get_abs_value_at("internal_bridge_speed", get_nozzle_config_index(m_writer->filament()->id()));
} else if (path.role() == erOverhangPerimeter || path.role() == erSupportTransition || path.role() == erBridgeInfill) {
speed = NOZZLE_CONFIG(bridge_speed);
@@ -8111,7 +8128,10 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
} else if (path.role() == erTopSolidInfill) {
speed = NOZZLE_CONFIG(top_surface_speed);
} else if (path.role() == erIroning) {
speed = m_config.get_abs_value("ironing_speed");
const size_t filament_idx = get_filament_config_index(m_writer->filament()->id());
speed = m_config.filament_ironing_speed.is_nil(filament_idx)
? m_config.get_abs_value("ironing_speed")
: m_config.filament_ironing_speed.get_at(filament_idx);
} else if (path.role() == erBottomSurface) {
speed = NOZZLE_CONFIG(initial_layer_infill_speed);
} else if (path.role() == erGapFill) {
@@ -9419,7 +9439,7 @@ void GCode::update_placeholder_parser_with_variant_params()
}
}
std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bool by_object, int toolchange_temp_override)
std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bool by_object, int toolchange_temp_override, bool defer_temp_wait)
{
int new_extruder_id = get_extruder_id(new_filament_id);
if (!m_writer->need_toolchange(new_filament_id))
@@ -9518,7 +9538,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
// per-layer nozzle grouping; resolve the column instead of indexing by the filament id.
size_t new_fi = get_filament_config_index((int)new_filament_id);
float new_retract_length = m_config.retraction_length.get_at(new_fi);
float new_retract_length_toolchange = m_config.retract_length_toolchange.get_at(new_filament_id);
float new_retract_length_toolchange = m_config.retract_length_toolchange.get_at(new_fi);
int new_filament_temp = this->on_first_layer() ? m_config.nozzle_temperature_initial_layer.get_at(new_fi) : m_config.nozzle_temperature.get_at(new_fi);
// BBS: if print_z == 0 use first layer temperature
if (abs(print_z) < EPSILON)
@@ -9526,6 +9546,24 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
if (toolchange_temp_override > 0)
new_filament_temp = toolchange_temp_override;
// With wait_for_temp_on_wipe_tower the blocking M109 is deferred to the wipe tower, so raise
// the incoming filament's target here — ahead of the tool change rather than after it — and
// let the heat-up overlap the change itself as well as the travel to the tower. The command
// always carries an explicit tool index (the option is off for single extruder MM, so the
// writer emits one), leaving the outgoing filament that pre_toolchange just dropped to its
// standby temperature alone. nozzle_temperature == 0 means "use the first layer temperature".
if (defer_temp_wait) {
// Target what the tower will wait on. It waits on the first layer temperature not only on
// the first layer but also while priming, which runs before any layer is set: there
// on_first_layer() is false and print_z is the initial layer height, so neither test above
// catches it. nozzle_temperature == 0 means "use the first layer temperature" as well.
int preheat_temp = new_filament_temp;
if (toolchange_temp_override <= 0 && (m_layer == nullptr || preheat_temp <= 0))
preheat_temp = m_config.nozzle_temperature_initial_layer.get_at(new_fi);
if (preheat_temp > 0)
gcode += m_writer->set_temperature(preheat_temp, false, new_filament_id);
}
Vec3d nozzle_pos = m_writer->get_position();
float old_retract_length, old_retract_length_toolchange, wipe_volume;
int old_filament_temp, old_filament_e_feedrate;
@@ -9549,7 +9587,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
// gap-filled carry-forward, so its current-layer column matches the nozzle it occupies.
size_t old_fi = get_filament_config_index(old_filament_id);
old_retract_length = m_config.retraction_length.get_at(old_fi);
old_retract_length_toolchange = m_config.retract_length_toolchange.get_at(old_filament_id);
old_retract_length_toolchange = m_config.retract_length_toolchange.get_at(old_fi);
old_filament_temp = this->on_first_layer()? m_config.nozzle_temperature_initial_layer.get_at(old_fi) : m_config.nozzle_temperature.get_at(old_fi);
//During the filament change, the extruder will extrude an extra length of grab_length for the corresponding detection, so the purge can reduce this length.
@@ -9829,8 +9867,10 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
}
check_add_eol(gcode);
}
// Set the new extruder to the operating temperature.
if (m_ooze_prevention.enable)
// Set the new extruder to the operating temperature. With defer_temp_wait the target was
// already raised before the tool change and the blocking wait belongs to the wipe tower
// generator, so there is nothing left to restore here.
if (m_ooze_prevention.enable && !defer_temp_wait)
gcode += m_ooze_prevention.post_toolchange(*this);
if (m_config.enable_pressure_advance.get_at(new_filament_id)) {