From 679794d3c3b61352ac076555087ba4b5a35209b3 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Sat, 23 Aug 2025 17:46:25 +0800 Subject: [PATCH] Update code structure to match BBS --- src/libslic3r/GCode/GCodeProcessor.cpp | 1600 ++++++++++++------------ src/libslic3r/GCode/GCodeProcessor.hpp | 1 + 2 files changed, 805 insertions(+), 796 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index ab2dce0ca4..ce4cec180c 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -419,6 +419,810 @@ void GCodeProcessor::TimeProcessor::reset() machines[static_cast(PrintEstimatedStatistics::ETimeMode::Normal)].enabled = true; } +#if __has_include() + template + struct is_from_chars_convertible : std::false_type {}; + template + struct is_from_chars_convertible(), std::declval(), std::declval()))>> : std::true_type {}; +#endif + +// Returns true if the number was parsed correctly into out and the number spanned the whole input string. +template +[[nodiscard]] static inline bool parse_number(const std::string_view sv, T &out) +{ + // https://www.bfilipek.com/2019/07/detect-overload-from-chars.html#example-stdfromchars +#if __has_include() + // Visual Studio 19 supports from_chars all right. + // OSX compiler that we use only implements std::from_chars just for ints. + // GCC that we compile on does not provide at all. + if constexpr (is_from_chars_convertible::value) { + auto str_end = sv.data() + sv.size(); + auto [end_ptr, error_code] = std::from_chars(sv.data(), str_end, out); + return error_code == std::errc() && end_ptr == str_end; + } + else +#endif + { + // Legacy conversion, which is costly due to having to make a copy of the string before conversion. + try { + assert(sv.size() < 1024); + assert(sv.data() != nullptr); + std::string str { sv }; + size_t read = 0; + if constexpr (std::is_same_v) + out = std::stoi(str, &read); + else if constexpr (std::is_same_v) + out = std::stol(str, &read); + else if constexpr (std::is_same_v) + out = string_to_double_decimal_point(str, &read); + else if constexpr (std::is_same_v) + out = string_to_double_decimal_point(str, &read); + return str.size() == read; + } catch (...) { + return false; + } + } +} + +// Helper class to modify and export gcode to file +class ExportLines +{ +public: + struct Backtrace + { + float time{60.0f}; + int steps{10}; + float time_step() const { return time / float(steps); } + }; + + enum class EWriteType { BySize, ByTime }; + +private: + static void update_lines_ends_and_out_file_pos(const std::string& out_string, std::vector& lines_ends, size_t* out_file_pos) + { + for (size_t i = 0; i < out_string.size(); ++i) { + if (out_string[i] == '\n') + lines_ends.emplace_back((out_file_pos != nullptr) ? *out_file_pos + i + 1 : i + 1); + } + if (out_file_pos != nullptr) + *out_file_pos += out_string.size(); + } + + struct LineData + { + std::string line; + std::array(PrintEstimatedStatistics::ETimeMode::Count)> times{0.0f, 0.0f}; + }; + + enum ETimeMode { + Normal = static_cast(PrintEstimatedStatistics::ETimeMode::Normal), + Stealth = static_cast(PrintEstimatedStatistics::ETimeMode::Stealth) + }; + +#ifndef NDEBUG + class Statistics + { + ExportLines& m_parent; + size_t m_max_size{0}; + size_t m_lines_count{0}; + size_t m_max_lines_count{0}; + + public: + explicit Statistics(ExportLines& parent) : m_parent(parent) {} + + void add_line(size_t line_size) + { + ++m_lines_count; + m_max_size = std::max(m_max_size, m_parent.get_size() + line_size); + m_max_lines_count = std::max(m_max_lines_count, m_lines_count); + } + + void remove_line() { --m_lines_count; } + void remove_all_lines() { m_lines_count = 0; } + }; + + Statistics m_statistics; +#endif // NDEBUG + + EWriteType m_write_type{EWriteType::BySize}; + // Time machines containing g1 times cache + const std::array(PrintEstimatedStatistics::ETimeMode::Count)>& m_machines; + // Current time + std::array(PrintEstimatedStatistics::ETimeMode::Count)> m_times{0.0f, 0.0f}; + // Current size in bytes + size_t m_size{0}; + + // gcode lines cache + std::deque m_lines; + size_t m_added_lines_counter{0}; + // map of gcode line ids from original to final + // used to update m_result.moves[].gcode_id + std::vector> m_gcode_lines_map; + + size_t m_times_cache_id{0}; + size_t m_out_file_pos{0}; + +public: + ExportLines(EWriteType type, const std::array(PrintEstimatedStatistics::ETimeMode::Count)>& machines) +#ifndef NDEBUG + : m_statistics(*this) + , m_write_type(type) + , m_machines(machines){} +#else + : m_write_type(type), m_machines(machines) + {} +#endif // NDEBUG + + // return: number of internal G1 lines (from G2/G3 splitting) processed + unsigned int update(const std::string& line, size_t lines_counter, size_t g1_lines_counter) + { + unsigned int ret = 0; + m_gcode_lines_map.push_back({lines_counter, 0}); + + if (GCodeReader::GCodeLine::cmd_is(line, "G0") || GCodeReader::GCodeLine::cmd_is(line, "G1") || + GCodeReader::GCodeLine::cmd_is(line, "G2") || GCodeReader::GCodeLine::cmd_is(line, "G3") || + GCodeReader::GCodeLine::cmd_is(line, "G28")) + ++g1_lines_counter; + else + return ret; + + auto init_it = m_machines[Normal].g1_times_cache.begin() + m_times_cache_id; + auto it = init_it; + while (it != m_machines[Normal].g1_times_cache.end() && it->id < g1_lines_counter) { + ++it; + ++m_times_cache_id; + } + + if (it == m_machines[Normal].g1_times_cache.end() || it->id > g1_lines_counter) + return ret; + + // search for internal G1 lines + if (GCodeReader::GCodeLine::cmd_is(line, "G2") || GCodeReader::GCodeLine::cmd_is(line, "G3")) { + while (it != m_machines[Normal].g1_times_cache.end() && it->remaining_internal_g1_lines > 0) { + ++it; + ++m_times_cache_id; + ++g1_lines_counter; + ++ret; + } + } + + if (it != m_machines[Normal].g1_times_cache.end() && it->id == g1_lines_counter) { + m_times[Normal] = it->elapsed_time; + if (!m_machines[Stealth].g1_times_cache.empty()) + m_times[Stealth] = (m_machines[Stealth].g1_times_cache.begin() + + std::distance(m_machines[Normal].g1_times_cache.begin(), it)) + ->elapsed_time; + } + + return ret; + } + + // add the given gcode line to the cache + void append_line(const std::string& line, const bool ignore_from_move = false) + { + if (line.empty()) + return; + + m_lines.push_back({line, m_times}); +#ifndef NDEBUG + m_statistics.add_line(line.length()); +#endif // NDEBUG + m_size += line.length(); + ++m_added_lines_counter; + if (!ignore_from_move) { + assert(!m_gcode_lines_map.empty()); + m_gcode_lines_map.back().second = m_added_lines_counter; + } + } + + // Insert the gcode lines required by the command cmd by backtracing into the cache + void insert_lines(const Backtrace& backtrace, + const std::string& cmd, + std::function&)> line_inserter, + std::function line_replacer) + { + // Orca: find start pos by seaching G28/G29/PRINT_START/START_PRINT commands + auto is_start_pos = [](const std::string& curr_cmd) { + return boost::iequals(curr_cmd, "G28") || boost::iequals(curr_cmd, "G29") || boost::iequals(curr_cmd, "PRINT_START") || + boost::iequals(curr_cmd, "START_PRINT"); + }; + assert(!m_lines.empty()); + const float time_step = backtrace.time_step(); + size_t rev_it_dist = 0; // distance from the end of the cache of the starting point of the backtrace + float last_time_insertion = 0.0f; // used to avoid inserting two lines at the same time + for (int i = 0; i < backtrace.steps; ++i) { + const float backtrace_time_i = (i + 1) * time_step; + const float time_threshold_i = m_times[Normal] - backtrace_time_i; + auto rev_it = m_lines.rbegin() + rev_it_dist; + auto start_rev_it = rev_it; + + std::string curr_cmd = GCodeReader::GCodeLine::extract_cmd(rev_it->line); + // backtrace into the cache to find the place where to insert the line + while (rev_it != m_lines.rend() && rev_it->times[Normal] > time_threshold_i && curr_cmd != cmd && !is_start_pos(curr_cmd)) { + rev_it->line = line_replacer(rev_it->line); + ++rev_it; + if (rev_it != m_lines.rend()) + curr_cmd = GCodeReader::GCodeLine::extract_cmd(rev_it->line); + } + + // we met the previous evenience of cmd, or the start position, stop inserting lines + if (rev_it != m_lines.rend() && (curr_cmd == cmd || is_start_pos(curr_cmd))) + break; + + // insert the line for the current step + if (rev_it != m_lines.rend() && rev_it != start_rev_it && rev_it->times[Normal] != last_time_insertion) { + last_time_insertion = rev_it->times[Normal]; + std::vector time_diffs; + time_diffs.push_back(m_times[Normal] - last_time_insertion); + if (!m_machines[Stealth].g1_times_cache.empty()) + time_diffs.push_back(m_times[Stealth] - rev_it->times[Stealth]); + const std::string out_line = line_inserter(i + 1, time_diffs); + rev_it_dist = std::distance(m_lines.rbegin(), rev_it) + 1; + m_lines.insert(rev_it.base(), {out_line, rev_it->times}); +#ifndef NDEBUG + m_statistics.add_line(out_line.length()); +#endif // NDEBUG + m_size += out_line.length(); + // synchronize gcode lines map + for (auto map_it = m_gcode_lines_map.rbegin(); map_it != m_gcode_lines_map.rbegin() + rev_it_dist - 1; ++map_it) { + ++map_it->second; + } + + ++m_added_lines_counter; + } + } + } + + // write to file: + // m_write_type == EWriteType::ByTime - all lines older than m_time - backtrace_time + // m_write_type == EWriteType::BySize - all lines if current size is greater than 65535 bytes + void write(FilePtr& out, float backtrace_time, GCodeProcessorResult& result, const std::string& out_path) + { + if (m_lines.empty()) + return; + + // collect lines to write into a single string + std::string out_string; + if (!m_lines.empty()) { + if (m_write_type == EWriteType::ByTime) { + while (m_lines.front().times[Normal] < m_times[Normal] - backtrace_time) { + const LineData& data = m_lines.front(); + out_string += data.line; + m_size -= data.line.length(); + m_lines.pop_front(); +#ifndef NDEBUG + m_statistics.remove_line(); +#endif // NDEBUG + } + } else { + if (m_size > 65535) { + while (!m_lines.empty()) { + out_string += m_lines.front().line; + m_lines.pop_front(); + } + m_size = 0; +#ifndef NDEBUG + m_statistics.remove_all_lines(); +#endif // NDEBUG + } + } + } + + { + write_to_file(out, out_string, result, out_path); + update_lines_ends_and_out_file_pos(out_string, result.lines_ends, &m_out_file_pos); + } + } + + // flush the current content of the cache to file + void flush(FilePtr& out, GCodeProcessorResult& result, const std::string& out_path) + { + // collect lines to flush into a single string + std::string out_string; + while (!m_lines.empty()) { + out_string += m_lines.front().line; + m_lines.pop_front(); + } + m_size = 0; +#ifndef NDEBUG + m_statistics.remove_all_lines(); +#endif // NDEBUG + + { + write_to_file(out, out_string, result, out_path); + update_lines_ends_and_out_file_pos(out_string, result.lines_ends, &m_out_file_pos); + } + } + + void synchronize_moves(GCodeProcessorResult& result) const + { + auto it = m_gcode_lines_map.begin(); + for (GCodeProcessorResult::MoveVertex& move : result.moves) { + while (it != m_gcode_lines_map.end() && it->first < move.gcode_id) { + ++it; + } + if (it != m_gcode_lines_map.end() && it->first == move.gcode_id) + move.gcode_id = it->second; + } + } + + size_t get_size() const { return m_size; } + +private: + void write_to_file(FilePtr& out, const std::string& out_string, GCodeProcessorResult& result, const std::string& out_path) + { + if (!out_string.empty()) { + if (true) { + fwrite((const void*) out_string.c_str(), 1, out_string.length(), out.f); + if (ferror(out.f)) { + out.close(); + boost::nowide::remove(out_path.c_str()); + throw Slic3r::RuntimeError("GCode processor post process export failed.\nIs the disk full?"); + } + } + } + } +}; + +void GCodeProcessor::run_post_process() +{ + FilePtr in{ boost::nowide::fopen(m_result.filename.c_str(), "rb") }; + if (in.f == nullptr) + throw Slic3r::RuntimeError(std::string("GCode processor post process export failed.\nCannot open file for reading.\n")); + + // temporary file to contain modified gcode + std::string out_path = m_result.filename + ".postprocess"; + FilePtr out{ boost::nowide::fopen(out_path.c_str(), "wb") }; + if (out.f == nullptr) + throw Slic3r::RuntimeError(std::string("GCode processor post process export failed.\nCannot open file for writing.\n")); + + std::vector filament_mm(m_result.filaments_count, 0.0); + std::vector filament_cm3(m_result.filaments_count, 0.0); + std::vector filament_g(m_result.filaments_count, 0.0); + std::vector filament_cost(m_result.filaments_count, 0.0); + + double filament_total_g = 0.0; + double filament_total_cost = 0.0; + + for (const auto& [id, volume] : m_result.print_statistics.total_volumes_per_extruder) { + filament_mm[id] = volume / (static_cast(M_PI) * sqr(0.5 * m_result.filament_diameters[id])); + filament_cm3[id] = volume * 0.001; + filament_g[id] = filament_cm3[id] * double(m_result.filament_densities[id]); + filament_cost[id] = filament_g[id] * double(m_result.filament_costs[id]) * 0.001; + filament_total_g += filament_g[id]; + filament_total_cost += filament_cost[id]; + } + + double total_g_wipe_tower = m_print->print_statistics().total_wipe_tower_filament; + + + auto time_in_minutes = [](float time_in_seconds) { + assert(time_in_seconds >= 0.f); + return int((time_in_seconds + 0.5f) / 60.0f); + }; + + auto time_in_last_minute = [](float time_in_seconds) { + assert(time_in_seconds <= 60.0f); + return time_in_seconds / 60.0f; + }; + + auto format_line_M73_main = [this](const std::string& mask, int percent, int time) { + if(this->m_disable_m73) + return std::string(""); + + char line_M73[64]; + sprintf(line_M73, mask.c_str(), + std::to_string(percent).c_str(), + std::to_string(time).c_str()); + return std::string(line_M73); + }; + + auto format_line_M73_stop_int = [this](const std::string& mask, int time) { + if (this->m_disable_m73) + return std::string(""); + char line_M73[64]; + sprintf(line_M73, mask.c_str(), std::to_string(time).c_str()); + return std::string(line_M73); + }; + + auto format_line_exhaust_fan_control = [](const std::string& mask,int fan_index,int percent) { + char line_fan[64] = { 0 }; + sprintf(line_fan,mask.c_str(), + std::to_string(fan_index).c_str(), + std::to_string(int((percent/100.0)*255)).c_str()); + return std::string(line_fan); + }; + + auto format_time_float = [](float time) { + return Slic3r::float_to_string_decimal_point(time, 2); + }; + + auto format_line_M73_stop_float = [format_time_float](const std::string& mask, float time) { + char line_M73[64]; + sprintf(line_M73, mask.c_str(), format_time_float(time).c_str()); + return std::string(line_M73); + }; + + std::string gcode_line; + size_t g1_lines_counter = 0; + // keeps track of last exported pair + std::array, static_cast(PrintEstimatedStatistics::ETimeMode::Count)> last_exported_main; + for (size_t i = 0; i < static_cast(PrintEstimatedStatistics::ETimeMode::Count); ++i) { + last_exported_main[i] = { 0, time_in_minutes(m_time_processor.machines[i].time) }; + } + + // keeps track of last exported remaining time to next printer stop + std::array(PrintEstimatedStatistics::ETimeMode::Count)> last_exported_stop; + for (size_t i = 0; i < static_cast(PrintEstimatedStatistics::ETimeMode::Count); ++i) { + last_exported_stop[i] = time_in_minutes(m_time_processor.machines[i].time); + } + + ExportLines export_line(m_result.backtrace_enabled ? ExportLines::EWriteType::ByTime : ExportLines::EWriteType::BySize, + m_time_processor.machines); + + // replace placeholder lines with the proper final value + // gcode_line is in/out parameter, to reduce expensive memory allocation + auto process_placeholders = [&](std::string& gcode_line) { + bool processed = false; + + // remove trailing '\n' + auto line = std::string_view(gcode_line).substr(0, gcode_line.length() - 1); + + if (line.length() > 1) { + line = line.substr(1); + if (line == reserved_tag(ETags::First_Line_M73_Placeholder) || line == reserved_tag(ETags::Last_Line_M73_Placeholder)) { + for (size_t i = 0; i < static_cast(PrintEstimatedStatistics::ETimeMode::Count); ++i) { + const TimeMachine& machine = m_time_processor.machines[i]; + if (machine.enabled) { + // export pair + export_line.append_line(format_line_M73_main(machine.line_m73_main_mask.c_str(), + (line == reserved_tag(ETags::First_Line_M73_Placeholder)) ? 0 : 100, + (line == reserved_tag(ETags::First_Line_M73_Placeholder)) ? time_in_minutes(machine.time) : 0)); + processed = true; + + // export remaining time to next printer stop + if (line == reserved_tag(ETags::First_Line_M73_Placeholder) && !machine.stop_times.empty()) { + const int to_export_stop = time_in_minutes(machine.stop_times.front().elapsed_time); + export_line.append_line(format_line_M73_stop_int(machine.line_m73_stop_mask.c_str(), to_export_stop)); + last_exported_stop[i] = to_export_stop; + } + } + } + } else if (line == reserved_tag(ETags::Estimated_Printing_Time_Placeholder)) { + for (size_t i = 0; i < static_cast(PrintEstimatedStatistics::ETimeMode::Count); ++i) { + const TimeMachine& machine = m_time_processor.machines[i]; + PrintEstimatedStatistics::ETimeMode mode = static_cast(i); + if (mode == PrintEstimatedStatistics::ETimeMode::Normal || machine.enabled) { + char buf[128]; + if (!s_IsBBLPrinter) + // Orca: compatibility with klipper_estimator + sprintf(buf, "; estimated printing time (%s mode) = %s\n", + (mode == PrintEstimatedStatistics::ETimeMode::Normal) ? "normal" : "silent", + get_time_dhms(machine.time).c_str()); + else { + sprintf(buf, "; model printing time: %s; total estimated time: %s\n", + get_time_dhms(machine.time - machine.prepare_time).c_str(), get_time_dhms(machine.time).c_str()); + } + export_line.append_line(buf); + } + } + for (size_t i = 0; i < static_cast(PrintEstimatedStatistics::ETimeMode::Count); ++i) { + const TimeMachine& machine = m_time_processor.machines[i]; + PrintEstimatedStatistics::ETimeMode mode = static_cast(i); + if (mode == PrintEstimatedStatistics::ETimeMode::Normal || machine.enabled) { + char buf[128]; + sprintf(buf, "; estimated first layer printing time (%s mode) = %s\n", + (mode == PrintEstimatedStatistics::ETimeMode::Normal) ? "normal" : "silent", + get_time_dhms(machine.prepare_time).c_str()); + export_line.append_line(buf); + processed = true; + } + } + } + // Orca: write total layer number, this is used by Bambu printers only as of now + else if (line == reserved_tag(ETags::Total_Layer_Number_Placeholder)) { + char buf[128]; + sprintf(buf, "; total layer number: %u\n", m_layer_id); + export_line.append_line(buf); + processed = true; + } + } + + return processed; + }; + + auto process_used_filament = [&](std::string& gcode_line) { + // Prefilter for parsing speed. + if (gcode_line.size() < 8 || gcode_line[0] != ';' || gcode_line[1] != ' ') + return false; + if (const char c = gcode_line[2]; c != 'f' && c != 't') + return false; + auto process_tag = [](std::string& gcode_line, const std::string_view tag, const std::vector& values) { + if (boost::algorithm::starts_with(gcode_line, tag)) { + gcode_line = tag; + char buf[1024]; + for (size_t i = 0; i < values.size(); ++i) { + sprintf(buf, i == values.size() - 1 ? " %.2lf\n" : " %.2lf,", values[i]); + gcode_line += buf; + } + return true; + } + return false; + }; + + bool ret = false; + ret |= process_tag(gcode_line, PrintStatistics::FilamentUsedMmMask, filament_mm); + ret |= process_tag(gcode_line, PrintStatistics::FilamentUsedGMask, filament_g); + ret |= process_tag(gcode_line, PrintStatistics::TotalFilamentUsedGMask, { filament_total_g }); + ret |= process_tag(gcode_line, PrintStatistics::FilamentUsedCm3Mask, filament_cm3); + ret |= process_tag(gcode_line, PrintStatistics::FilamentCostMask, filament_cost); + ret |= process_tag(gcode_line, PrintStatistics::TotalFilamentCostMask, { filament_total_cost }); + return ret; + }; + + // check for temporary lines + auto is_temporary_decoration = [](const std::string_view gcode_line) { + // remove trailing '\n' + assert(! gcode_line.empty()); + assert(gcode_line.back() == '\n'); + + // return true for decorations which are used in processing the gcode but that should not be exported into the final gcode + // i.e.: + // bool ret = gcode_line.substr(0, gcode_line.length() - 1) == ";" + Layer_Change_Tag; + // ... + // return ret; + return false; + }; + + // Iterators for the normal and silent cached time estimate entry recently processed, used by process_line_G1. + auto g1_times_cache_it = Slic3r::reserve_vector::const_iterator>(m_time_processor.machines.size()); + for (const auto& machine : m_time_processor.machines) + g1_times_cache_it.emplace_back(machine.g1_times_cache.begin()); + + // add lines M73 to exported gcode + auto process_line_move = [ + // Lambdas, mostly for string formatting, all with an empty capture block. + time_in_minutes, format_time_float, format_line_M73_main, format_line_M73_stop_int, format_line_M73_stop_float, time_in_last_minute,format_line_exhaust_fan_control, + &self = std::as_const(m_time_processor), + // Caches, to be modified + &g1_times_cache_it, &last_exported_main, &last_exported_stop, + // String output + &export_line] + (const size_t g1_lines_counter) { + for (size_t i = 0; i < static_cast(PrintEstimatedStatistics::ETimeMode::Count); ++i) { + const TimeMachine& machine = self.machines[i]; + if (machine.enabled) { + // export pair + // Skip all machine.g1_times_cache below g1_lines_counter. + auto& it = g1_times_cache_it[i]; + while (it != machine.g1_times_cache.end() && it->id < g1_lines_counter) + ++it; + if (it != machine.g1_times_cache.end() && it->id == g1_lines_counter) { + std::pair to_export_main = { int(100.0f * it->elapsed_time / machine.time), + time_in_minutes(machine.time - it->elapsed_time) }; + + if (last_exported_main[i] != to_export_main) { + export_line.append_line(format_line_M73_main(machine.line_m73_main_mask.c_str(), + to_export_main.first, to_export_main.second), true); + last_exported_main[i] = to_export_main; + } + // export remaining time to next printer stop + auto it_stop = std::upper_bound(machine.stop_times.begin(), machine.stop_times.end(), it->elapsed_time, + [](float value, const TimeMachine::StopTime& t) { return value < t.elapsed_time; }); + if (it_stop != machine.stop_times.end()) { + int to_export_stop = time_in_minutes(it_stop->elapsed_time - it->elapsed_time); + if (last_exported_stop[i] != to_export_stop) { + if (to_export_stop > 0) { + if (last_exported_stop[i] != to_export_stop) { + export_line.append_line(format_line_M73_stop_int(machine.line_m73_stop_mask.c_str(), to_export_stop), true); + last_exported_stop[i] = to_export_stop; + } + } + else { + bool is_last = false; + auto next_it = it + 1; + is_last |= (next_it == machine.g1_times_cache.end()); + + if (next_it != machine.g1_times_cache.end()) { + auto next_it_stop = std::upper_bound(machine.stop_times.begin(), machine.stop_times.end(), next_it->elapsed_time, + [](float value, const TimeMachine::StopTime& t) { return value < t.elapsed_time; }); + is_last |= (next_it_stop != it_stop); + + std::string time_float_str = format_time_float(time_in_last_minute(it_stop->elapsed_time - it->elapsed_time)); + std::string next_time_float_str = format_time_float(time_in_last_minute(it_stop->elapsed_time - next_it->elapsed_time)); + is_last |= (string_to_double_decimal_point(time_float_str) > 0. && string_to_double_decimal_point(next_time_float_str) == 0.); + } + + if (is_last) { + if (std::distance(machine.stop_times.begin(), it_stop) == static_cast(machine.stop_times.size() - 1)) + export_line.append_line(format_line_M73_stop_int(machine.line_m73_stop_mask.c_str(), to_export_stop), true); + else + export_line.append_line(format_line_M73_stop_float(machine.line_m73_stop_mask.c_str(), time_in_last_minute(it_stop->elapsed_time - it->elapsed_time)), true); + + last_exported_stop[i] = to_export_stop; + } + } + } + } + } + } + } + }; + + // add lines M104 to exported gcode + auto process_line_T = [this, &export_line](const std::string& gcode_line, const size_t g1_lines_counter, const ExportLines::Backtrace& backtrace) { + const std::string cmd = GCodeReader::GCodeLine::extract_cmd(gcode_line); + + int tool_number = -1; + if (!parse_number(std::string_view(cmd).substr(1), tool_number)){ + // invalid T command, such as the "TIMELAPSE_TAKE_FRAME" gcode, just ignore + return; + } + if (cmd.size() >= 2) { + if (tool_number != -1) { + if (tool_number < 0 || (int)m_extruder_temps_config.size() <= tool_number) { + // found an invalid value, clamp it to a valid one + tool_number = std::clamp(0, m_extruder_temps_config.size() - 1, tool_number); + // emit warning + std::string warning = "GCode Post-Processor encountered an invalid toolchange, maybe from a custom gcode:"; + warning += "\n> "; + warning += gcode_line; + warning += "Generated M104 lines may be incorrect."; + BOOST_LOG_TRIVIAL(error) << warning; + // Orca todo + if (m_print != nullptr) + m_print->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, warning); + } + export_line.insert_lines( + backtrace, cmd, + // line inserter + [tool_number, this](unsigned int id, const std::vector& time_diffs) { + const int temperature = int(m_layer_id != 1 ? m_extruder_temps_config[tool_number] : + m_extruder_temps_first_layer_config[tool_number]); + // Orca: M104.1 for XL printers, I can't find the documentation for this so I copied the C++ comments from + // Prusa-Firmware-Buddy here + /** + * M104.1: Early Set Hotend Temperature (preheat, and with stealth mode support) + * + * This GCode is used to tell the XL printer the time estimate when a tool will be used next, + * so that the printer can start preheating the tool in advance. + * + * ## Parameters + * - `P` - - time in seconds till the temperature S is required (in standard mode) + * - `Q` - - time in seconds till the temperature S is required (in stealth mode) + * The rest is same as M104 + */ + if (this->m_is_XL_printer) { + std::string out = "M104.1 T" + std::to_string(tool_number); + if (time_diffs.size() > 0) + out += " P" + std::to_string(int(std::round(time_diffs[0]))); + if (time_diffs.size() > 1) + out += " Q" + std::to_string(int(std::round(time_diffs[1]))); + out += " S" + std::to_string(temperature) + "\n"; + return out; + } else { + std::string comment = "preheat T" + std::to_string(tool_number) + + " time: " + std::to_string((int) std::round(time_diffs[0])) + "s"; + return GCodeWriter::set_temperature(temperature, this->m_flavor, false, tool_number, comment); + } + }, + // line replacer + [this, tool_number](const std::string& line) { + if (GCodeReader::GCodeLine::cmd_is(line, "M104")) { + GCodeReader::GCodeLine gline; + GCodeReader reader; + reader.parse_line(line, [&gline](GCodeReader& reader, const GCodeReader::GCodeLine& l) { gline = l; }); + + float val; + if (gline.has_value('T', val) && gline.raw().find("cooldown") != std::string::npos) { + if (static_cast(val) == tool_number) + return std::string("; removed M104\n"); + } + } + return line; + } + ); + } + } + }; + + m_result.lines_ends.clear(); + // m_result.lines_ends.emplace_back(std::vector()); + + unsigned int line_id = 0; + // Backtrace data for Tx gcode lines + const ExportLines::Backtrace backtrace_T = { m_preheat_time, m_preheat_steps }; + // In case there are multiple sources of backtracing, keeps track of the longest backtrack time needed + // to flush the backtrace cache accordingly + float max_backtrace_time = 120.0f; + + { + // Read the input stream 64kB at a time, extract lines and process them. + std::vector buffer(65536 * 10, 0); + // Line buffer. + assert(gcode_line.empty()); + for (;;) { + size_t cnt_read = ::fread(buffer.data(), 1, buffer.size(), in.f); + if (::ferror(in.f)) + throw Slic3r::RuntimeError(std::string("GCode processor post process export failed.\nError while reading from file.\n")); + bool eof = cnt_read == 0; + auto it = buffer.begin(); + auto it_bufend = buffer.begin() + cnt_read; + while (it != it_bufend || (eof && ! gcode_line.empty())) { + // Find end of line. + bool eol = false; + auto it_end = it; + for (; it_end != it_bufend && ! (eol = *it_end == '\r' || *it_end == '\n'); ++ it_end) ; + // End of line is indicated also if end of file was reached. + eol |= eof && it_end == it_bufend; + gcode_line.insert(gcode_line.end(), it, it_end); + + it = it_end; + // append EOL. + if (it != it_bufend && *it == '\r') { + gcode_line += *it++; + } + if (it != it_bufend && *it == '\n') { + gcode_line += *it++; + } + + if (eol) { + ++line_id; + const unsigned int internal_g1_lines_counter = export_line.update(gcode_line, line_id, g1_lines_counter); + // replace placeholder lines + bool processed = process_placeholders(gcode_line); + if (processed) + gcode_line.clear(); + if (!processed) + processed = process_used_filament(gcode_line); + if (!processed && !is_temporary_decoration(gcode_line)) { + if (GCodeReader::GCodeLine::cmd_is(gcode_line, "G0") || GCodeReader::GCodeLine::cmd_is(gcode_line, "G1")) { + export_line.append_line(gcode_line); + // add lines M73 where needed + process_line_move(g1_lines_counter++); + gcode_line.clear(); + } + else if (GCodeReader::GCodeLine::cmd_is(gcode_line, "G2") || GCodeReader::GCodeLine::cmd_is(gcode_line, "G3")) { + export_line.append_line(gcode_line); + // add lines M73 where needed + process_line_move(g1_lines_counter + internal_g1_lines_counter); + g1_lines_counter += (1 + internal_g1_lines_counter); + gcode_line.clear(); + } + else if (GCodeReader::GCodeLine::cmd_is(gcode_line, "G28")) { + ++g1_lines_counter; + } + else if (m_result.backtrace_enabled && GCodeReader::GCodeLine::cmd_starts_with(gcode_line, "T")) { + // add lines M104 where needed + process_line_T(gcode_line, g1_lines_counter, backtrace_T); + max_backtrace_time = std::max(max_backtrace_time, backtrace_T.time); + } + } + + if (!gcode_line.empty()) + export_line.append_line(gcode_line); + export_line.write(out, 1.1f * max_backtrace_time, m_result, out_path); + gcode_line.clear(); + } + } + if (eof) + break; + } + } + + export_line.flush(out, m_result, out_path); + + out.close(); + in.close(); + + const std::string result_filename = m_result.filename; + export_line.synchronize_moves(m_result); + + if (rename_file(out_path, result_filename)) + throw Slic3r::RuntimeError(std::string("Failed to rename the output G-code file from ") + out_path + " to " + result_filename + '\n' + + "Is " + out_path + " locked?" + '\n'); +} + void GCodeProcessor::UsedFilaments::reset() { color_change_cache = 0.0f; @@ -1905,51 +2709,6 @@ void GCodeProcessor::process_gcode_line(const GCodeReader::GCodeLine& line, bool } } -#if __has_include() - template - struct is_from_chars_convertible : std::false_type {}; - template - struct is_from_chars_convertible(), std::declval(), std::declval()))>> : std::true_type {}; -#endif - -// Returns true if the number was parsed correctly into out and the number spanned the whole input string. -template -[[nodiscard]] static inline bool parse_number(const std::string_view sv, T &out) -{ - // https://www.bfilipek.com/2019/07/detect-overload-from-chars.html#example-stdfromchars -#if __has_include() - // Visual Studio 19 supports from_chars all right. - // OSX compiler that we use only implements std::from_chars just for ints. - // GCC that we compile on does not provide at all. - if constexpr (is_from_chars_convertible::value) { - auto str_end = sv.data() + sv.size(); - auto [end_ptr, error_code] = std::from_chars(sv.data(), str_end, out); - return error_code == std::errc() && end_ptr == str_end; - } - else -#endif - { - // Legacy conversion, which is costly due to having to make a copy of the string before conversion. - try { - assert(sv.size() < 1024); - assert(sv.data() != nullptr); - std::string str { sv }; - size_t read = 0; - if constexpr (std::is_same_v) - out = std::stoi(str, &read); - else if constexpr (std::is_same_v) - out = std::stol(str, &read); - else if constexpr (std::is_same_v) - out = string_to_double_decimal_point(str, &read); - else if constexpr (std::is_same_v) - out = string_to_double_decimal_point(str, &read); - return str.size() == read; - } catch (...) { - return false; - } - } -} - int GCodeProcessor::get_gcode_last_filament(const std::string& gcode_str) { int str_size = gcode_str.size(); @@ -4576,757 +5335,6 @@ void GCodeProcessor::process_filament_change(int id) store_move_vertex(EMoveType::Tool_change); } -static void update_lines_ends_and_out_file_pos(const std::string& out_string, std::vector& lines_ends, size_t* out_file_pos) -{ - for (size_t i = 0; i < out_string.size(); ++i) { - if (out_string[i] == '\n') - lines_ends.emplace_back((out_file_pos != nullptr) ? *out_file_pos + i + 1 : i + 1); - } - if (out_file_pos != nullptr) - *out_file_pos += out_string.size(); -} - -void GCodeProcessor::run_post_process() -{ - FilePtr in{ boost::nowide::fopen(m_result.filename.c_str(), "rb") }; - if (in.f == nullptr) - throw Slic3r::RuntimeError(std::string("GCode processor post process export failed.\nCannot open file for reading.\n")); - - // temporary file to contain modified gcode - std::string out_path = m_result.filename + ".postprocess"; - FilePtr out{ boost::nowide::fopen(out_path.c_str(), "wb") }; - if (out.f == nullptr) - throw Slic3r::RuntimeError(std::string("GCode processor post process export failed.\nCannot open file for writing.\n")); - - std::vector filament_mm(m_result.filaments_count, 0.0); - std::vector filament_cm3(m_result.filaments_count, 0.0); - std::vector filament_g(m_result.filaments_count, 0.0); - std::vector filament_cost(m_result.filaments_count, 0.0); - - double filament_total_g = 0.0; - double filament_total_cost = 0.0; - - for (const auto& [id, volume] : m_result.print_statistics.total_volumes_per_extruder) { - filament_mm[id] = volume / (static_cast(M_PI) * sqr(0.5 * m_result.filament_diameters[id])); - filament_cm3[id] = volume * 0.001; - filament_g[id] = filament_cm3[id] * double(m_result.filament_densities[id]); - filament_cost[id] = filament_g[id] * double(m_result.filament_costs[id]) * 0.001; - filament_total_g += filament_g[id]; - filament_total_cost += filament_cost[id]; - } - - double total_g_wipe_tower = m_print->print_statistics().total_wipe_tower_filament; - - - auto time_in_minutes = [](float time_in_seconds) { - assert(time_in_seconds >= 0.f); - return int((time_in_seconds + 0.5f) / 60.0f); - }; - - auto time_in_last_minute = [](float time_in_seconds) { - assert(time_in_seconds <= 60.0f); - return time_in_seconds / 60.0f; - }; - - auto format_line_M73_main = [this](const std::string& mask, int percent, int time) { - if(this->m_disable_m73) - return std::string(""); - - char line_M73[64]; - sprintf(line_M73, mask.c_str(), - std::to_string(percent).c_str(), - std::to_string(time).c_str()); - return std::string(line_M73); - }; - - auto format_line_M73_stop_int = [this](const std::string& mask, int time) { - if (this->m_disable_m73) - return std::string(""); - char line_M73[64]; - sprintf(line_M73, mask.c_str(), std::to_string(time).c_str()); - return std::string(line_M73); - }; - - auto format_time_float = [](float time) { - return Slic3r::float_to_string_decimal_point(time, 2); - }; - - auto format_line_M73_stop_float = [format_time_float](const std::string& mask, float time) { - char line_M73[64]; - sprintf(line_M73, mask.c_str(), format_time_float(time).c_str()); - return std::string(line_M73); - }; - - std::string gcode_line; - size_t g1_lines_counter = 0; - // keeps track of last exported pair - std::array, static_cast(PrintEstimatedStatistics::ETimeMode::Count)> last_exported_main; - for (size_t i = 0; i < static_cast(PrintEstimatedStatistics::ETimeMode::Count); ++i) { - last_exported_main[i] = { 0, time_in_minutes(m_time_processor.machines[i].time) }; - } - - // keeps track of last exported remaining time to next printer stop - std::array(PrintEstimatedStatistics::ETimeMode::Count)> last_exported_stop; - for (size_t i = 0; i < static_cast(PrintEstimatedStatistics::ETimeMode::Count); ++i) { - last_exported_stop[i] = time_in_minutes(m_time_processor.machines[i].time); - } - - // Helper class to modify and export gcode to file - class ExportLines - { - public: - struct Backtrace - { - float time{ 60.0f }; - int steps{ 10 }; - float time_step() const { return time / float(steps); } - }; - - enum class EWriteType - { - BySize, - ByTime - }; - - private: - struct LineData - { - std::string line; - std::array(PrintEstimatedStatistics::ETimeMode::Count)> times{ 0.0f, 0.0f }; - }; - - enum ETimeMode - { - Normal = static_cast(PrintEstimatedStatistics::ETimeMode::Normal), - Stealth = static_cast(PrintEstimatedStatistics::ETimeMode::Stealth) - }; - -#ifndef NDEBUG - class Statistics - { - ExportLines& m_parent; - size_t m_max_size{ 0 }; - size_t m_lines_count{ 0 }; - size_t m_max_lines_count{ 0 }; - - public: - explicit Statistics(ExportLines& parent) - : m_parent(parent) - {} - - void add_line(size_t line_size) { - ++m_lines_count; - m_max_size = std::max(m_max_size, m_parent.get_size() + line_size); - m_max_lines_count = std::max(m_max_lines_count, m_lines_count); - } - - void remove_line() { --m_lines_count; } - void remove_all_lines() { m_lines_count = 0; } - }; - - Statistics m_statistics; -#endif // NDEBUG - - EWriteType m_write_type{ EWriteType::BySize }; - // Time machines containing g1 times cache - const std::array(PrintEstimatedStatistics::ETimeMode::Count)>& m_machines; - // Current time - std::array(PrintEstimatedStatistics::ETimeMode::Count)> m_times{ 0.0f, 0.0f }; - // Current size in bytes - size_t m_size{ 0 }; - - // gcode lines cache - std::deque m_lines; - size_t m_added_lines_counter{ 0 }; - // map of gcode line ids from original to final - // used to update m_result.moves[].gcode_id - std::vector> m_gcode_lines_map; - - size_t m_times_cache_id{ 0 }; - size_t m_out_file_pos{ 0 }; - - - public: - ExportLines(EWriteType type, - const std::array(PrintEstimatedStatistics::ETimeMode::Count)>& machines) -#ifndef NDEBUG - : m_statistics(*this), m_write_type(type), m_machines(machines) {} -#else - : m_write_type(type), m_machines(machines) {} -#endif // NDEBUG - - // return: number of internal G1 lines (from G2/G3 splitting) processed - unsigned int update(const std::string& line, size_t lines_counter, size_t g1_lines_counter) { - unsigned int ret = 0; - m_gcode_lines_map.push_back({ lines_counter, 0 }); - - if (GCodeReader::GCodeLine::cmd_is(line, "G0") || - GCodeReader::GCodeLine::cmd_is(line, "G1") || - GCodeReader::GCodeLine::cmd_is(line, "G2") || - GCodeReader::GCodeLine::cmd_is(line, "G3") || - GCodeReader::GCodeLine::cmd_is(line, "G28")) - ++g1_lines_counter; - else - return ret; - - auto init_it = m_machines[Normal].g1_times_cache.begin() + m_times_cache_id; - auto it = init_it; - while (it != m_machines[Normal].g1_times_cache.end() && it->id < g1_lines_counter) { - ++it; - ++m_times_cache_id; - } - - if (it == m_machines[Normal].g1_times_cache.end() || it->id > g1_lines_counter) - return ret; - - // search for internal G1 lines - if (GCodeReader::GCodeLine::cmd_is(line, "G2") || GCodeReader::GCodeLine::cmd_is(line, "G3")) { - while (it != m_machines[Normal].g1_times_cache.end() && it->remaining_internal_g1_lines > 0) { - ++it; - ++m_times_cache_id; - ++g1_lines_counter; - ++ret; - } - } - - if (it != m_machines[Normal].g1_times_cache.end() && it->id == g1_lines_counter) { - m_times[Normal] = it->elapsed_time; - if (!m_machines[Stealth].g1_times_cache.empty()) - m_times[Stealth] = (m_machines[Stealth].g1_times_cache.begin() + std::distance(m_machines[Normal].g1_times_cache.begin(), it))->elapsed_time; - } - - return ret; - } - - // add the given gcode line to the cache - void append_line(const std::string& line, const bool ignore_from_move = false) { - if (line.empty()) return; - - m_lines.push_back({ line, m_times }); -#ifndef NDEBUG - m_statistics.add_line(line.length()); -#endif // NDEBUG - m_size += line.length(); - ++m_added_lines_counter; - if (!ignore_from_move) { - assert(!m_gcode_lines_map.empty()); - m_gcode_lines_map.back().second = m_added_lines_counter; - } - } - - // Insert the gcode lines required by the command cmd by backtracing into the cache - void insert_lines(const Backtrace& backtrace, const std::string& cmd, - std::function&)> line_inserter, - std::function line_replacer) { - // Orca: find start pos by seaching G28/G29/PRINT_START/START_PRINT commands - auto is_start_pos = [](const std::string& curr_cmd) { - return boost::iequals(curr_cmd, "G28") - || boost::iequals(curr_cmd, "G29") - || boost::iequals(curr_cmd, "PRINT_START") - || boost::iequals(curr_cmd, "START_PRINT"); - }; - assert(!m_lines.empty()); - const float time_step = backtrace.time_step(); - size_t rev_it_dist = 0; // distance from the end of the cache of the starting point of the backtrace - float last_time_insertion = 0.0f; // used to avoid inserting two lines at the same time - for (int i = 0; i < backtrace.steps; ++i) { - const float backtrace_time_i = (i + 1) * time_step; - const float time_threshold_i = m_times[Normal] - backtrace_time_i; - auto rev_it = m_lines.rbegin() + rev_it_dist; - auto start_rev_it = rev_it; - - std::string curr_cmd = GCodeReader::GCodeLine::extract_cmd(rev_it->line); - // backtrace into the cache to find the place where to insert the line - while (rev_it != m_lines.rend() && rev_it->times[Normal] > time_threshold_i && curr_cmd != cmd && !is_start_pos(curr_cmd)) { - rev_it->line = line_replacer(rev_it->line); - ++rev_it; - if (rev_it != m_lines.rend()) - curr_cmd = GCodeReader::GCodeLine::extract_cmd(rev_it->line); - } - - // we met the previous evenience of cmd, or the start position, stop inserting lines - if (rev_it != m_lines.rend() && (curr_cmd == cmd || is_start_pos(curr_cmd))) - break; - - // insert the line for the current step - if (rev_it != m_lines.rend() && rev_it != start_rev_it && rev_it->times[Normal] != last_time_insertion) { - last_time_insertion = rev_it->times[Normal]; - std::vector time_diffs; - time_diffs.push_back(m_times[Normal] - last_time_insertion); - if (!m_machines[Stealth].g1_times_cache.empty()) - time_diffs.push_back(m_times[Stealth] - rev_it->times[Stealth]); - const std::string out_line = line_inserter(i + 1, time_diffs); - rev_it_dist = std::distance(m_lines.rbegin(), rev_it) + 1; - m_lines.insert(rev_it.base(), { out_line, rev_it->times }); -#ifndef NDEBUG - m_statistics.add_line(out_line.length()); -#endif // NDEBUG - m_size += out_line.length(); - // synchronize gcode lines map - for (auto map_it = m_gcode_lines_map.rbegin(); map_it != m_gcode_lines_map.rbegin() + rev_it_dist - 1; ++map_it) { - ++map_it->second; - } - - ++m_added_lines_counter; - } - } - } - - // write to file: - // m_write_type == EWriteType::ByTime - all lines older than m_time - backtrace_time - // m_write_type == EWriteType::BySize - all lines if current size is greater than 65535 bytes - void write(FilePtr& out, float backtrace_time, GCodeProcessorResult& result, const std::string& out_path) { - if (m_lines.empty()) - return; - - // collect lines to write into a single string - std::string out_string; - if (!m_lines.empty()) { - if (m_write_type == EWriteType::ByTime) { - while (m_lines.front().times[Normal] < m_times[Normal] - backtrace_time) { - const LineData& data = m_lines.front(); - out_string += data.line; - m_size -= data.line.length(); - m_lines.pop_front(); -#ifndef NDEBUG - m_statistics.remove_line(); -#endif // NDEBUG - } - } - else { - if (m_size > 65535) { - while (!m_lines.empty()) { - out_string += m_lines.front().line; - m_lines.pop_front(); - } - m_size = 0; -#ifndef NDEBUG - m_statistics.remove_all_lines(); -#endif // NDEBUG - } - } - } - - { - write_to_file(out, out_string, result, out_path); - update_lines_ends_and_out_file_pos(out_string, result.lines_ends, &m_out_file_pos); - } - } - - // flush the current content of the cache to file - void flush(FilePtr& out, GCodeProcessorResult& result, const std::string& out_path) { - // collect lines to flush into a single string - std::string out_string; - while (!m_lines.empty()) { - out_string += m_lines.front().line; - m_lines.pop_front(); - } - m_size = 0; -#ifndef NDEBUG - m_statistics.remove_all_lines(); -#endif // NDEBUG - - { - write_to_file(out, out_string, result, out_path); - update_lines_ends_and_out_file_pos(out_string, result.lines_ends, &m_out_file_pos); - } - } - - void synchronize_moves(GCodeProcessorResult& result) const { - auto it = m_gcode_lines_map.begin(); - for (GCodeProcessorResult::MoveVertex& move : result.moves) { - while (it != m_gcode_lines_map.end() && it->first < move.gcode_id) { - ++it; - } - if (it != m_gcode_lines_map.end() && it->first == move.gcode_id) - move.gcode_id = it->second; - } - } - - size_t get_size() const { return m_size; } - - private: - void write_to_file(FilePtr& out, const std::string& out_string, GCodeProcessorResult& result, const std::string& out_path) { - if (!out_string.empty()) { - if (true) { - fwrite((const void*)out_string.c_str(), 1, out_string.length(), out.f); - if (ferror(out.f)) { - out.close(); - boost::nowide::remove(out_path.c_str()); - throw Slic3r::RuntimeError("GCode processor post process export failed.\nIs the disk full?"); - } - } - } - } - }; - - ExportLines export_lines(m_result.backtrace_enabled ? ExportLines::EWriteType::ByTime : ExportLines::EWriteType::BySize, - m_time_processor.machines); - - // replace placeholder lines with the proper final value - // gcode_line is in/out parameter, to reduce expensive memory allocation - auto process_placeholders = [&](std::string& gcode_line) { - bool processed = false; - - // remove trailing '\n' - auto line = std::string_view(gcode_line).substr(0, gcode_line.length() - 1); - - if (line.length() > 1) { - line = line.substr(1); - if (true && - (line == reserved_tag(ETags::First_Line_M73_Placeholder) || line == reserved_tag(ETags::Last_Line_M73_Placeholder))) { - for (size_t i = 0; i < static_cast(PrintEstimatedStatistics::ETimeMode::Count); ++i) { - const TimeMachine& machine = m_time_processor.machines[i]; - if (machine.enabled) { - // export pair - export_lines.append_line(format_line_M73_main(machine.line_m73_main_mask.c_str(), - (line == reserved_tag(ETags::First_Line_M73_Placeholder)) ? 0 : 100, - (line == reserved_tag(ETags::First_Line_M73_Placeholder)) ? time_in_minutes(machine.time) : 0)); - processed = true; - - // export remaining time to next printer stop - if (line == reserved_tag(ETags::First_Line_M73_Placeholder) && !machine.stop_times.empty()) { - const int to_export_stop = time_in_minutes(machine.stop_times.front().elapsed_time); - export_lines.append_line(format_line_M73_stop_int(machine.line_m73_stop_mask.c_str(), to_export_stop)); - last_exported_stop[i] = to_export_stop; - } - } - } - } else if (line == reserved_tag(ETags::Estimated_Printing_Time_Placeholder)) { - for (size_t i = 0; i < static_cast(PrintEstimatedStatistics::ETimeMode::Count); ++i) { - const TimeMachine& machine = m_time_processor.machines[i]; - PrintEstimatedStatistics::ETimeMode mode = static_cast(i); - if (mode == PrintEstimatedStatistics::ETimeMode::Normal || machine.enabled) { - char buf[128]; - if (!s_IsBBLPrinter) - // Orca: compatibility with klipper_estimator - sprintf(buf, "; estimated printing time (%s mode) = %s\n", - (mode == PrintEstimatedStatistics::ETimeMode::Normal) ? "normal" : "silent", - get_time_dhms(machine.time).c_str()); - else { - sprintf(buf, "; model printing time: %s; total estimated time: %s\n", - get_time_dhms(machine.time - machine.prepare_time).c_str(), get_time_dhms(machine.time).c_str()); - } - export_lines.append_line(buf); - } - } - for (size_t i = 0; i < static_cast(PrintEstimatedStatistics::ETimeMode::Count); ++i) { - const TimeMachine& machine = m_time_processor.machines[i]; - PrintEstimatedStatistics::ETimeMode mode = static_cast(i); - if (mode == PrintEstimatedStatistics::ETimeMode::Normal || machine.enabled) { - char buf[128]; - sprintf(buf, "; estimated first layer printing time (%s mode) = %s\n", - (mode == PrintEstimatedStatistics::ETimeMode::Normal) ? "normal" : "silent", - get_time_dhms(machine.prepare_time).c_str()); - export_lines.append_line(buf); - processed = true; - } - } - } - // Orca: write total layer number, this is used by Bambu printers only as of now - else if (line == reserved_tag(ETags::Total_Layer_Number_Placeholder)) { - char buf[128]; - sprintf(buf, "; total layer number: %u\n", m_layer_id); - export_lines.append_line(buf); - processed = true; - } - } - - return processed; - }; - - auto process_used_filament = [&](std::string& gcode_line) { - // Prefilter for parsing speed. - if (gcode_line.size() < 8 || gcode_line[0] != ';' || gcode_line[1] != ' ') - return false; - if (const char c = gcode_line[2]; c != 'f' && c != 't') - return false; - auto process_tag = [](std::string& gcode_line, const std::string_view tag, const std::vector& values) { - if (boost::algorithm::starts_with(gcode_line, tag)) { - gcode_line = tag; - char buf[1024]; - for (size_t i = 0; i < values.size(); ++i) { - sprintf(buf, i == values.size() - 1 ? " %.2lf\n" : " %.2lf,", values[i]); - gcode_line += buf; - } - return true; - } - return false; - }; - - bool ret = false; - ret |= process_tag(gcode_line, PrintStatistics::FilamentUsedMmMask, filament_mm); - ret |= process_tag(gcode_line, PrintStatistics::FilamentUsedGMask, filament_g); - ret |= process_tag(gcode_line, PrintStatistics::TotalFilamentUsedGMask, { filament_total_g }); - ret |= process_tag(gcode_line, PrintStatistics::FilamentUsedCm3Mask, filament_cm3); - ret |= process_tag(gcode_line, PrintStatistics::FilamentCostMask, filament_cost); - ret |= process_tag(gcode_line, PrintStatistics::TotalFilamentCostMask, { filament_total_cost }); - return ret; - }; - - // check for temporary lines - auto is_temporary_decoration = [](const std::string_view gcode_line) { - // remove trailing '\n' - assert(!gcode_line.empty()); - assert(gcode_line.back() == '\n'); - - // return true for decorations which are used in processing the gcode but that should not be exported into the final gcode - // i.e.: - // bool ret = gcode_line.substr(0, gcode_line.length() - 1) == ";" + Layer_Change_Tag; - // ... - // return ret; - return false; - }; - - // Iterators for the normal and silent cached time estimate entry recently processed, used by process_line_G1. - auto g1_times_cache_it = Slic3r::reserve_vector::const_iterator>(m_time_processor.machines.size()); - for (const auto& machine : m_time_processor.machines) - g1_times_cache_it.emplace_back(machine.g1_times_cache.begin()); - - // add lines M73 to exported gcode - auto process_line_G1 = [this, - // Lambdas, mostly for string formatting, all with an empty capture block. - time_in_minutes, format_time_float, format_line_M73_main, format_line_M73_stop_int, format_line_M73_stop_float, time_in_last_minute, - // Caches, to be modified - &g1_times_cache_it, &last_exported_main, &last_exported_stop, - &export_lines] - (const size_t g1_lines_counter) { - if (true) { - for (size_t i = 0; i < static_cast(PrintEstimatedStatistics::ETimeMode::Count); ++i) { - const TimeMachine& machine = m_time_processor.machines[i]; - if (machine.enabled) { - // export pair - // Skip all machine.g1_times_cache below g1_lines_counter. - auto& it = g1_times_cache_it[i]; - while (it != machine.g1_times_cache.end() && it->id < g1_lines_counter) - ++it; - if (it != machine.g1_times_cache.end() && it->id == g1_lines_counter) { - std::pair to_export_main = { int(100.0f * it->elapsed_time / machine.time), - time_in_minutes(machine.time - it->elapsed_time) }; - if (last_exported_main[i] != to_export_main) { - export_lines.append_line(format_line_M73_main(machine.line_m73_main_mask.c_str(), - to_export_main.first, to_export_main.second), true); - last_exported_main[i] = to_export_main; - } - // export remaining time to next printer stop - auto it_stop = std::upper_bound(machine.stop_times.begin(), machine.stop_times.end(), it->elapsed_time, - [](float value, const TimeMachine::StopTime& t) { return value < t.elapsed_time; }); - if (it_stop != machine.stop_times.end()) { - int to_export_stop = time_in_minutes(it_stop->elapsed_time - it->elapsed_time); - if (last_exported_stop[i] != to_export_stop) { - if (to_export_stop > 0) { - if (last_exported_stop[i] != to_export_stop) { - export_lines.append_line(format_line_M73_stop_int(machine.line_m73_stop_mask.c_str(), to_export_stop), true); - last_exported_stop[i] = to_export_stop; - } - } - else { - bool is_last = false; - auto next_it = it + 1; - is_last |= (next_it == machine.g1_times_cache.end()); - - if (next_it != machine.g1_times_cache.end()) { - auto next_it_stop = std::upper_bound(machine.stop_times.begin(), machine.stop_times.end(), next_it->elapsed_time, - [](float value, const TimeMachine::StopTime& t) { return value < t.elapsed_time; }); - is_last |= (next_it_stop != it_stop); - - std::string time_float_str = format_time_float(time_in_last_minute(it_stop->elapsed_time - it->elapsed_time)); - std::string next_time_float_str = format_time_float(time_in_last_minute(it_stop->elapsed_time - next_it->elapsed_time)); - is_last |= (string_to_double_decimal_point(time_float_str) > 0. && string_to_double_decimal_point(next_time_float_str) == 0.); - } - - if (is_last) { - if (std::distance(machine.stop_times.begin(), it_stop) == static_cast(machine.stop_times.size() - 1)) - export_lines.append_line(format_line_M73_stop_int(machine.line_m73_stop_mask.c_str(), to_export_stop), true); - else - export_lines.append_line(format_line_M73_stop_float(machine.line_m73_stop_mask.c_str(), time_in_last_minute(it_stop->elapsed_time - it->elapsed_time)), true); - - last_exported_stop[i] = to_export_stop; - } - } - } - } - } - } - } - } - }; - - // add lines M104 to exported gcode - auto process_line_T = [this, &export_lines](const std::string& gcode_line, const size_t g1_lines_counter, const ExportLines::Backtrace& backtrace) { - const std::string cmd = GCodeReader::GCodeLine::extract_cmd(gcode_line); - - int tool_number = -1; - if (!parse_number(std::string_view(cmd).substr(1), tool_number)){ - // invalid T command, such as the "TIMELAPSE_TAKE_FRAME" gcode, just ignore - return; - } - if (cmd.size() >= 2) { - if (tool_number != -1) { - if (tool_number < 0 || (int)m_extruder_temps_config.size() <= tool_number) { - // found an invalid value, clamp it to a valid one - tool_number = std::clamp(0, m_extruder_temps_config.size() - 1, tool_number); - // emit warning - std::string warning = "GCode Post-Processor encountered an invalid toolchange, maybe from a custom gcode:"; - warning += "\n> "; - warning += gcode_line; - warning += "Generated M104 lines may be incorrect."; - BOOST_LOG_TRIVIAL(error) << warning; - // Orca todo - if (m_print != nullptr) - m_print->active_step_add_warning(PrintStateBase::WarningLevel::CRITICAL, warning); - } - export_lines.insert_lines( - backtrace, cmd, - // line inserter - [tool_number, this](unsigned int id, const std::vector& time_diffs) { - const int temperature = int(m_layer_id != 1 ? m_extruder_temps_config[tool_number] : - m_extruder_temps_first_layer_config[tool_number]); - // Orca: M104.1 for XL printers, I can't find the documentation for this so I copied the C++ comments from - // Prusa-Firmware-Buddy here - /** - * M104.1: Early Set Hotend Temperature (preheat, and with stealth mode support) - * - * This GCode is used to tell the XL printer the time estimate when a tool will be used next, - * so that the printer can start preheating the tool in advance. - * - * ## Parameters - * - `P` - - time in seconds till the temperature S is required (in standard mode) - * - `Q` - - time in seconds till the temperature S is required (in stealth mode) - * The rest is same as M104 - */ - if (this->m_is_XL_printer) { - std::string out = "M104.1 T" + std::to_string(tool_number); - if (time_diffs.size() > 0) - out += " P" + std::to_string(int(std::round(time_diffs[0]))); - if (time_diffs.size() > 1) - out += " Q" + std::to_string(int(std::round(time_diffs[1]))); - out += " S" + std::to_string(temperature) + "\n"; - return out; - } else { - std::string comment = "preheat T" + std::to_string(tool_number) + - " time: " + std::to_string((int) std::round(time_diffs[0])) + "s"; - return GCodeWriter::set_temperature(temperature, this->m_flavor, false, tool_number, comment); - } - }, - // line replacer - [this, tool_number](const std::string& line) { - if (GCodeReader::GCodeLine::cmd_is(line, "M104")) { - GCodeReader::GCodeLine gline; - GCodeReader reader; - reader.parse_line(line, [&gline](GCodeReader& reader, const GCodeReader::GCodeLine& l) { gline = l; }); - - float val; - if (gline.has_value('T', val) && gline.raw().find("cooldown") != std::string::npos) { - if (static_cast(val) == tool_number) - return std::string("; removed M104\n"); - } - } - return line; - } - ); - } - } - }; - - m_result.lines_ends.clear(); - // m_result.lines_ends.emplace_back(std::vector()); - - unsigned int line_id = 0; - // Backtrace data for Tx gcode lines - const ExportLines::Backtrace backtrace_T = { m_preheat_time, m_preheat_steps }; - // In case there are multiple sources of backtracing, keeps track of the longest backtrack time needed - // to flush the backtrace cache accordingly - float max_backtrace_time = 120.0f; - - { - // Read the input stream 64kB at a time, extract lines and process them. - std::vector buffer(65536 * 10, 0); - // Line buffer. - assert(gcode_line.empty()); - for (;;) { - size_t cnt_read = ::fread(buffer.data(), 1, buffer.size(), in.f); - if (::ferror(in.f)) - throw Slic3r::RuntimeError(std::string("GCode processor post process export failed.\nError while reading from file.\n")); - bool eof = cnt_read == 0; - auto it = buffer.begin(); - auto it_bufend = buffer.begin() + cnt_read; - while (it != it_bufend || (eof && !gcode_line.empty())) { - // Find end of line. - bool eol = false; - auto it_end = it; - for (; it_end != it_bufend && !(eol = *it_end == '\r' || *it_end == '\n'); ++it_end); - // End of line is indicated also if end of file was reached. - eol |= eof && it_end == it_bufend; - gcode_line.insert(gcode_line.end(), it, it_end); - - it = it_end; - // append EOL. - if (it != it_bufend && *it == '\r') { - gcode_line += *it++; - } - if (it != it_bufend && *it == '\n') { - gcode_line += *it++; - } - - if (eol) { - ++line_id; - const unsigned int internal_g1_lines_counter = export_lines.update(gcode_line, line_id, g1_lines_counter); - // replace placeholder lines - bool processed = process_placeholders(gcode_line); - if (processed) - gcode_line.clear(); - if (!processed) - processed = process_used_filament(gcode_line); - if (!processed && !is_temporary_decoration(gcode_line)) { - if (GCodeReader::GCodeLine::cmd_is(gcode_line, "G0") || GCodeReader::GCodeLine::cmd_is(gcode_line, "G1")) { - export_lines.append_line(gcode_line); - // add lines M73 where needed - process_line_G1(g1_lines_counter++); - gcode_line.clear(); - } - else if (GCodeReader::GCodeLine::cmd_is(gcode_line, "G2") || GCodeReader::GCodeLine::cmd_is(gcode_line, "G3")) { - export_lines.append_line(gcode_line); - // add lines M73 where needed - process_line_G1(g1_lines_counter + internal_g1_lines_counter); - g1_lines_counter += (1 + internal_g1_lines_counter); - gcode_line.clear(); - } - else if (GCodeReader::GCodeLine::cmd_is(gcode_line, "G28")) { - ++g1_lines_counter; - } - else if (m_result.backtrace_enabled && GCodeReader::GCodeLine::cmd_starts_with(gcode_line, "T")) { - // add lines M104 where needed - process_line_T(gcode_line, g1_lines_counter, backtrace_T); - max_backtrace_time = std::max(max_backtrace_time, backtrace_T.time); - } - } - - if (!gcode_line.empty()) - export_lines.append_line(gcode_line); - export_lines.write(out, 1.1f * max_backtrace_time, m_result, out_path); - gcode_line.clear(); - } - } - if (eof) - break; - } - } - - export_lines.flush(out, m_result, out_path); - - - out.close(); - in.close(); - - const std::string result_filename = m_result.filename; - export_lines.synchronize_moves(m_result); - - if (rename_file(out_path, result_filename)) - throw Slic3r::RuntimeError(std::string("Failed to rename the output G-code file from ") + out_path + " to " + result_filename + '\n' + - "Is " + out_path + " locked?" + '\n'); -} - void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type) { int filament_id = get_filament_id(); diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 0aca77b8ea..19cd201b56 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -431,6 +431,7 @@ class Print; private: + friend class ExportLines; struct TimeMachine { struct State