Compare commits

...

7 Commits

Author SHA1 Message Date
SoftFever
8d2196d4a6 Refine wipe tower size estimates, fit checks, and travel-path code
- Account the SEMM flush-matrix volume in the rib tower size estimate,
  reading its gate from the plate config: m_print may not have been
  applied yet on fresh plates or in the CLI. Rectangle output is
  unchanged.
- Drop the brim growth from the printable-area tower check — the mesh
  bottom already includes the brim, and the pre-slice auto-brim
  estimate can overshoot the generated brim by several mm, so prints
  that physically fit hard-errored. Skip the check when no tower will
  be printed.
- Test the rotated tower body polygon in the gap-route early-out; at
  off-axis angles the body rect's bounding box covers most of the brim
  ring, defeating the tightening.
- Regenerate the wipe tower when it is moved or rotated while
  wait_for_temp_on_wipe_tower is active: the temperature-wait park
  bakes a bed-relative side choice into the cached tower gcode, so a
  re-slice after dragging the tower could park off the bed. Adds a
  regression test.
- Clean up: reuse OozePrevention::_get_temp for the deferred preheat,
  park and route against the exact printable-area polygon, fold the
  bed clamp into generate_path_to_wipe_tower, and take ConfigBase in
  the flush estimators so PartPlate passes the plate config directly.
2026-08-04 13:59:59 +08:00
SoftFever
4303d723a1 Validate the wipe tower against the printable area
A tower (grown by its brim) hanging past the printable outline now fails
Print::validate like the existing exclusion-area check, instead of slicing
silently in the CLI and only warning post-slice in the GUI.
2026-08-03 15:58:36 +08:00
SoftFever
132086933f Fix wipe tower rotation in the clearance check
The pre-generation square was rotated by degrees through the radians API
around the bed origin, and the post-slice mesh bottom was never rotated;
rotate both around the tower anchor via deg2rad. Exact no-op at angle 0.
2026-08-03 15:58:36 +08:00
SoftFever
4d81f0c181 Clamp gap-entry scrub excursions to the wipe tower margin
The dry drag-out and the flat-ironing spiral could leave the margin band no
tower envelope accounts for (the ironing area is unbounded user input);
truncate them to WIPE_TOWER_MARGIN around the tower outline instead of
skipping the scrub.
2026-08-03 15:23:54 +08:00
SoftFever
1e0fb1a0ae Fix the gap-route early-out to test the tower body, not the brim envelope
An approach starting over the first-layer brim ring skipped routing and
travelled straight across the printed wall; only starts over the tower body
itself may go direct.
2026-08-03 15:22:15 +08:00
SoftFever
72998ddda2 Route wipe tower entries near bed edges and honor the real bed shape
Clamp the router's avoid box against the bed shrunk by the routing
clearance so a tower near a bed edge still gets a routed entry instead of
a straight line across its wall, and test route corners against the real
printable outline instead of its bounding box so circular and custom beds
reject off-bed corners.
2026-08-03 15:20:36 +08:00
SoftFever
4d421918ef Harden wait_for_temp_on_wipe_tower parking and temperature deferral
Share one wait_for_temp_enabled gate between the tower and append_tcr2 so a
SEMM profile carrying the flag keeps its blocking ooze-prevention wait.
Park beside the tower on custom polygonal beds via point-in-polygon, clamp
the near-side park toward the bed edge before crossing to the far side, and
start non-blocking heating at the toolchange when ooze prevention emits
nothing so heat-up overlaps the travel.
2026-08-03 15:18:38 +08:00
7 changed files with 317 additions and 90 deletions

View File

@@ -770,30 +770,46 @@ 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, bool clamp_avoid_to_bed) 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.
// 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;
if (clamp_avoid_to_bed) {
// The inflated corners must stay on the bed for a route to be generated at all
// (tested below): clamp the box against the bed shrunk by the clearance 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_polygons);
clamp_bbx.offset(-(alpha + SCALED_EPSILON));
avoid_polygon_inner.min = avoid_polygon_inner.min.cwiseMax(clamp_bbx.min);
avoid_polygon_inner.max = avoid_polygon_inner.max.cwiseMin(clamp_bbx.max);
if (avoid_polygon_inner.min.x() >= avoid_polygon_inner.max.x() ||
avoid_polygon_inner.min.y() >= avoid_polygon_inner.max.y()) {
res.points.push_back(end_pos);
return res;
}
}
avoid_polygon_inner.offset(alpha);
coord_t width = avoid_polygon_inner.max[0] - avoid_polygon_inner.min[0];
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 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.
Points avoid_points = avoid_polygon_inner.polygon().points;
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;
@@ -900,27 +916,30 @@ 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).
// Printable-area outline for tower-approach routing, in object coordinates (shared by
// the BBL avoid-perimeter path in append_tcr and the Type2 skip-points router). The
// real printable_area polygon is returned (not its bounding box) so the router's
// on-bed containment tests fail where circular/custom beds have no bed.
// 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
Polygons WipeTowerIntegration::printer_travel_polygons(GCode &gcodegen) const
{
const Vec2f plate_origin_2d(m_plate_origin(0), m_plate_origin(1));
BoundingBox printer_bbx;
Polygons bed_polygons;
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);
bed_polygons = gcodegen.m_print->get_extruder_shared_printable_polygon();
for (Polygon &poly : bed_polygons)
for (Point &p : poly.points)
p = wipe_tower_point_to_object_point(gcodegen, unscaled<float>(p) + plate_origin_2d);
} else {
Points bed_points;
Polygon bed_polygon;
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);
bed_polygon.points.push_back(wipe_tower_point_to_object_point(gcodegen, p.cast<float>() + plate_origin_2d));
bed_polygons.emplace_back(std::move(bed_polygon));
}
return printer_bbx;
return bed_polygons;
}
// With skip points enabled the Type2 tower wall has an opening at each toolchange's
@@ -941,9 +960,20 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
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))
// 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.
const float body_width = gcodegen.m_config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib ? m_wipe_tower_depth : m_right;
Polygon body_points = scaled(BoundingBoxf(Vec2d(0., 0.), Vec2d(body_width, m_wipe_tower_depth))).polygon();
for (auto& p : body_points.points)
p = wipe_tower_point_to_object_point(gcodegen, transform_wt2_pt(unscale(p).cast<float>()) + plate_origin_2d);
// Test the rotated polygon itself, not its bounding box — at rotation angles off
// the axes the box's corner triangles cover most of the brim ring.
if (body_points.contains(route_start))
return {};
Polyline travel_polyline = generate_path_to_wipe_tower(route_start, start_wipe_pos, avoid_bbx, printer_travel_bounds(gcodegen));
Polyline travel_polyline = generate_path_to_wipe_tower(route_start, start_wipe_pos, avoid_bbx,
printer_travel_polygons(gcodegen), /*clamp_avoid_to_bed=*/true);
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)
@@ -1324,7 +1354,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);
@@ -1336,7 +1366,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, printer_travel_polygons(gcodegen));
for (size_t i = 0; i < travel_polyline.points.size(); ++i) {
const auto &p = travel_polyline.points[i];
@@ -1557,7 +1587,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
toolchange_temp_override = interface_temp;
}
toolchange_gcode_str = gcodegen.set_extruder(new_extruder_id, tcr.print_z, false, toolchange_temp_override,
gcodegen.config().wait_for_temp_on_wipe_tower.value); // TODO: toolchange_z vs print_z
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
@@ -9350,8 +9380,20 @@ 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.
std::string restore_temp_gcode;
if (m_ooze_prevention.enable)
gcode += m_ooze_prevention.post_toolchange(*this, !defer_temp_wait);
restore_temp_gcode = m_ooze_prevention.post_toolchange(*this, !defer_temp_wait);
if (defer_temp_wait && restore_temp_gcode.empty()) {
// The blocking M109 is emitted by the wipe tower generator; with no temperature
// command here the nozzle would only start heating once the head arrives there.
// Heat non-blocking now so the heat-up overlaps the travel, targeting what the
// tower will wait on (the writer already holds the new filament, so _get_temp
// resolves its column) plus the interface override.
int temp = toolchange_temp_override > 0 ? toolchange_temp_override : m_ooze_prevention._get_temp(*this);
if (temp > 0)
restore_temp_gcode = m_writer.set_temperature(temp, false, new_filament_id);
}
gcode += restore_temp_gcode;
if (m_config.enable_pressure_advance.get_at(new_filament_id)) {
gcode += m_writer.set_pressure_advance(m_config.pressure_advance.get_at(new_filament_id));

View File

@@ -51,8 +51,9 @@ public:
OozePrevention() : enable(false) {}
std::string pre_toolchange(GCode &gcodegen);
std::string post_toolchange(GCode &gcodegen, bool wait = true);
private:
// The operating temperature for the writer's current filament; public so the
// wait_for_temp_on_wipe_tower pre-heat in GCode::set_extruder targets the same
// temperature the tower's blocking M109 will wait on.
int _get_temp(const GCode &gcodegen) const;
};
@@ -130,11 +131,11 @@ public:
private:
WipeTowerIntegration& operator=(const WipeTowerIntegration&);
std::string append_tcr(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const;
Polyline generate_path_to_wipe_tower(const Point &start_pos, const Point &end_pos, const BoundingBox &avoid_polygon, const BoundingBox &printer_bbx) const;
Polyline generate_path_to_wipe_tower(const Point &start_pos, const Point &end_pos, const BoundingBox &avoid_polygon, const Polygons &bed_polygons, bool clamp_avoid_to_bed = false) const;
std::string append_tcr2(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const;
std::string travel_to_tower_gap(GCode &gcodegen, const Point &route_start, const Point &start_wipe_pos) const;
Vec2f transform_wt2_pt(const Vec2f &pt) const;
BoundingBox printer_travel_bounds(GCode &gcodegen) const;
Polygons printer_travel_polygons(GCode &gcodegen) const;
// Postprocesses gcode: rotates and moves G1 extrusions and returns result
std::string post_process_wipe_tower_moves(const WipeTower::ToolChangeResult& tcr, const Vec2f& translation, float angle) const;

View File

@@ -735,14 +735,23 @@ public:
float edge_length = std::sqrt(area);
Vec2f box_max = center + Vec2f{step_length, step_length};
Vec2f box_min = center - Vec2f{step_length, step_length};
// The ironing area is unbounded user input; keep the spiral inside the margin band
// around the tower outline that every tower envelope already accounts for.
// step_length is the perimeter line width, and writer coordinates are offset
// from the tower outline by m_y_shift in y.
const float lim = float(WIPE_TOWER_MARGIN) - step_length / 2.f;
const Vec2f clamp_min{-lim, -lim - m_y_shift};
const Vec2f clamp_max{m_wipe_tower_width + lim, m_wipe_tower_depth + lim - m_y_shift};
int n = std::ceil(edge_length / step_length / 2.f);
if (n <= 0)
return;
while (n--) {
travel(box_max.x(), m_current_pos.y(), feedrate);
travel(m_current_pos.x(), box_max.y(), feedrate);
travel(box_min.x(), m_current_pos.y(), feedrate);
travel(m_current_pos.x(), box_min.y(), feedrate);
const Vec2f lap_max = box_max.cwiseMin(clamp_max);
const Vec2f lap_min = box_min.cwiseMax(clamp_min);
travel(lap_max.x(), m_current_pos.y(), feedrate);
travel(m_current_pos.x(), lap_max.y(), feedrate);
travel(lap_min.x(), m_current_pos.y(), feedrate);
travel(m_current_pos.x(), lap_min.y(), feedrate);
box_max += Vec2f{step_length, step_length};
box_min -= Vec2f{step_length, step_length};
@@ -1009,6 +1018,13 @@ bool WipeTower2::use_gap_wall(const PrintConfig& config)
return config.prime_tower_skip_points.value && config.wipe_tower_wall_type.value != wtwCone;
}
bool WipeTower2::wait_for_temp_enabled(const PrintConfig& config)
{
// SEMM runs its own unload/load temperature sequence; the GUI hides the option
// there but a profile may still carry it set.
return config.wait_for_temp_on_wipe_tower.value && !config.single_extruder_multi_material.value;
}
WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& default_region_config,int plate_idx, Vec3d plate_origin, const std::vector<std::vector<float>>& wiping_matrix, size_t initial_tool) :
m_semm(config.single_extruder_multi_material.value),
m_enable_filament_ramming(config.enable_filament_ramming.value),
@@ -1039,7 +1055,7 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau
m_use_gap_wall(use_gap_wall(config)),
m_enable_tower_interface_features(config.enable_tower_interface_features.value),
m_enable_tower_interface_cooldown_during_tower(config.enable_tower_interface_cooldown_during_tower.value),
m_wait_for_temp_on_wipe_tower(config.wait_for_temp_on_wipe_tower.value && !config.single_extruder_multi_material.value)
m_wait_for_temp_on_wipe_tower(wait_for_temp_enabled(config))
{
// Read absolute value of first layer speed, if given as percentage,
// it is taken over following default. Speeds from config are not
@@ -1071,7 +1087,6 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau
// Calculate where the priming lines should be - very naive test not detecting parallelograms etc.
const std::vector<Vec2d>& bed_points = config.printable_area.values;
BoundingBoxf bb(bed_points);
m_bed_bbox = bb;
m_bed_width = float(bb.size().x());
m_bed_shape = (bed_points.size() == 4 ? RectangularBed : CircularBed);
@@ -1090,6 +1105,7 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau
m_bed_bottom_left = m_bed_shape == RectangularBed
? Vec2f(bed_points.front().x(), bed_points.front().y())
: Vec2f::Zero();
m_bed_polygon = Polygon::new_scale(bed_points);
}
@@ -1703,9 +1719,10 @@ void WipeTower2::toolchange_Change(
// The Tn above was issued without a blocking temperature wait (OozePrevention::post_toolchange
// only restores the target); block here, before the deretraction below, which must not extrude
// on a cold nozzle. Like the Bambu H2C, park beside the tower for the heat-up so drool lands
// next to it instead of on its top surface — nearest x side first, the other side as fallback,
// in place if both would leave the bed. Raw pre-rotated moves (see the repositioning move
// below) keep the writer's tracked position at the tower entry. The tag keeps the
// next to it instead of on its top surface — nearest x side first, then that side clamped
// toward the bed edge, then the far side, in place if all would leave the bed. Raw
// pre-rotated moves (see the repositioning move below) keep the writer's tracked position
// at the tower entry. The tag keeps the
// interface-temp deduplication pass in append_tcr2 from stripping the M109.
if (wait_for_temp_here && wait_beside_tower) {
// The rib wall and the stabilization cone bulge past the nominal width rectangle
@@ -1736,28 +1753,47 @@ void WipeTower2::toolchange_Change(
min_x -= brim;
max_x += brim;
}
constexpr float gap = 2.f;
const float near_x = writer.x() < m_wipe_tower_width / 2.f ? min_x - gap : max_x + gap;
const float far_x = near_x < m_wipe_tower_width / 2.f ? max_x + gap : min_x - gap;
const float a = float(m_wipe_tower_rotation_angle * M_PI / 180.);
const float c = std::cos(a), s = std::sin(a);
for (float side_x : { near_x, far_x }) {
const Vec2f stop = writer.rotated(Vec2f(side_x, writer.y()));
const Vec2f wt = stop + m_rib_offset;
const Vec2f bed_pt(c * wt.x() - s * wt.y() + m_wipe_tower_pos.x(),
s * wt.x() + c * wt.y() + m_wipe_tower_pos.y());
bool on_bed = false;
if (m_bed_shape == RectangularBed)
on_bed = m_bed_bbox.contains(bed_pt.cast<double>());
else if (m_bed_shape == CircularBed)
on_bed = (bed_pt.cast<double>() - m_bed_bbox.center()).norm() <= m_bed_width / 2.;
if (!on_bed)
continue;
constexpr float gap = 2.f;
constexpr float min_gap = 0.5f;
const bool on_left = writer.x() < m_wipe_tower_width / 2.f;
const float near_x = on_left ? min_x - gap : max_x + gap;
const float far_x = on_left ? max_x + gap : min_x - gap;
const Eigen::Rotation2Df to_bed(float(Geometry::deg2rad(m_wipe_tower_rotation_angle)));
auto park_pt_on_bed = [this, &writer, to_bed](float side_x) {
const Vec2f bed_pt = to_bed * (writer.rotated(Vec2f(side_x, writer.y())) + m_rib_offset) + m_wipe_tower_pos;
return m_bed_polygon.contains(Point::new_scale(bed_pt.x(), bed_pt.y()));
};
float park_x = near_x;
bool have_park = park_pt_on_bed(near_x);
if (!have_park) {
// The ideal near point hangs off the bed: pull it back to the bed edge as long
// as that still clears the tower envelope by min_gap (the BBL tower clamps its
// stop_pos against the bed the same way in append_tcr). Bisection, because with
// tower rotation and non-rectangular beds the bed edge is not axis-aligned.
const float limit_x = on_left ? min_x - min_gap : max_x + min_gap;
if (park_pt_on_bed(limit_x)) {
float on = limit_x, off = near_x;
for (int i = 0; i < 8; ++i) {
const float mid = 0.5f * (on + off);
if (park_pt_on_bed(mid))
on = mid;
else
off = mid;
}
park_x = on;
have_park = true;
}
}
if (!have_park && park_pt_on_bed(far_x)) {
park_x = far_x;
have_park = true;
}
if (have_park) {
const Vec2f stop = writer.rotated(Vec2f(park_x, writer.y()));
writer.feedrate(m_travel_speed * 60.f)
.append(std::string("G1 X") + Slic3r::float_to_string_decimal_point(stop.x())
+ " Y" + Slic3r::float_to_string_decimal_point(stop.y())
+ never_skip_tag() + "\n");
break;
}
writer.set_extruder_temp(wait_for_temp, true, wait_for_temp_tag());
}
@@ -1878,11 +1914,14 @@ void WipeTower2::toolchange_Wipe(
ironing_length = std::max(xr - writer.x(), 0.f);
const float retract_length = m_filpar[m_current_tool].retract_length;
const float retract_speed = m_filpar[m_current_tool].retract_speed * 60.f;
// Keep the dry scrub inside the margin band around the tower outline that
// every tower envelope already accounts for.
const float scrub_lim = float(WIPE_TOWER_MARGIN) - m_perimeter_width / 2.f;
writer.extrude(writer.x() + ironing_length, writer.y(), wipe_speed);
const Vec2f iron_end = writer.pos();
writer.retract(retract_length, retract_speed);
writer.travel(writer.x() - 1.5f * ironing_length, writer.y(), 600.f);
writer.travel(writer.x() + 0.5f * ironing_length, writer.y(), 240.f);
const Vec2f iron_end(writer.x() + ironing_length, writer.y());
writer.travel(std::clamp(writer.x() - 1.5f * ironing_length, -scrub_lim, m_wipe_tower_width + scrub_lim), writer.y(), 600.f);
writer.travel(std::clamp(writer.x() + 0.5f * ironing_length, -scrub_lim, m_wipe_tower_width + scrub_lim), writer.y(), 240.f);
writer.spiral_flat_ironing(writer.pos(), m_filpar[m_current_tool].tower_ironing_area, m_perimeter_width, flat_iron_speed);
writer.travel(iron_end, wipe_speed);
writer.retract(-retract_length, retract_speed);
@@ -2102,15 +2141,17 @@ std::pair<double, double> WipeTower2::get_wipe_tower_cone_base(double width, dou
}
// Static method to extract wipe_volumes[from][to] from the configuration.
std::vector<std::vector<float>> WipeTower2::extract_wipe_volumes(const PrintConfig& config)
// Takes a ConfigBase so the GUI's wipe tower size estimate can pass the plate's
// DynamicPrintConfig directly instead of materializing a full PrintConfig per call.
std::vector<std::vector<float>> WipeTower2::extract_wipe_volumes(const ConfigBase& config)
{
// Get wiping matrix to get number of extruders and convert vector<double> to vector<float>:
std::vector<float> wiping_matrix(cast<float>(config.flush_volumes_matrix.values));
auto scale = config.flush_multiplier.get_at(0);
std::vector<float> wiping_matrix(cast<float>(config.option<ConfigOptionFloats>("flush_volumes_matrix")->values));
auto scale = config.option<ConfigOptionFloats>("flush_multiplier")->get_at(0);
// The values shall only be used when SEMM is enabled. The purging for other printers
// is determined by filament_minimal_purge_on_wipe_tower.
if (! config.purge_in_prime_tower.value || ! config.single_extruder_multi_material.value)
if (! config.option<ConfigOptionBool>("purge_in_prime_tower")->value || ! config.option<ConfigOptionBool>("single_extruder_multi_material")->value)
std::fill(wiping_matrix.begin(), wiping_matrix.end(), 0.f);
// Extract purging volumes for each extruder pair:
@@ -2120,13 +2161,28 @@ std::vector<std::vector<float>> WipeTower2::extract_wipe_volumes(const PrintConf
wipe_volumes.push_back(std::vector<float>(wiping_matrix.begin()+i*number_of_extruders, wiping_matrix.begin()+(i+1)*number_of_extruders));
// Also include filament_minimal_purge_on_wipe_tower. This is needed for the preview.
const auto *minimal_purge = config.option<ConfigOptionFloats>("filament_minimal_purge_on_wipe_tower");
for (unsigned int i = 0; i<number_of_extruders; ++i)
for (unsigned int j = 0; j<number_of_extruders; ++j)
wipe_volumes[i][j] = std::max<float>(wipe_volumes[i][j] * scale, config.filament_minimal_purge_on_wipe_tower.get_at(j));
wipe_volumes[i][j] = std::max<float>(wipe_volumes[i][j] * scale, minimal_purge->get_at(j));
return wipe_volumes;
}
float WipeTower2::estimate_semm_flush_volume(const ConfigBase& config, size_t filaments_cnt)
{
std::vector<std::vector<float>> wipe_volumes = extract_wipe_volumes(config);
std::vector<float> max_wipe_volumes;
for (const std::vector<float> &v : wipe_volumes)
max_wipe_volumes.emplace_back(*std::max_element(v.begin(), v.end()));
float maximum = std::accumulate(max_wipe_volumes.begin(), max_wipe_volumes.end(), 0.f);
maximum = maximum * filaments_cnt / max_wipe_volumes.size();
// Orca: it's overshooting a bit, so let's reduce it a bit
maximum *= 0.6;
return maximum;
}
static float get_wipe_depth(float volume, float layer_height, float perimeter_width, float extra_flow, float extra_spacing, float width)
{
float length_to_extrude = (volume_to_length(volume, perimeter_width, layer_height)) / extra_flow;

View File

@@ -17,6 +17,7 @@ namespace Slic3r
class WipeTowerWriter2;
class PrintRegionConfig;
class ConfigBase;
class WipeTower2
{
@@ -26,7 +27,10 @@ public:
// in WipeTowerIntegration::append_tcr2 does not strip it.
static const std::string wait_for_temp_tag() { return ";_WAIT_FOR_TEMP_ON_WIPE_TOWER"; }
static std::pair<double, double> get_wipe_tower_cone_base(double width, double height, double depth, double angle_deg);
static std::vector<std::vector<float>> extract_wipe_volumes(const PrintConfig& config);
static std::vector<std::vector<float>> extract_wipe_volumes(const ConfigBase& config);
// Estimated total flush volume of a SEMM print with the given number of filaments,
// used to reserve wipe tower space before the tower is generated.
static float estimate_semm_flush_volume(const ConfigBase& config, size_t filaments_cnt);
// Construct ToolChangeResult from current state of WipeTower2 and WipeTowerWriter2.
@@ -41,6 +45,11 @@ public:
// Shared with the entry routing in GCode.cpp so the router and the tower agree.
static bool use_gap_wall(const PrintConfig& config);
// Whether the blocking toolchange temperature wait moves onto the wipe tower.
// Shared with the defer flag in GCode.cpp append_tcr2 so the deferral and the
// tower's tagged M109 can never disagree.
static bool wait_for_temp_enabled(const PrintConfig& config);
// x -- x coordinates of wipe tower in mm ( left bottom corner )
// y -- y coordinates of wipe tower in mm ( left bottom corner )
// width -- width of wipe tower in mm ( default 60 mm - leave as it is )
@@ -267,7 +276,7 @@ private:
} m_bed_shape;
float m_bed_width; // width of the bed bounding box
Vec2f m_bed_bottom_left; // bottom-left corner coordinates (for rectangular beds)
BoundingBoxf m_bed_bbox; // bounding box of the printable area
Polygon m_bed_polygon; // printable_area contour (scaled)
float m_perimeter_width = 0.4f * Width_To_Nozzle_Ratio; // Width of an extrusion line, also a perimeter spacing for 100% infill.
float m_extrusion_flow = 0.038f; //0.029f;// Extrusion flow is derived from m_perimeter_width, layer height and filament diameter.

View File

@@ -282,6 +282,14 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|| opt_key == "wipe_tower_x"
|| opt_key == "wipe_tower_y"
|| opt_key == "wipe_tower_rotation_angle") {
// The tower gcode itself is position-independent (position and rotation are applied
// at export), except that the wait_for_temp_on_wipe_tower park bakes a bed-relative
// side choice into it (WipeTower2::toolchange_Change) — regenerate it when the tower
// moves. Gating on the old config is safe: both inputs of wait_for_temp_enabled
// invalidate psWipeTower themselves when they are part of the same diff.
if ((opt_key == "wipe_tower_x" || opt_key == "wipe_tower_y" || opt_key == "wipe_tower_rotation_angle")
&& WipeTower2::wait_for_temp_enabled(m_config))
steps.emplace_back(psWipeTower);
steps.emplace_back(psSkirtBrim);
} else if (
opt_key == "slicing_pipeline_plugin"
@@ -1040,13 +1048,14 @@ static StringObjectException layered_print_cleareance_valid(const Print &print,
wipe_tower_convex_hull.points.emplace_back(scale_(x + width), scale_(y));
wipe_tower_convex_hull.points.emplace_back(scale_(x + width), scale_(y + depth));
wipe_tower_convex_hull.points.emplace_back(scale_(x), scale_(y + depth));
wipe_tower_convex_hull.rotate(a);
wipe_tower_convex_hull.rotate(Geometry::deg2rad(a), Point(scale_(x), scale_(y)));
convex_hulls_temp.push_back(wipe_tower_convex_hull);
} else {
//here, wipe_tower_polygon is not always convex.
Polygon wipe_tower_polygon;
if (print.wipe_tower_data().wipe_tower_mesh_data)
wipe_tower_polygon = print.wipe_tower_data().wipe_tower_mesh_data->bottom;
wipe_tower_polygon.rotate(Geometry::deg2rad(a));
wipe_tower_polygon.translate(Point(scale_(x), scale_(y)));
convex_hulls_temp.push_back(wipe_tower_polygon);
}
@@ -1065,6 +1074,21 @@ static StringObjectException layered_print_cleareance_valid(const Print &print,
if (print_config.enable_wrapping_detection.value && !intersection({wrapping_poly}, convex_hulls_temp).empty()) {
return {L("Prime Tower") + L(" is too close to clumping detection area, and collisions will be caused.\n")};
}
// Skip the containment check for towers that will never be printed (single-filament
// prints without smooth timelapse keep the config's tower position but emit nothing).
// Pre-generation only the body square is tested — the auto-brim estimate can overshoot
// the generated brim by several mm and must not hard-fail a print that physically fits.
// Post-generation the mesh bottom already includes the real brim, so the exact
// footprint is tested.
if (!convex_hulls_temp.empty() && (filaments_count > 1 || print.enable_timelapse_print())) {
// The shared printable polygon is plate-local, while the tower polygons above are
// already shifted by the plate origin.
Polygons printable_polys = print.get_extruder_shared_printable_polygon();
std::for_each(printable_polys.begin(), printable_polys.end(),
[&plate_origin](Polygon& p) { p.translate(scale_(plate_origin.x()), scale_(plate_origin.y())); });
if (!diff(convex_hulls_temp, printable_polys).empty())
return {L("Prime Tower") + L(" is partially outside the printable area, and it cannot be printed.\n")};
}
return {};
}
@@ -3920,6 +3944,8 @@ const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const
if (m_config.nozzle_diameter.values.size() == 2) volume += filament_change_volume * (int) (filaments_cnt / 2);
if (m_config.wipe_tower_wall_type.value == WipeTowerWallType::wtwRib) {
if (m_config.purge_in_prime_tower && m_config.single_extruder_multi_material)
volume = WipeTower2::estimate_semm_flush_volume(m_config, filaments_cnt);
double depth = std::sqrt(volume / layer_height * extra_spacing);
if (need_wipe_tower || filaments_cnt > 1) {
float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height);
@@ -3935,15 +3961,7 @@ const WipeTowerData &Print::wipe_tower_data(size_t filaments_cnt) const
// Calculating depth should take into account currently set wiping volumes.
// For a long time, the initial preview would just use 900/width per toolchange (15mm on a 60mm wide tower)
// and it worked well enough. Let's try to do slightly better by accounting for the purging volumes.
std::vector<std::vector<float>> wipe_volumes = WipeTower2::extract_wipe_volumes(m_config);
std::vector<float> max_wipe_volumes;
for (const std::vector<float> &v : wipe_volumes)
max_wipe_volumes.emplace_back(*std::max_element(v.begin(), v.end()));
float maximum = std::accumulate(max_wipe_volumes.begin(), max_wipe_volumes.end(), 0.f);
maximum = maximum * filaments_cnt / max_wipe_volumes.size();
// Orca: it's overshooting a bit, so let's reduce it a bit
maximum *= 0.6;
float maximum = WipeTower2::estimate_semm_flush_volume(m_config, filaments_cnt);
const_cast<Print *>(this)->m_wipe_tower_data.depth = maximum / (layer_height * width);
} else {
double depth = volume / (layer_height * width) * extra_spacing;

View File

@@ -2263,6 +2263,12 @@ Vec3d PartPlate::estimate_wipe_tower_size(const DynamicPrintConfig & config, con
double volume = wipe_volume * (extruder_count == 2 ? plate_extruder_size : (plate_extruder_size - 1));
if (extruder_count == 2) volume += filament_change_volume * (int) (plate_extruder_size / 2);
if (use_rib_wall) {
// Read from the passed plate config — m_print may not have been applied yet
// (fresh plates, CLI), in which case its PrintConfig still holds defaults.
const auto *purge_opt = config.option<ConfigOptionBool>("purge_in_prime_tower");
const auto *semm_opt = config.option<ConfigOptionBool>("single_extruder_multi_material");
if (purge_opt && purge_opt->value && semm_opt && semm_opt->value)
volume = WipeTower2::estimate_semm_flush_volume(config, plate_extruder_size);
depth = std::sqrt(volume / layer_height * extra_spacing);
if (need_wipe_tower || plate_extruder_size > 1) {
float min_wipe_tower_depth = WipeTower::get_limit_depth_by_height(max_height);

View File

@@ -7,6 +7,7 @@
#include <cctype>
#include <limits>
#include <set>
#include <sstream>
#include <string>
#include <vector>
@@ -29,6 +30,31 @@ static std::set<int> tools_for_role(const std::string& gcode, const std::string&
return tools;
}
// X where the nozzle sits while each tagged _WAIT_FOR_TEMP_ON_WIPE_TOWER M109 blocks:
// the nearest preceding G1 carrying an X (the park travel emitted just before the wait).
static std::vector<double> wait_park_xs(const std::string& gcode)
{
std::vector<std::string> lines;
std::istringstream stream(gcode);
for (std::string line; std::getline(stream, line);)
lines.emplace_back(std::move(line));
std::vector<double> xs;
for (size_t i = 0; i < lines.size(); ++i) {
if (lines[i].rfind("M109", 0) != 0 || lines[i].find("_WAIT_FOR_TEMP_ON_WIPE_TOWER") == std::string::npos)
continue;
for (size_t j = i; j-- > 0;) {
if (lines[j].rfind("G1 ", 0) != 0)
continue;
const size_t x_pos = lines[j].find('X');
if (x_pos == std::string::npos)
continue;
xs.push_back(std::stod(lines[j].substr(x_pos + 1)));
break;
}
}
return xs;
}
// Tool index = filament id - 1; brim and skirt follow the wall filament.
TEST_CASE("Each feature prints with its assigned filament", "[MultiFilament]")
{
@@ -122,13 +148,9 @@ TEST_CASE("Toolchange temperature wait moves to the wipe tower when enabled", "[
// Split into lines and scan the "; CP TOOLCHANGE START".."; CP TOOLCHANGE END" blocks.
std::vector<std::string> lines;
for (size_t pos = 0; pos < gcode.size();) {
size_t eol = gcode.find('\n', pos);
if (eol == std::string::npos)
eol = gcode.size();
lines.emplace_back(gcode.substr(pos, eol - pos));
pos = eol + 1;
}
std::istringstream gcode_stream(gcode);
for (std::string line; std::getline(gcode_stream, line);)
lines.emplace_back(std::move(line));
const auto is_tool_line = [](const std::string& l) { return l.size() >= 2 && l[0] == 'T' && std::isdigit((unsigned char)l[1]); };
const auto is_m109_line = [](const std::string& l) { return l.rfind("M109", 0) == 0; };
const auto is_tagged_wait = [](const std::string& l) { return l.find("_WAIT_FOR_TEMP_ON_WIPE_TOWER") != std::string::npos; };
@@ -204,6 +226,79 @@ TEST_CASE("Toolchange temperature wait moves to the wipe tower when enabled", "[
}
}
// The temperature-wait park picks its side of the tower by testing bed containment with the
// tower position at psWipeTower generation time, while WipeTowerIntegration shifts the cached
// moves by the CURRENT position at export. Moving the tower normally invalidates only
// psSkirtBrim (tower gcode is position-independent), but the park makes it bed-relative, so a
// GUI-style move-and-reslice on the same Print must regenerate the tower — otherwise the stale
// park prints outside the bed. Contract: every tagged wait parks inside the printable area.
TEST_CASE("Wipe tower temperature-wait park is regenerated when the tower moves", "[MultiFilament]")
{
// Two objects, one filament each: a toolchange (and a tagged wait) on every layer, like
// the wait test above — but on a single-extruder machine profile: the synthetic
// dual-extruder keys would drag in the extruder-variant expansion, which is not
// idempotent on the default machine profile and would pollute the re-apply diff below.
// Rectangle wall and no brim keep the tower-local footprint inside [0, 35], so the park
// sits at the generator's 2mm side gap: local -2 or 37.
DynamicPrintConfig config = multifilament_config(2, {
{ "single_extruder_multi_material", 0 },
{ "enable_prime_tower", 1 },
{ "prime_tower_width", 35 },
{ "wipe_tower_wall_type", "rectangle" }, // the default rib bulges past the width
{ "prime_tower_brim_width", 0 }, // the default 3 widens the first-layer envelope
{ "printable_area", "0x0,200x0,200x200,0x200" },
{ "wipe_tower_x", "0" },
{ "wipe_tower_y", "50" },
{ "ooze_prevention", 1 },
{ "standby_temperature_delta", -40 },
{ "wait_for_temp_on_wipe_tower", 1 },
});
// init_print force-sets this on its own copy; set it here too so the re-apply below
// diffs in wipe_tower_x ONLY — the exact GUI increment under test.
config.set_key_value("gcode_comments", new ConfigOptionBool(true));
Print print;
Model model;
const std::vector<std::vector<ConfigBase::SetDeserializeItem>> overrides{
{ { "extruder", 1 } }, { { "extruder", 2 } } }; // object-level, see the wait test above
init_print(std::vector<TriangleMesh>{ cube(20), cube(20) }, print, model, config, &overrides);
const std::string at_edge = gcode(print);
const std::vector<double> at_edge_parks = wait_park_xs(at_edge);
REQUIRE(!at_edge_parks.empty()); // the feature under test is active
for (double x : at_edge_parks) {
INFO("wait park X " << x << " with the tower at x=0 on a 200mm bed");
CHECK(x >= -0.05);
CHECK(x <= 200.05);
}
REQUIRE(print.is_step_done(psWipeTower));
// Move the tower to the right bed edge (164 + 35 = 199 keeps the body printable) and
// re-apply on the SAME Print, as the GUI does. Base the re-apply on the print's own
// resolved config so the diff is wipe_tower_x alone — re-applying the caller's config
// would also diff the apply-time extruder normalization write-backs, and those keys
// regenerate the tower for the wrong reason. The cached right-side park would export
// at 164 + 37 = 201, off the bed; regeneration clamps the park against the bed edge.
// Assemble the moved config exactly the way init_print assembled the first one — the
// apply-time normalization is only idempotent when both applies start from the same
// derivation, and any stray diff key would regenerate the tower for the wrong reason.
config.set_deserialize_strict({ { "wipe_tower_x", "164" } });
DynamicPrintConfig moved_config = DynamicPrintConfig::full_print_config();
moved_config.apply(config);
moved_config.set_key_value("gcode_comments", new ConfigOptionBool(true));
print.apply(model, moved_config);
CHECK_FALSE(print.is_step_done(psWipeTower)); // the move must re-generate the tower
const std::string moved = gcode(print);
const std::vector<double> moved_parks = wait_park_xs(moved);
REQUIRE(!moved_parks.empty()); // the waits must survive the re-slice
for (double x : moved_parks) {
INFO("wait park X " << x << " with the tower at x=164 on a 200mm bed");
CHECK(x >= -0.05);
CHECK(x <= 200.05);
}
}
// max_layer_height can be shorter than the extruder count (normalization sizes it to the
// filament count under single_extruder_multi_material). calc_max_layer_height() in ToolOrdering
// indexed it per-nozzle and read past the end. Shortened directly here to isolate that read;