mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-08-04 08:42:10 +00:00
Compare commits
7 Commits
feature/up
...
feature/wi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d2196d4a6 | ||
|
|
4303d723a1 | ||
|
|
132086933f | ||
|
|
4d81f0c181 | ||
|
|
1e0fb1a0ae | ||
|
|
72998ddda2 | ||
|
|
4d421918ef |
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user