diff --git a/src/libslic3r/GCode.cpp b/src/libslic3r/GCode.cpp index 508ab5b421..1500483f6e 100644 --- a/src/libslic3r/GCode.cpp +++ b/src/libslic3r/GCode.cpp @@ -3985,6 +3985,16 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato this->placeholder_parser().set("print_time_minute", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder))); this->placeholder_parser().set("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder))); this->placeholder_parser().set("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder))); + // The final print statistics are known only after the whole G-code is generated, so they are filled in during post-processing too. + // The running extruded_* totals are left alone; profiles use them as numbers. + this->placeholder_parser().set("used_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder))); + this->placeholder_parser().set("print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder))); + this->placeholder_parser().set("normal_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Normal_Print_Time_Placeholder))); + this->placeholder_parser().set("silent_print_time", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Silent_Print_Time_Placeholder))); + this->placeholder_parser().set("total_weight", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Weight_Placeholder))); + this->placeholder_parser().set("total_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Cost_Placeholder))); + this->placeholder_parser().set("total_wipe_tower_cost", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Cost_Placeholder))); + this->placeholder_parser().set("total_wipe_tower_filament", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Wipe_Tower_Filament_Placeholder))); // Sync variant-mapped params into placeholder_parser before processing start gcode update_placeholder_parser_with_variant_params(); diff --git a/src/libslic3r/GCode/GCodeProcessor.cpp b/src/libslic3r/GCode/GCodeProcessor.cpp index ac047be180..6346651448 100644 --- a/src/libslic3r/GCode/GCodeProcessor.cpp +++ b/src/libslic3r/GCode/GCodeProcessor.cpp @@ -120,7 +120,13 @@ const std::vector GCodeProcessor::Reserved_Tags = { "@PRINT_TIME_HOUR@", "@PRINT_TIME_MINUTE@", "@PRINT_TIME_SEC@", - "@USED_FILAMENT_LENGTH@" + "@USED_FILAMENT_LENGTH@", + "@NORMAL_PRINT_TIME@", + "@SILENT_PRINT_TIME@", + "@TOTAL_WEIGHT@", + "@TOTAL_COST@", + "@TOTAL_WIPE_TOWER_COST@", + "@TOTAL_WIPE_TOWER_FILAMENT@" }; const std::vector GCodeProcessor::Reserved_Tags_compatible = { @@ -147,7 +153,13 @@ const std::vector GCodeProcessor::Reserved_Tags_compatible = { "@PRINT_TIME_HOUR@", "@PRINT_TIME_MINUTE@", "@PRINT_TIME_SEC@", - "@USED_FILAMENT_LENGTH@" + "@USED_FILAMENT_LENGTH@", + "@NORMAL_PRINT_TIME@", + "@SILENT_PRINT_TIME@", + "@TOTAL_WEIGHT@", + "@TOTAL_COST@", + "@TOTAL_WIPE_TOWER_COST@", + "@TOTAL_WIPE_TOWER_FILAMENT@" }; @@ -1264,19 +1276,22 @@ void GCodeProcessor::run_post_process() return ret; }; - // Process inline placeholders (print_time_total_sec, print_time_day, print_time_hour, print_time_minute, print_time_sec and used_filament_length) + // Process inline placeholders (print_time_*, used_filament_length and the print statistics known only after export) auto process_inline_placeholders = [&](std::string& gcode_line) { bool processed = false; // Every inline placeholder contains '@', so a line without one has nothing to replace. if (gcode_line.find('@') == std::string::npos) return processed; - const std::string& print_time_total_placeholder = reserved_tag(ETags::Print_Time_Total_Sec_Placeholder); - const std::string& print_time_day_placeholder = reserved_tag(ETags::Print_Time_Day_Placeholder); - const std::string& print_time_hour_placeholder = reserved_tag(ETags::Print_Time_Hour_Placeholder); - const std::string& print_time_minute_placeholder = reserved_tag(ETags::Print_Time_Minute_Placeholder); - const std::string& print_time_sec_placeholder = reserved_tag(ETags::Print_Time_Sec_Placeholder); - const std::string& used_filament_placeholder = reserved_tag(ETags::Used_Filament_Length_Placeholder); + auto replace_tag = [&gcode_line, &processed](ETags tag, const std::string& value) { + const std::string& placeholder = reserved_tag(tag); + size_t pos = gcode_line.find(placeholder); + while (pos != std::string::npos) { + gcode_line.replace(pos, placeholder.length(), value); + processed = true; + pos = gcode_line.find(placeholder, pos + value.length()); + } + }; double print_time_total_sec = m_time_processor.machines[static_cast(PrintEstimatedStatistics::ETimeMode::Normal)].time; if (print_time_total_sec < 0.0) @@ -1289,70 +1304,31 @@ void GCodeProcessor::run_post_process() int print_time_minute = (day_remainder_seconds % 3600) / 60; int print_time_sec = day_remainder_seconds % 60; - // Replace print_time_total_sec - size_t pos = gcode_line.find(print_time_total_placeholder); - while (pos != std::string::npos) { - char buf[64]; - sprintf(buf, "%.2f", print_time_total_sec); - gcode_line.replace(pos, print_time_total_placeholder.length(), buf); - processed = true; - pos = gcode_line.find(print_time_total_placeholder, pos + strlen(buf)); + double total_filament_mm = 0.0; + for (const auto& mm : filament_mm) { + total_filament_mm += mm; } - // Replace print_time_day - pos = gcode_line.find(print_time_day_placeholder); - while (pos != std::string::npos) { - char buf[64]; - sprintf(buf, "%d", print_time_day); - gcode_line.replace(pos, print_time_day_placeholder.length(), buf); - processed = true; - pos = gcode_line.find(print_time_day_placeholder, pos + strlen(buf)); - } - - // Replace print_time_hour - pos = gcode_line.find(print_time_hour_placeholder); - while (pos != std::string::npos) { - char buf[64]; - sprintf(buf, "%d", print_time_hour); - gcode_line.replace(pos, print_time_hour_placeholder.length(), buf); - processed = true; - pos = gcode_line.find(print_time_hour_placeholder, pos + strlen(buf)); - } - - // Replace print_time_minute - pos = gcode_line.find(print_time_minute_placeholder); - while (pos != std::string::npos) { - char buf[64]; - sprintf(buf, "%d", print_time_minute); - gcode_line.replace(pos, print_time_minute_placeholder.length(), buf); - processed = true; - pos = gcode_line.find(print_time_minute_placeholder, pos + strlen(buf)); - } - - // Replace print_time_sec - pos = gcode_line.find(print_time_sec_placeholder); - while (pos != std::string::npos) { - char buf[64]; - sprintf(buf, "%d", print_time_sec); - gcode_line.replace(pos, print_time_sec_placeholder.length(), buf); - processed = true; - pos = gcode_line.find(print_time_sec_placeholder, pos + strlen(buf)); - } - - // Replace used_filament_length - pos = gcode_line.find(used_filament_placeholder); - while (pos != std::string::npos) { - double total_filament_mm = 0.0; - for (const auto& mm : filament_mm) { - total_filament_mm += mm; - } - double used_filament_length = total_filament_mm / 1000.0; // Convert mm to m - char buf[64]; - sprintf(buf, "%.2f", used_filament_length); - gcode_line.replace(pos, used_filament_placeholder.length(), buf); - processed = true; - pos = gcode_line.find(used_filament_placeholder, pos + strlen(buf)); - } + replace_tag(ETags::Print_Time_Total_Sec_Placeholder, Slic3r::float_to_string_decimal_point(print_time_total_sec, 2)); + replace_tag(ETags::Print_Time_Day_Placeholder, std::to_string(print_time_day)); + replace_tag(ETags::Print_Time_Hour_Placeholder, std::to_string(print_time_hour)); + replace_tag(ETags::Print_Time_Minute_Placeholder, std::to_string(print_time_minute)); + replace_tag(ETags::Print_Time_Sec_Placeholder, std::to_string(print_time_sec)); + replace_tag(ETags::Used_Filament_Length_Placeholder, + Slic3r::float_to_string_decimal_point(total_filament_mm / 1000.0, 2)); // Convert mm to m + // Same values as the print statistics that filename_format gets (PrintStatistics::config(), filled in by + // update_print_estimated_stats() in GCode.cpp), which runs only after this post-processing. + replace_tag(ETags::Normal_Print_Time_Placeholder, get_time_dhm(PrintEstimatedStatistics::ETimeMode::Normal)); + replace_tag(ETags::Silent_Print_Time_Placeholder, is_stealth_time_estimator_enabled() ? + get_time_dhm(PrintEstimatedStatistics::ETimeMode::Stealth) : + short_time("N/A")); + const double total_cost = filament_total_cost + m_print->config().time_cost.value * print_time_total_sec / 3600.0; + replace_tag(ETags::Total_Weight_Placeholder, Slic3r::float_to_string_decimal_point(filament_total_g, 2)); + replace_tag(ETags::Total_Cost_Placeholder, Slic3r::float_to_string_decimal_point(total_cost, 2)); + replace_tag(ETags::Total_Wipe_Tower_Cost_Placeholder, + Slic3r::float_to_string_decimal_point(m_print->print_statistics().total_wipe_tower_cost, 2)); + replace_tag(ETags::Total_Wipe_Tower_Filament_Placeholder, + Slic3r::float_to_string_decimal_point(m_print->print_statistics().total_wipe_tower_filament, 2)); return processed; }; diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index 87c08617d1..72fb84bad0 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -601,6 +601,12 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f; Print_Time_Minute_Placeholder, Print_Time_Sec_Placeholder, Used_Filament_Length_Placeholder, + Normal_Print_Time_Placeholder, + Silent_Print_Time_Placeholder, + Total_Weight_Placeholder, + Total_Cost_Placeholder, + Total_Wipe_Tower_Cost_Placeholder, + Total_Wipe_Tower_Filament_Placeholder, }; static const std::string& reserved_tag(ETags tag) { return s_IsBBLPrinter ? Reserved_Tags[static_cast(tag)] : Reserved_Tags_compatible[static_cast(tag)]; } diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index dd78ef5f5f..670c2aa7c1 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -735,6 +735,65 @@ TEST_CASE("Custom G-code placeholders are substituted", "[Print]") CHECK(per_layer.find(";Layer:399 ") != std::string::npos); } +// The final print statistics are known only after the whole G-code is generated, yet every custom G-code, +// the file header included, gets the same values as filename_format. +TEST_CASE("Print statistics in custom G-code match the final print statistics", "[Print][Regression]") +{ + const std::string gcode_key = GENERATE(as{}, "file_start_gcode", "machine_start_gcode", "machine_end_gcode"); + // Bambu printers use another tag table and G-code layout. + const bool is_bbl = GENERATE(false, true); + CAPTURE(gcode_key, is_bbl); + + const std::vector number_keys = { "total_weight", "used_filament", "total_cost", "total_wipe_tower_cost", + "total_wipe_tower_filament" }; + const std::vector time_keys = { "print_time", "normal_print_time", "silent_print_time" }; + std::string templ; + for (const std::vector *keys : { &number_keys, &time_keys }) + for (const std::string &key : *keys) + templ += "; stat " + key + "={" + key + "}\n"; + + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({ + { gcode_key, templ }, + { "filament_density", "1.25" }, + { "filament_cost", "20" }, + { "time_cost", "2" }, + }); + Print print; + Model model; + init_print({ cube(20) }, print, model, config); + print.is_BBL_printer() = is_bbl; + + std::string gcode; + REQUIRE_NOTHROW(gcode = Slic3r::Test::gcode(print)); + + // Match whole lines only: the config block repeats the template on a single line, and Bambu printers + // write that block before the start G-code. + const std::string lines = "\n" + gcode; + const auto value_of = [&lines](const std::string &key) { + const std::string prefix = "\n; stat " + key + "="; + const size_t start = lines.find(prefix); + REQUIRE(start != std::string::npos); + const size_t begin = start + prefix.size(); + return lines.substr(begin, lines.find('\n', begin) - begin); + }; + // The same statistics filename_format is expanded with. + DynamicConfig stats = print.print_statistics().config(); + // A 20mm cube with filament_density 1.25 weighs something, unlike the running extruded_weight_total in the header. + CHECK(stats.opt_float("total_weight") > 0.); + for (const std::string &key : number_keys) { + CAPTURE(key); + const std::string value = value_of(key); + REQUIRE(value.find('@') == std::string::npos); + // Written with two decimals. + CHECK_THAT(std::stod(value), Catch::Matchers::WithinAbs(stats.opt_float(key), 0.006)); + } + for (const std::string &key : time_keys) { + CAPTURE(key); + CHECK(value_of(key) == stats.opt_string(key)); + } +} + TEST_CASE("export_gcode writes G-code without a result pointer", "[Print][export_gcode]") { Print print;