|
|
|
@@ -24,6 +24,7 @@
|
|
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <array>
|
|
|
|
|
#include <boost/algorithm/string/case_conv.hpp>
|
|
|
|
|
#include <boost/algorithm/string/classification.hpp>
|
|
|
|
|
#include <boost/algorithm/string/constants.hpp>
|
|
|
|
|
#include <boost/log/trivial.hpp>
|
|
|
|
@@ -378,6 +379,11 @@ void GCodeProcessor::TimeMachine::reset()
|
|
|
|
|
travel_acceleration = 0.0f;
|
|
|
|
|
max_travel_acceleration = 0.0f;
|
|
|
|
|
extrude_factor_override_percentage = 1.0f;
|
|
|
|
|
klipper = false;
|
|
|
|
|
minimum_cruise_ratio = 0.5f;
|
|
|
|
|
requested_accel_to_decel = -1.0f;
|
|
|
|
|
klipper_junction_flush = 1.0f;
|
|
|
|
|
klipper_queue_priming = true;
|
|
|
|
|
time = 0.0f;
|
|
|
|
|
stop_times = std::vector<StopTime>();
|
|
|
|
|
curr.reset();
|
|
|
|
@@ -493,16 +499,87 @@ GCodeProcessor::TimeMachine::AdditionalBuffer GCodeProcessor::TimeMachine::merge
|
|
|
|
|
return merged;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Klipper toolhead.py LookAheadQueue.flush: the pseudo-acceleration pass
|
|
|
|
|
// reserves cruising distance across a sequence, not separately in each segment.
|
|
|
|
|
size_t GCodeProcessor::TimeMachine::plan_klipper(bool lazy)
|
|
|
|
|
{
|
|
|
|
|
klipper_junction_flush = requested_accel_to_decel >= 0.0f ? 0.250f : 0.150f;
|
|
|
|
|
klipper_queue_priming = false;
|
|
|
|
|
size_t flush_count = blocks.size();
|
|
|
|
|
bool update_flush_count = lazy;
|
|
|
|
|
float next_start_v2 = 0.0f;
|
|
|
|
|
float next_mcr_start_v2 = 0.0f;
|
|
|
|
|
float peak_cruise_v2 = 0.0f;
|
|
|
|
|
size_t pending = 0;
|
|
|
|
|
for (size_t i = blocks.size(); i-- > 0;) {
|
|
|
|
|
TimeBlock& block = blocks[i];
|
|
|
|
|
const float delta_v2 = 2.0f * block.distance * block.acceleration;
|
|
|
|
|
const float reachable_start_v2 = next_start_v2 + delta_v2;
|
|
|
|
|
const float start_v2 = std::min(sqr(block.max_entry_speed), reachable_start_v2);
|
|
|
|
|
const float reachable_mcr_v2 = next_mcr_start_v2 + block.mcr_delta_v2;
|
|
|
|
|
const float mcr_start_v2 = std::min(block.max_mcr_entry_speed_sqr, reachable_mcr_v2);
|
|
|
|
|
float cruise_v2 = -1.0f;
|
|
|
|
|
++pending;
|
|
|
|
|
if (mcr_start_v2 < reachable_mcr_v2) {
|
|
|
|
|
if (mcr_start_v2 + block.mcr_delta_v2 > next_mcr_start_v2 || pending > 1) {
|
|
|
|
|
// Stop at this block, not past it: the blocks above still have a cruise speed
|
|
|
|
|
// that depends on moves not yet seen, so only [0, i) may be committed.
|
|
|
|
|
if (update_flush_count && peak_cruise_v2 > 0.0f) {
|
|
|
|
|
flush_count = i;
|
|
|
|
|
update_flush_count = false;
|
|
|
|
|
}
|
|
|
|
|
peak_cruise_v2 = std::min(sqr(block.feedrate_profile.cruise),
|
|
|
|
|
0.5f * (mcr_start_v2 + reachable_mcr_v2));
|
|
|
|
|
}
|
|
|
|
|
cruise_v2 = std::min({0.5f * (start_v2 + reachable_start_v2),
|
|
|
|
|
sqr(block.feedrate_profile.cruise), peak_cruise_v2});
|
|
|
|
|
pending = 0;
|
|
|
|
|
}
|
|
|
|
|
// Until the forward pass, these fields hold v^2, not speeds; -1 marks
|
|
|
|
|
// an unassigned cruise speed. A lazy return leaves them in this state.
|
|
|
|
|
block.feedrate_profile.entry = start_v2;
|
|
|
|
|
block.feedrate_profile.exit = next_start_v2;
|
|
|
|
|
block.trapezoid.cruise_feedrate = cruise_v2;
|
|
|
|
|
block.peak_cruise_v2 = peak_cruise_v2;
|
|
|
|
|
next_start_v2 = start_v2;
|
|
|
|
|
next_mcr_start_v2 = mcr_start_v2;
|
|
|
|
|
}
|
|
|
|
|
if (update_flush_count)
|
|
|
|
|
return 0;
|
|
|
|
|
// A run of blocks that cannot accelerate is resolved here, once the peak cruise speed of the
|
|
|
|
|
// accelerating block that precedes it is known. Each one takes a running minimum with its own
|
|
|
|
|
// entry speed, seeded from that peak rather than from the setter's own clamped cruise speed.
|
|
|
|
|
float prev_cruise_v2 = 0.0f;
|
|
|
|
|
for (size_t i = 0; i < flush_count; ++i) {
|
|
|
|
|
TimeBlock& block = blocks[i];
|
|
|
|
|
float cruise_v2 = block.trapezoid.cruise_feedrate;
|
|
|
|
|
if (cruise_v2 < 0.0f) {
|
|
|
|
|
cruise_v2 = std::min(prev_cruise_v2, block.feedrate_profile.entry);
|
|
|
|
|
prev_cruise_v2 = cruise_v2;
|
|
|
|
|
} else
|
|
|
|
|
prev_cruise_v2 = block.peak_cruise_v2;
|
|
|
|
|
const float start_v2 = std::min(block.feedrate_profile.entry, cruise_v2);
|
|
|
|
|
const float end_v2 = std::min(block.feedrate_profile.exit, cruise_v2);
|
|
|
|
|
// A zero acceleration would put an infinity in the ramp distances; the Marlin helpers guard
|
|
|
|
|
// their divides the same way and fall back to a pure cruise.
|
|
|
|
|
const float inv_two_accel = block.acceleration > 0.0f ? 0.5f / block.acceleration : 0.0f;
|
|
|
|
|
block.feedrate_profile.entry = std::sqrt(start_v2);
|
|
|
|
|
block.feedrate_profile.exit = std::sqrt(end_v2);
|
|
|
|
|
block.trapezoid.cruise_feedrate = std::sqrt(cruise_v2);
|
|
|
|
|
block.trapezoid.accelerate_until = (cruise_v2 - start_v2) * inv_two_accel;
|
|
|
|
|
block.trapezoid.decelerate_after = block.distance - (cruise_v2 - end_v2) * inv_two_accel;
|
|
|
|
|
}
|
|
|
|
|
return flush_count;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, PrintEstimatedStatistics::ETimeMode mode, size_t keep_last_n_blocks, float additional_time, EMoveType target_move_type, bool is_final)
|
|
|
|
|
{
|
|
|
|
|
if (!enabled)
|
|
|
|
|
return;
|
|
|
|
|
// Orca: on the finalization pass, drain extra time that is still buffered (e.g. a
|
|
|
|
|
// trailing filament change) even with fewer than two blocks queued -- no later pass
|
|
|
|
|
// exists to attribute it on. Every other pass keeps the original >= 2 requirement,
|
|
|
|
|
// and an empty buffer keeps today's early-return unchanged (no behavior change).
|
|
|
|
|
// Drain buffered delays on the final pass, even with no motion left.
|
|
|
|
|
// A trailing filament change has no later move to receive its time.
|
|
|
|
|
const bool drain_final = is_final && !m_additional_time_buffer.empty();
|
|
|
|
|
if (blocks.size() < 2 && !drain_final) {
|
|
|
|
|
if ((klipper ? blocks.empty() : blocks.size() < 2) && !drain_final) {
|
|
|
|
|
// Not enough blocks to attribute the extra time to yet; buffer it so it is
|
|
|
|
|
// applied on a later pass instead of being dropped.
|
|
|
|
|
if (additional_time > 0.0f)
|
|
|
|
@@ -510,7 +587,7 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert(keep_last_n_blocks <= blocks.size());
|
|
|
|
|
assert(klipper || keep_last_n_blocks <= blocks.size());
|
|
|
|
|
|
|
|
|
|
// Merge any previously buffered extra time with this call's extra time. Each
|
|
|
|
|
// entry is applied, in order, to the first block matching its target move type
|
|
|
|
@@ -520,23 +597,36 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
|
|
|
|
|
additional_buffer.emplace_back(target_move_type, additional_time);
|
|
|
|
|
additional_buffer = merge_adjacent_additional_time_blocks(additional_buffer);
|
|
|
|
|
|
|
|
|
|
// reverse_pass
|
|
|
|
|
for (int i = static_cast<int>(blocks.size()) - 1; i > 0; --i) {
|
|
|
|
|
planner_reverse_pass_kernel(blocks[i - 1], blocks[i]);
|
|
|
|
|
size_t n_blocks_process;
|
|
|
|
|
if (klipper) {
|
|
|
|
|
n_blocks_process = plan_klipper(keep_last_n_blocks != 0);
|
|
|
|
|
keep_last_n_blocks = blocks.size() - n_blocks_process;
|
|
|
|
|
} else {
|
|
|
|
|
for (int i = static_cast<int>(blocks.size()) - 1; i > 0; --i)
|
|
|
|
|
planner_reverse_pass_kernel(blocks[i - 1], blocks[i]);
|
|
|
|
|
for (size_t i = 0; i + 1 < blocks.size(); ++i)
|
|
|
|
|
planner_forward_pass_kernel(blocks[i], blocks[i + 1]);
|
|
|
|
|
recalculate_trapezoids(blocks);
|
|
|
|
|
n_blocks_process = blocks.size() - keep_last_n_blocks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// forward_pass
|
|
|
|
|
for (size_t i = 0; i + 1 < blocks.size(); ++i) {
|
|
|
|
|
planner_forward_pass_kernel(blocks[i], blocks[i + 1]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
recalculate_trapezoids(blocks);
|
|
|
|
|
|
|
|
|
|
const size_t n_blocks_process = blocks.size() - keep_last_n_blocks;
|
|
|
|
|
size_t additional_buffer_idx = 0;
|
|
|
|
|
for (size_t i = 0; i < n_blocks_process; ++i) {
|
|
|
|
|
const TimeBlock& block = blocks[i];
|
|
|
|
|
float block_time = block.time();
|
|
|
|
|
double motion_time;
|
|
|
|
|
if (klipper && block.trapezoid.cruise_feedrate > 0.0f) {
|
|
|
|
|
// Integrate using the planned speeds. Reconstructing the exit speed
|
|
|
|
|
// from rounded deceleration distance loses precision near a stop.
|
|
|
|
|
const double cruise = block.trapezoid.cruise_feedrate;
|
|
|
|
|
motion_time = block.distance / cruise;
|
|
|
|
|
if (block.acceleration > 0.0f) {
|
|
|
|
|
const double accel_delta = cruise - block.feedrate_profile.entry;
|
|
|
|
|
const double decel_delta = cruise - block.feedrate_profile.exit;
|
|
|
|
|
motion_time += (sqr(accel_delta) + sqr(decel_delta)) / (2.0 * block.acceleration * cruise);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
motion_time = block.time();
|
|
|
|
|
}
|
|
|
|
|
double block_time = motion_time;
|
|
|
|
|
if (additional_buffer_idx < additional_buffer.size()) {
|
|
|
|
|
const EMoveType buf_move_type = additional_buffer[additional_buffer_idx].first;
|
|
|
|
|
if (buf_move_type == EMoveType::Noop || buf_move_type == block.move_type) {
|
|
|
|
@@ -545,13 +635,13 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
time += double(block_time);
|
|
|
|
|
time += block_time;
|
|
|
|
|
// Orca: accumulate per-SkipType time spent inside SKIPPABLE regions, folded into this single
|
|
|
|
|
// calculate_time write site. Fires fleet-wide (the shipping time_lapse_gcode template stamps
|
|
|
|
|
// blocks stTimelapse), but writes only skippable_part_time, which has no g-code-emitting
|
|
|
|
|
// reader until the pre-heat injector consumes it, so output is byte-identical.
|
|
|
|
|
if (block.skippable_type != SkipType::stNone)
|
|
|
|
|
result.skippable_part_time[block.skippable_type] += block.time();
|
|
|
|
|
result.skippable_part_time[block.skippable_type] += motion_time;
|
|
|
|
|
result.moves[block.move_id].time[static_cast<size_t>(mode)] = block_time;
|
|
|
|
|
gcode_time.cache += block_time;
|
|
|
|
|
//BBS
|
|
|
|
@@ -689,14 +779,12 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
|
|
|
|
|
if (keep_last_n_blocks) {
|
|
|
|
|
blocks.erase(blocks.begin(), blocks.begin() + n_blocks_process);
|
|
|
|
|
|
|
|
|
|
// Ensure that the new first block's entry speed will be preserved to prevent discontinuity
|
|
|
|
|
// between the erased blocks' exit speed and the new first block's entry speed.
|
|
|
|
|
// Otherwise, the first block's entry speed could be recalculated on the next pass without
|
|
|
|
|
// considering that there are no more blocks before this first block. This could lead
|
|
|
|
|
// to discontinuity between the exit speed (of already processed blocks) and the entry
|
|
|
|
|
// speed of the first block.
|
|
|
|
|
TimeBlock &first_block = blocks.front();
|
|
|
|
|
first_block.max_entry_speed = first_block.feedrate_profile.entry;
|
|
|
|
|
// Preserve the boundary speed in the Marlin planner. Klipper only flushes
|
|
|
|
|
// moves whose cruise speed is already independent of future moves.
|
|
|
|
|
if (!klipper) {
|
|
|
|
|
TimeBlock& first_block = blocks.front();
|
|
|
|
|
first_block.max_entry_speed = first_block.feedrate_profile.entry;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
blocks.clear();
|
|
|
|
|
}
|
|
|
|
@@ -3872,9 +3960,9 @@ void GCodeProcessor::finalize(bool post_process)
|
|
|
|
|
update_slice_warnings();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float GCodeProcessor::get_time(PrintEstimatedStatistics::ETimeMode mode) const
|
|
|
|
|
double GCodeProcessor::get_time(PrintEstimatedStatistics::ETimeMode mode) const
|
|
|
|
|
{
|
|
|
|
|
return (mode < PrintEstimatedStatistics::ETimeMode::Count) ? float(m_time_processor.machines[static_cast<size_t>(mode)].time) : 0.0f;
|
|
|
|
|
return (mode < PrintEstimatedStatistics::ETimeMode::Count) ? m_time_processor.machines[static_cast<size_t>(mode)].time : 0.0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
float GCodeProcessor::get_prepare_time(PrintEstimatedStatistics::ETimeMode mode) const
|
|
|
|
@@ -5031,6 +5119,14 @@ void GCodeProcessor::process_G1(const GCodeReader::GCodeLine& line, const std::o
|
|
|
|
|
if (line.has_e()) g1_axes[E] = (double)line.e();
|
|
|
|
|
std::optional<double> g1_feedrate = std::nullopt;
|
|
|
|
|
if (line.has_f()) g1_feedrate = (double)line.f();
|
|
|
|
|
if (m_flavor == gcfKlipper) {
|
|
|
|
|
// Preserve decimal coordinates for short segments without parsing twice.
|
|
|
|
|
for (unsigned char axis = X; axis <= E; ++axis)
|
|
|
|
|
if (line.has(static_cast<Axis>(axis)))
|
|
|
|
|
g1_axes[axis] = line.precise_value(static_cast<Axis>(axis));
|
|
|
|
|
if (line.has_f())
|
|
|
|
|
g1_feedrate = line.precise_value(F);
|
|
|
|
|
}
|
|
|
|
|
process_G1(g1_axes, g1_feedrate);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -5228,7 +5324,7 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
|
|
|
|
|
|
|
|
|
//BBS: limite the cruise according to centripetal acceleration
|
|
|
|
|
//Only need to handle when both prev and curr segment has movement in x-y plane
|
|
|
|
|
if ((prev.exit_direction(0) != 0.0f || prev.exit_direction(1) != 0.0f) &&
|
|
|
|
|
if (m_flavor != gcfKlipper && (prev.exit_direction(0) != 0.0f || prev.exit_direction(1) != 0.0f) &&
|
|
|
|
|
(curr.enter_direction(0) != 0.0f || curr.enter_direction(1) != 0.0f)) {
|
|
|
|
|
Vec3f v1 = prev.exit_direction;
|
|
|
|
|
v1(2, 0) = 0.0f;
|
|
|
|
@@ -5252,6 +5348,15 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_flavor == gcfKlipper && !is_extrusion_only_move(delta_pos)) {
|
|
|
|
|
const float max_velocity = get_axis_max_feedrate(static_cast<PrintEstimatedStatistics::ETimeMode>(i), X, m_machine_config_idx);
|
|
|
|
|
if (max_velocity > 0.0f)
|
|
|
|
|
curr.feedrate = std::min(curr.feedrate, max_velocity);
|
|
|
|
|
}
|
|
|
|
|
// Klipper applies E limits to retractions and moves without XY motion,
|
|
|
|
|
// not to positive extrusion along a printing path.
|
|
|
|
|
const bool limit_extruder = m_flavor != gcfKlipper || delta_pos[E] < 0.0 ||
|
|
|
|
|
(delta_pos[X] == 0.0 && delta_pos[Y] == 0.0);
|
|
|
|
|
// calculates block cruise feedrate
|
|
|
|
|
float min_feedrate_factor = 1.0f;
|
|
|
|
|
for (unsigned char a = X; a <= E; ++a) {
|
|
|
|
@@ -5260,7 +5365,8 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
|
|
|
|
curr.axis_feedrate[a] *= machine.extrude_factor_override_percentage;
|
|
|
|
|
|
|
|
|
|
curr.abs_axis_feedrate[a] = std::abs(curr.axis_feedrate[a]);
|
|
|
|
|
if (curr.abs_axis_feedrate[a] != 0.0f) {
|
|
|
|
|
if (curr.abs_axis_feedrate[a] != 0.0f && (a != E || limit_extruder) &&
|
|
|
|
|
(m_flavor != gcfKlipper || a == Z || a == E)) {
|
|
|
|
|
float axis_max_feedrate = get_axis_max_feedrate(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a), m_machine_config_idx);
|
|
|
|
|
if (axis_max_feedrate != 0.0f) min_feedrate_factor = std::min<float>(min_feedrate_factor, axis_max_feedrate / curr.abs_axis_feedrate[a]);
|
|
|
|
|
}
|
|
|
|
@@ -5277,20 +5383,42 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// calculates block acceleration
|
|
|
|
|
float acceleration =
|
|
|
|
|
(type == EMoveType::Travel) ? get_travel_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
|
|
|
|
|
(is_extrusion_only_move(delta_pos) ?
|
|
|
|
|
get_retract_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
|
|
|
|
|
get_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)));
|
|
|
|
|
float acceleration;
|
|
|
|
|
if (m_flavor == gcfKlipper && is_extrusion_only_move(delta_pos)) {
|
|
|
|
|
// Klipper's max_extrude_only_accel. machine_max_acceleration_e is optional and some
|
|
|
|
|
// shipped profiles leave it at 0, so keep the non-zero retract fallback.
|
|
|
|
|
acceleration = get_axis_max_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), E, m_machine_config_idx);
|
|
|
|
|
if (acceleration <= 0.0f)
|
|
|
|
|
acceleration = get_retract_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i));
|
|
|
|
|
} else
|
|
|
|
|
acceleration =
|
|
|
|
|
(m_flavor == gcfKlipper) ? get_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
|
|
|
|
|
(type == EMoveType::Travel) ? get_travel_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
|
|
|
|
|
(is_extrusion_only_move(delta_pos) ?
|
|
|
|
|
get_retract_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
|
|
|
|
|
get_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)));
|
|
|
|
|
|
|
|
|
|
//BBS
|
|
|
|
|
for (unsigned char a = X; a <= E; ++a) {
|
|
|
|
|
if ((a == E && !limit_extruder) || (m_flavor == gcfKlipper && (a == X || a == Y)))
|
|
|
|
|
continue;
|
|
|
|
|
float axis_max_acceleration = get_axis_max_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a), m_machine_config_idx);
|
|
|
|
|
// An unset (0) limit means "no limit", as in the feedrate loop above. Only the Klipper
|
|
|
|
|
// planner divides by the block acceleration, so leave the Marlin path as it was.
|
|
|
|
|
if (m_flavor == gcfKlipper && axis_max_acceleration <= 0.0f)
|
|
|
|
|
continue;
|
|
|
|
|
if (acceleration * std::abs(delta_pos[a]) * inv_distance > axis_max_acceleration)
|
|
|
|
|
acceleration = axis_max_acceleration / (std::abs(delta_pos[a]) * inv_distance);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
block.acceleration = acceleration;
|
|
|
|
|
if (m_flavor == gcfKlipper) {
|
|
|
|
|
block.junction_deviation = get_junction_deviation(static_cast<PrintEstimatedStatistics::ETimeMode>(i), acceleration);
|
|
|
|
|
const float cruise_acceleration = machine.requested_accel_to_decel >= 0.0f ?
|
|
|
|
|
std::min(machine.acceleration, machine.requested_accel_to_decel) :
|
|
|
|
|
machine.acceleration * (1.0f - machine.minimum_cruise_ratio);
|
|
|
|
|
block.mcr_delta_v2 = 2.0f * distance * std::min(acceleration, cruise_acceleration);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
|
|
|
|
|
const bool has_prev_move = !blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD;
|
|
|
|
@@ -5401,6 +5529,12 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
|
|
|
|
block.feedrate_profile.entry = std::min(vmax_junction, v_allowable);
|
|
|
|
|
|
|
|
|
|
block.max_entry_speed = vmax_junction;
|
|
|
|
|
if (m_flavor == gcfKlipper && has_prev_move) {
|
|
|
|
|
const TimeBlock& previous = blocks.back();
|
|
|
|
|
block.max_entry_speed = std::min(block.max_entry_speed,
|
|
|
|
|
std::sqrt(sqr(previous.max_entry_speed) + 2.0f * previous.distance * previous.acceleration));
|
|
|
|
|
block.max_mcr_entry_speed_sqr = std::min(sqr(block.max_entry_speed), previous.max_mcr_entry_speed_sqr + previous.mcr_delta_v2);
|
|
|
|
|
}
|
|
|
|
|
block.flags.nominal_length = (block.feedrate_profile.cruise <= v_allowable);
|
|
|
|
|
block.flags.recalculate = true;
|
|
|
|
|
block.safe_feedrate = curr.safe_feedrate;
|
|
|
|
@@ -5412,9 +5546,12 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
|
|
|
|
|
prev = curr;
|
|
|
|
|
|
|
|
|
|
blocks.push_back(block);
|
|
|
|
|
if (m_flavor == gcfKlipper && blocks.size() > 1)
|
|
|
|
|
machine.klipper_junction_flush -= distance / curr.feedrate;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (m_time_processor.machines[0].blocks.size() > TimeProcessor::Planner::refresh_threshold)
|
|
|
|
|
if (m_flavor == gcfKlipper ? m_time_processor.machines[0].klipper_junction_flush <= 0.0f :
|
|
|
|
|
m_time_processor.machines[0].blocks.size() > TimeProcessor::Planner::refresh_threshold)
|
|
|
|
|
calculate_time(m_result, TimeProcessor::Planner::queue_size);
|
|
|
|
|
|
|
|
|
|
const Vec3f plate_offset = {(float) m_x_offset, (float) m_y_offset, 0.0f};
|
|
|
|
@@ -6341,6 +6478,8 @@ void GCodeProcessor::process_M108(const GCodeReader::GCodeLine& line)
|
|
|
|
|
|
|
|
|
|
void GCodeProcessor::process_M109(const GCodeReader::GCodeLine& line)
|
|
|
|
|
{
|
|
|
|
|
if (m_flavor == gcfKlipper)
|
|
|
|
|
simulate_st_synchronize();
|
|
|
|
|
int filament_id = get_filament_id();
|
|
|
|
|
float new_temp;
|
|
|
|
|
if (line.has_value('R', new_temp)) {
|
|
|
|
@@ -6403,6 +6542,8 @@ void GCodeProcessor::process_M140(const GCodeReader::GCodeLine& line)
|
|
|
|
|
|
|
|
|
|
void GCodeProcessor::process_M190(const GCodeReader::GCodeLine& line)
|
|
|
|
|
{
|
|
|
|
|
if (m_flavor == gcfKlipper)
|
|
|
|
|
simulate_st_synchronize();
|
|
|
|
|
float new_temp;
|
|
|
|
|
if (line.has_value('S', new_temp))
|
|
|
|
|
m_highest_bed_temp = m_highest_bed_temp < (int)new_temp ? (int)new_temp : m_highest_bed_temp;
|
|
|
|
@@ -6480,6 +6621,21 @@ void GCodeProcessor::process_M203(const GCodeReader::GCodeLine& line)
|
|
|
|
|
void GCodeProcessor::process_M204(const GCodeReader::GCodeLine& line)
|
|
|
|
|
{
|
|
|
|
|
float value;
|
|
|
|
|
if (m_flavor == gcfKlipper) {
|
|
|
|
|
if (!line.has_value('S', value)) {
|
|
|
|
|
float travel;
|
|
|
|
|
if (!line.has_value('P', value) || !line.has_value('T', travel))
|
|
|
|
|
return;
|
|
|
|
|
value = std::min(value, travel);
|
|
|
|
|
}
|
|
|
|
|
// Klipper replaces its single live max_accel for print and travel; the profile-clamping
|
|
|
|
|
// setters would diverge from the firmware when custom G-code raises the limit.
|
|
|
|
|
if (value > 0.0f) {
|
|
|
|
|
for (auto& machine : m_time_processor.machines)
|
|
|
|
|
machine.acceleration = machine.travel_acceleration = value;
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
|
|
|
|
|
if (static_cast<PrintEstimatedStatistics::ETimeMode>(i) == PrintEstimatedStatistics::ETimeMode::Normal ||
|
|
|
|
|
m_time_processor.machine_envelope_processing_enabled) {
|
|
|
|
@@ -6541,50 +6697,50 @@ void GCodeProcessor::process_M205(const GCodeReader::GCodeLine& line)
|
|
|
|
|
|
|
|
|
|
void GCodeProcessor::process_SET_VELOCITY_LIMIT(const GCodeReader::GCodeLine& line)
|
|
|
|
|
{
|
|
|
|
|
// handle SQUARE_CORNER_VELOCITY
|
|
|
|
|
static const std::regex square_corner_velocity_pattern("\\sSQUARE_CORNER_VELOCITY\\s*=\\s*([0-9]*\\.*[0-9]*)");
|
|
|
|
|
std::smatch matches;
|
|
|
|
|
if (std::regex_search(line.raw(), matches, square_corner_velocity_pattern) && matches.size() == 2) {
|
|
|
|
|
float _jerk = 0;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
_jerk = std::stof(matches[1]);
|
|
|
|
|
static const std::regex parameter_pattern(
|
|
|
|
|
R"(\s(VELOCITY|ACCEL|ACCEL_TO_DECEL|SQUARE_CORNER_VELOCITY|MINIMUM_CRUISE_RATIO)\s*=\s*([+]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?)(?=\s|;|$))",
|
|
|
|
|
std::regex::icase);
|
|
|
|
|
const std::string command = line.raw().substr(0, line.raw().find(';'));
|
|
|
|
|
std::vector<std::pair<std::string, float>> parameters;
|
|
|
|
|
for (std::sregex_iterator it(command.begin(), command.end(), parameter_pattern), end; it != end; ++it) {
|
|
|
|
|
float value;
|
|
|
|
|
try {
|
|
|
|
|
value = std::stof((*it)[2].str());
|
|
|
|
|
} catch (const std::exception&) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
catch (...){}
|
|
|
|
|
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
|
|
|
|
|
set_option_value(m_time_processor.machine_limits.machine_max_jerk_x, i, _jerk);
|
|
|
|
|
set_option_value(m_time_processor.machine_limits.machine_max_jerk_y, i, _jerk);
|
|
|
|
|
if (std::isfinite(value))
|
|
|
|
|
parameters.emplace_back(boost::to_upper_copy((*it)[1].str()), value);
|
|
|
|
|
}
|
|
|
|
|
// Klipper 0.13 converts ACCEL_TO_DECEL only when the line has no MINIMUM_CRUISE_RATIO, and later
|
|
|
|
|
// releases ignore it, so a line carrying both (as Orca writes) is planned with the cruise ratio.
|
|
|
|
|
const bool has_cruise_ratio = std::any_of(parameters.begin(), parameters.end(),
|
|
|
|
|
[](const auto& parameter) { return parameter.first == "MINIMUM_CRUISE_RATIO" && parameter.second >= 0.0f && parameter.second < 1.0f; });
|
|
|
|
|
for (const auto& [name, value] : parameters) {
|
|
|
|
|
for (size_t i = 0; i < m_time_processor.machines.size(); ++i) {
|
|
|
|
|
TimeMachine& machine = m_time_processor.machines[i];
|
|
|
|
|
// Klipper replaces its single live max_accel for print and travel; the profile-clamping
|
|
|
|
|
// setters would diverge from the firmware when custom G-code raises the limit.
|
|
|
|
|
if (name == "ACCEL" && value > 0.0f)
|
|
|
|
|
machine.acceleration = machine.travel_acceleration = value;
|
|
|
|
|
else if (name == "MINIMUM_CRUISE_RATIO" && value >= 0.0f && value < 1.0f) {
|
|
|
|
|
if (machine.klipper_queue_priming && machine.requested_accel_to_decel >= 0.0f)
|
|
|
|
|
machine.klipper_junction_flush -= 1.0f;
|
|
|
|
|
machine.minimum_cruise_ratio = value;
|
|
|
|
|
machine.requested_accel_to_decel = -1.0f;
|
|
|
|
|
} else if (name == "ACCEL_TO_DECEL" && value > 0.0f && !has_cruise_ratio) {
|
|
|
|
|
if (machine.klipper_queue_priming && machine.requested_accel_to_decel < 0.0f)
|
|
|
|
|
machine.klipper_junction_flush += 1.0f;
|
|
|
|
|
machine.requested_accel_to_decel = value;
|
|
|
|
|
} else if (name == "SQUARE_CORNER_VELOCITY" && value >= 0.0f) {
|
|
|
|
|
set_option_value(m_time_processor.machine_limits.machine_max_jerk_x, i, value);
|
|
|
|
|
set_option_value(m_time_processor.machine_limits.machine_max_jerk_y, i, value);
|
|
|
|
|
} else if (name == "VELOCITY" && value > 0.0f) {
|
|
|
|
|
set_option_value(m_time_processor.machine_limits.machine_max_speed_x, i, value);
|
|
|
|
|
set_option_value(m_time_processor.machine_limits.machine_max_speed_y, i, value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const std::regex accel_pattern("\\sACCEL\\s*=\\s*([0-9]*\\.*[0-9]*)");
|
|
|
|
|
if (std::regex_search(line.raw(), matches, accel_pattern) && matches.size() == 2) {
|
|
|
|
|
float _accl = 0;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
_accl = std::stof(matches[1]);
|
|
|
|
|
}
|
|
|
|
|
catch (...) {}
|
|
|
|
|
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
|
|
|
|
|
set_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), _accl);
|
|
|
|
|
set_travel_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), _accl);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const std::regex velocity_pattern("\\sVELOCITY\\s*=\\s*([0-9]*\\.*[0-9]*)");
|
|
|
|
|
if (std::regex_search(line.raw(), matches, velocity_pattern) && matches.size() == 2) {
|
|
|
|
|
float _speed = 0;
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
_speed = std::stof(matches[1]);
|
|
|
|
|
}
|
|
|
|
|
catch (...) {}
|
|
|
|
|
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
|
|
|
|
|
set_option_value(m_time_processor.machine_limits.machine_max_speed_x, i, _speed);
|
|
|
|
|
set_option_value(m_time_processor.machine_limits.machine_max_speed_y, i, _speed);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GCodeProcessor::process_M221(const GCodeReader::GCodeLine& line)
|
|
|
|
@@ -6621,14 +6777,15 @@ void GCodeProcessor::process_M400(const GCodeReader::GCodeLine& line)
|
|
|
|
|
{
|
|
|
|
|
float value_s = 0.0;
|
|
|
|
|
float value_p = 0.0;
|
|
|
|
|
if (line.has_value('S', value_s) || line.has_value('P', value_p)) {
|
|
|
|
|
value_s += value_p * 0.001;
|
|
|
|
|
// Skip post-print end-gcode dwells so they don't inflate the M73 estimate (see
|
|
|
|
|
// m_skip_end_gcode_delays). Only omits dwell time — no state is updated here.
|
|
|
|
|
if (m_skip_end_gcode_delays)
|
|
|
|
|
return;
|
|
|
|
|
simulate_st_synchronize(value_s);
|
|
|
|
|
}
|
|
|
|
|
const bool has_delay = line.has_value('S', value_s) || line.has_value('P', value_p);
|
|
|
|
|
if (m_flavor != gcfKlipper && !has_delay)
|
|
|
|
|
return;
|
|
|
|
|
value_s += value_p * 0.001;
|
|
|
|
|
// Skip post-print end-gcode dwells so they don't inflate the M73 estimate (see
|
|
|
|
|
// m_skip_end_gcode_delays). Only omits dwell time — no state is updated here.
|
|
|
|
|
if (m_skip_end_gcode_delays && has_delay)
|
|
|
|
|
return;
|
|
|
|
|
simulate_st_synchronize(value_s);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GCodeProcessor::process_M401(const GCodeReader::GCodeLine& line)
|
|
|
|
@@ -7460,12 +7617,12 @@ float GCodeProcessor::get_junction_deviation(PrintEstimatedStatistics::ETimeMode
|
|
|
|
|
{
|
|
|
|
|
const size_t id = static_cast<size_t>(mode);
|
|
|
|
|
|
|
|
|
|
// Klipper has no classic jerk: jd = scv^2 * (sqrt(2) - 1) / max_accel
|
|
|
|
|
// (toolhead.py::_calc_junction_deviation). Passing the block acceleration back in makes it cancel
|
|
|
|
|
// in calc_vmax_junction_deviation(), leaving the identity v == scv at a 90 degree corner.
|
|
|
|
|
// Klipper derives junction deviation from the live toolhead acceleration,
|
|
|
|
|
// before a move is constrained by Z or extruder limits.
|
|
|
|
|
if (m_flavor == gcfKlipper) {
|
|
|
|
|
// machine_max_jerk_x holds the square corner velocity; process_SET_VELOCITY_LIMIT() writes it.
|
|
|
|
|
const float scv = get_option_value(m_time_processor.machine_limits.machine_max_jerk_x, id);
|
|
|
|
|
acceleration = m_time_processor.machines[id].acceleration;
|
|
|
|
|
if (scv <= 0.0f || acceleration <= 0.0f)
|
|
|
|
|
return 0.0f;
|
|
|
|
|
return sqr(scv) * (std::sqrt(2.0f) - 1.0f) / acceleration;
|
|
|
|
@@ -7497,18 +7654,45 @@ float GCodeProcessor::calc_vmax_junction_deviation(const TimeBlock& block, const
|
|
|
|
|
const TimeMachine::State& curr, bool has_prev_move,
|
|
|
|
|
PrintEstimatedStatistics::ETimeMode mode) const
|
|
|
|
|
{
|
|
|
|
|
if (m_flavor == gcfKlipper) {
|
|
|
|
|
if (!has_prev_move || curr.enter_direction.isZero() || prev.exit_direction.isZero())
|
|
|
|
|
return 0.0f;
|
|
|
|
|
const TimeBlock& previous = m_time_processor.machines[static_cast<size_t>(mode)].blocks.back();
|
|
|
|
|
double limit = std::min(sqr(double(block.feedrate_profile.cruise)), sqr(double(prev.feedrate)));
|
|
|
|
|
const double extrude_ratio_change = std::abs(curr.axis_feedrate[E] / curr.feedrate -
|
|
|
|
|
prev.axis_feedrate[E] / prev.feedrate);
|
|
|
|
|
// Klipper's instantaneous_corner_velocity defaults to 1 mm/s.
|
|
|
|
|
// It is independent of machine_max_jerk_e; custom firmware values
|
|
|
|
|
// cannot be inferred from the slicer's jerk setting.
|
|
|
|
|
if (extrude_ratio_change > 0.0f)
|
|
|
|
|
limit = std::min(limit, sqr(1.0 / extrude_ratio_change));
|
|
|
|
|
// Recover directions from the double-precision axis velocities: float
|
|
|
|
|
// unit vectors lose the angle between nearly collinear short segments.
|
|
|
|
|
const Vec3d prev_direction = Vec3d(prev.axis_feedrate[X], prev.axis_feedrate[Y], prev.axis_feedrate[Z]).normalized();
|
|
|
|
|
const Vec3d curr_direction = Vec3d(curr.axis_feedrate[X], curr.axis_feedrate[Y], curr.axis_feedrate[Z]).normalized();
|
|
|
|
|
const double cosine = std::clamp(-prev_direction.dot(curr_direction), -1.0, 1.0);
|
|
|
|
|
const double sin_half = std::sqrt(0.5 * (1.0 - cosine));
|
|
|
|
|
const double cos_half = std::sqrt(0.5 * (1.0 + cosine));
|
|
|
|
|
if (sin_half < 1.0 && cos_half > 0.0) {
|
|
|
|
|
const double radius = sin_half / (1.0 - sin_half);
|
|
|
|
|
const double half_tan = 0.5 * sin_half / cos_half;
|
|
|
|
|
limit = std::min({limit,
|
|
|
|
|
radius * block.junction_deviation * block.acceleration,
|
|
|
|
|
radius * previous.junction_deviation * previous.acceleration,
|
|
|
|
|
half_tan * block.distance * block.acceleration,
|
|
|
|
|
half_tan * previous.distance * previous.acceleration});
|
|
|
|
|
}
|
|
|
|
|
return std::sqrt(limit);
|
|
|
|
|
}
|
|
|
|
|
const float junction_deviation = get_junction_deviation(mode, block.acceleration);
|
|
|
|
|
if (junction_deviation <= 0.0f)
|
|
|
|
|
return -1.0f; // classic jerk machine, the caller keeps its own computation
|
|
|
|
|
if (!has_prev_move)
|
|
|
|
|
return 0.0f; // starts from rest, the planner raises this on the reverse pass
|
|
|
|
|
|
|
|
|
|
// -1 for a straight continuation, +1 for a full reversal. Half angle identity, no acos()/sin().
|
|
|
|
|
// Both vectors are unit length over XYZE, so this really is a cosine: scaling by 1 / distance
|
|
|
|
|
// instead, as PrusaSlicer does, leaves an E term that makes extruding corners look straighter
|
|
|
|
|
// than they are. Marlin normalizes over XYZE for any extruding move (planner.cpp, esteps > 0)
|
|
|
|
|
// and Klipper keeps E out of the cosine entirely (toolhead.py::Move.calc_junction); both agree
|
|
|
|
|
// that the corner is planned by its geometry, and normalizing matches them to within 1e-5.
|
|
|
|
|
// Unit vectors over XYZE keep this a cosine. Scaling by 1 / XYZ distance
|
|
|
|
|
// leaves an E term that makes extruding corners appear straighter.
|
|
|
|
|
// Marlin normalizes over XYZE; the Klipper branch above uses XYZ only.
|
|
|
|
|
float junction_cos_theta = (-prev.jd_unit_vec).dot(curr.jd_unit_vec);
|
|
|
|
|
if (junction_cos_theta > 0.999999f)
|
|
|
|
|
return 0.0f; // the path doubles back, the machine has to stop
|
|
|
|
@@ -7524,8 +7708,8 @@ float GCodeProcessor::calc_vmax_junction_deviation(const TimeBlock& block, const
|
|
|
|
|
float vmax_junction_sqr = (junction_acceleration * junction_deviation * sin_theta_d2) / (1.0f - sin_theta_d2);
|
|
|
|
|
|
|
|
|
|
// Marlin's JD_HANDLE_SMALL_SEGMENTS: a short move through a shallow corner is treated as an arc and
|
|
|
|
|
// capped by the centripetal acceleration it needs. Klipper has no equivalent.
|
|
|
|
|
if (m_flavor != gcfKlipper && block.distance < 1.0f && junction_cos_theta < -0.7071067812f) {
|
|
|
|
|
// capped by the centripetal acceleration it needs.
|
|
|
|
|
if (block.distance < 1.0f && junction_cos_theta < -0.7071067812f) {
|
|
|
|
|
// Fast acos(-t), max. error +-0.033rad. MinMax polynomial by W. Randolph Franklin:
|
|
|
|
|
// https://wrf.ecse.rpi.edu/Research/Short_Notes/arcsin/onlyelem.html
|
|
|
|
|
const float neg = junction_cos_theta < 0.0f ? -1.0f : 1.0f;
|
|
|
|
@@ -7700,6 +7884,7 @@ void GCodeProcessor::calculate_time(GCodeProcessorResult& result, size_t keep_la
|
|
|
|
|
std::vector<TimeMachine::ActualSpeedMove> actual_speed_moves;
|
|
|
|
|
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
|
|
|
|
|
TimeMachine& machine = m_time_processor.machines[i];
|
|
|
|
|
machine.klipper = m_flavor == gcfKlipper;
|
|
|
|
|
machine.calculate_time(m_result, static_cast<PrintEstimatedStatistics::ETimeMode>(i), keep_last_n_blocks, additional_time, target_move_type, is_final);
|
|
|
|
|
if (static_cast<PrintEstimatedStatistics::ETimeMode>(i) == PrintEstimatedStatistics::ETimeMode::Normal)
|
|
|
|
|
actual_speed_moves = std::move(machine.actual_speed_moves);
|
|
|
|
@@ -7778,6 +7963,14 @@ void GCodeProcessor::calculate_time(GCodeProcessorResult& result, size_t keep_la
|
|
|
|
|
void GCodeProcessor::simulate_st_synchronize(float additional_time, EMoveType target_move_type)
|
|
|
|
|
{
|
|
|
|
|
calculate_time(m_result, 0, additional_time, target_move_type);
|
|
|
|
|
if (m_flavor == gcfKlipper) {
|
|
|
|
|
// ToolHead overrides the normal look-ahead window with BUFFER_TIME_HIGH
|
|
|
|
|
// at startup and after synchronization: 2 s on legacy, 1 s on modern Klipper.
|
|
|
|
|
for (TimeMachine& machine : m_time_processor.machines) {
|
|
|
|
|
machine.klipper_junction_flush = machine.requested_accel_to_decel >= 0.0f ? 2.0f : 1.0f;
|
|
|
|
|
machine.klipper_queue_priming = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GCodeProcessor::update_estimated_times_stats()
|
|
|
|
@@ -7812,7 +8005,7 @@ double GCodeProcessor::extract_absolute_position_on_axis(Axis axis, const GCodeR
|
|
|
|
|
is_relative |= (m_e_local_positioning_type == EPositioningType::Relative);
|
|
|
|
|
|
|
|
|
|
const double lengthsScaleFactor = (m_units == EUnits::Inches) ? double(INCHES_TO_MM) : 1.0;
|
|
|
|
|
double ret = line.value(Slic3r::Axis(axis)) * lengthsScaleFactor;
|
|
|
|
|
double ret = line.precise_value(Slic3r::Axis(axis)) * lengthsScaleFactor;
|
|
|
|
|
// if (axis == E && m_use_volumetric_e)
|
|
|
|
|
// ret /= area_filament_cross_section;
|
|
|
|
|
return is_relative ? m_start_position[axis] + ret : m_origin[axis] + ret;
|
|
|
|
|