feat: Add File header G-code option and print_time_sec/filament_length_m placeholders (#12186)

## Problem

Some Creality printers (e.g. Ender-3 V3 SE) rely on metadata comments in the first lines of G-code files to display print info on the screen (estimated time, filament usage, layer height). This follows a format originally used by Cura/Creality Print:
```
;FLAVOR:Marlin
;TIME:3708.97
;Filament used:6.21m
;Layer height:0.2
```

OrcaSlicer currently has no way to write content before the `HEADER_BLOCK` in the G-code output, and does not expose `print_time_sec` or `filament_length_m` as usable placeholders. As a result, these printers show zeroed-out print info on screen.

## Changes

### 1. New placeholders (post-processing)
Added two new G-code placeholders resolved during post-processing:
- `{print_time_sec}` — total estimated print time in seconds (double)
- `{filament_length_m}` — total filament length in meters (double)

These values are only available after G-code generation, so they use inline marker replacement (`@PRINT_TIME_SEC@`, `@FILAMENT_LENGTH_M@`) that `GCodeProcessor::run_post_process()` substitutes with actual computed values — the same pattern used by existing M73 and layer count placeholders.

### 2. New "File header G-code" option (`machine_top_gcode`)
Added a new G-code field in **Printer Settings > Machine G-code** that writes content at the very top of the output file, before `HEADER_BLOCK_START`. This allows users to add firmware-specific metadata that must appear in the first lines.

### 3. Creality Ender-3 V3 SE profile update
Pre-configured the Ender-3 V3 SE profiles (all 4 nozzle variants) with a default `machine_top_gcode` value matching the Cura-compatible header format, so print info displays correctly out of the box.

# Screenshots

| Before (current OrcaSlicer) | After (this PR) |
|---|---|
| ![IMG_5285](https://github.com/user-attachments/assets/fdd2cc96-6593-4108-83f4-e272d5547467) | ![IMG_5286](https://github.com/user-attachments/assets/6cb4091a-4560-4596-93d9-49f2ff8f0a2b) |

## Tests

- Sliced test model with Ender-3 V3 SE profile
- Verified G-code output contains correct values in first lines:
  - `;TIME:` with actual print time in seconds
  - `;Filament used:` with filament length in meters
  - `;Layer height:` with correct layer height
- Verified `{print_time_sec}` and `{filament_length_m}` work in both Machine Start G-code and File header G-code fields
- Verified empty `machine_top_gcode` produces no extra output
- Built and tested on macOS (arm64)
This commit is contained in:
davyd
2026-02-08 12:56:28 -03:00
committed by GitHub
parent 1663dab6ff
commit 2f2f0df6af
11 changed files with 99 additions and 3 deletions

View File

@@ -73,7 +73,9 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags = {
"_DURING_PRINT_EXHAUST_FAN",
" WIPE_TOWER_START",
" WIPE_TOWER_END",
" PA_CHANGE:"
" PA_CHANGE:",
"@PRINT_TIME_SEC@",
"@USED_FILAMENT_LENGTH@"
};
const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
@@ -94,7 +96,9 @@ const std::vector<std::string> GCodeProcessor::Reserved_Tags_compatible = {
"_DURING_PRINT_EXHAUST_FAN",
" WIPE_TOWER_START",
" WIPE_TOWER_END",
" PA_CHANGE:"
" PA_CHANGE:",
"@PRINT_TIME_SEC@",
"@USED_FILAMENT_LENGTH@"
};
@@ -1101,6 +1105,42 @@ void GCodeProcessor::run_post_process()
return ret;
};
// Process inline placeholders (print_time_sec and used_filament_length)
auto process_inline_placeholders = [&](std::string& gcode_line) {
bool processed = false;
const std::string& print_time_placeholder = reserved_tag(ETags::Print_Time_Sec_Placeholder);
const std::string& used_filament_placeholder = reserved_tag(ETags::Used_Filament_Length_Placeholder);
// Replace print_time_sec
size_t pos = gcode_line.find(print_time_placeholder);
while (pos != std::string::npos) {
double print_time_sec = m_time_processor.machines[static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal)].time;
char buf[64];
sprintf(buf, "%.2f", print_time_sec);
gcode_line.replace(pos, print_time_placeholder.length(), buf);
processed = true;
pos = gcode_line.find(print_time_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));
}
return processed;
};
// check for temporary lines
auto is_temporary_decoration = [](const std::string_view gcode_line) {
// remove trailing '\n'
@@ -1317,6 +1357,8 @@ void GCodeProcessor::run_post_process()
gcode_line.clear();
if (!processed)
processed = process_used_filament(gcode_line);
if (!gcode_line.empty())
process_inline_placeholders(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);