From 05da6bbcc87bc687bf45a78b4f00400ff28d5bdf Mon Sep 17 00:00:00 2001 From: SoftFever Date: Tue, 29 Sep 2026 01:26:10 +0800 Subject: [PATCH 1/3] Remove unreachable ACE block from the Kobra X filament change G-code The block ran only when flush_length_4 is -1392 and read ace_t_box_vector / ace_t_slot_vector, which only Anycubic's own slicer defines. It emitted comments only, so the printed G-code is unchanged. --- resources/profiles/Anycubic.json | 2 +- .../profiles/Anycubic/machine/Anycubic Kobra X 0.4 nozzle.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/profiles/Anycubic.json b/resources/profiles/Anycubic.json index bb0eedbd91..731d2b32eb 100644 --- a/resources/profiles/Anycubic.json +++ b/resources/profiles/Anycubic.json @@ -1,6 +1,6 @@ { "name": "Anycubic", - "version": "02.04.00.07", + "version": "02.04.00.08", "force_update": "0", "description": "Anycubic configurations", "machine_model_list": [ diff --git a/resources/profiles/Anycubic/machine/Anycubic Kobra X 0.4 nozzle.json b/resources/profiles/Anycubic/machine/Anycubic Kobra X 0.4 nozzle.json index 8e2952b123..c7b3ce89a4 100644 --- a/resources/profiles/Anycubic/machine/Anycubic Kobra X 0.4 nozzle.json +++ b/resources/profiles/Anycubic/machine/Anycubic Kobra X 0.4 nozzle.json @@ -53,7 +53,7 @@ "before_layer_change_gcode": "", "best_object_pos": "0.5,0.5", "change_extrusion_role_gcode": "", - "change_filament_gcode": "; FLUSH_START\n;@2026-04-03 17:46:45 3+n换料gcode,包括7=3+4,19=3+4×4\n;_GP_INLINE_ESTIMATED_PRINTING_TIME_PLACEHOLDER\n;;; SET_VELOCITY_LIMIT VELOCITY=350 ACCEL=10000\n;;; G1 E-2 F4800\n;;; G1 Z{toolchange_z+0.6} F1800\n;;; M106 S0\n;;; G1 X265 F21000\n;;; M400 P370\n;;; G1 X277.5 F600\n;;; G1 E-2 F300\n;;; M400 P361\n;;; G1 Z{toolchange_z+3} F1200\n;;; G1 X0 F21000\n;;; G1 X-17.5 F5250\n;;; M400 P1569\n\n{local minimal_extrude_ = 5.0}\n{if one_of(filament_type[current_extruder], \"TPU\", \"PVA\")}\n{local minimal_extrude_ = 8.0}\n{endif}\n;;; G1 E{minimal_extrude_} F300\n;;; G1 E-33 F600\n\n{ local curr_t = current_extruder; local next_t = next_extruder;}\n{ local tab_step_time_ = (0, 850, 1350, 850, 850, 0, 1350, 1350, 1350, 1350, 0, 850, 850, 1350, 850, 0)};\n{ if (0 <= curr_t && curr_t < 3) then local step_from_=curr_t else local step_from_=3 endif}; from {step_from_}\n{ if (0 <= next_t && next_t < 3) then local step_into_=next_t else local step_into_=3 endif}; from {step_into_}\n;;; M400 P{tab_step_time_[step_from_ * 4 + step_into_]}\n\n{ local magic_mask_with_box = flush_length_4}\n{ if magic_mask_with_box == -1392 }\n { local is_curr_in_box = size(ace_t_box_vector) > curr_t ? ace_t_box_vector[curr_t] >= 0 ? true : false : false}; is_curr_in_box = {is_curr_in_box}\n { local is_next_in_box = size(ace_t_box_vector) > next_t ? ace_t_box_vector[next_t] >= 0 ? true : false : false}; is_next_in_box = {is_next_in_box}\n\n; curr_t box={ace_t_box_vector[curr_t]}, slot={ace_t_slot_vector[curr_t]}; next_t box={ace_t_box_vector[next_t]}, slot={ace_t_slot_vector[next_t]}\n { if ace_t_box_vector[curr_t] < 0 && ace_t_box_vector[next_t] < 0}\n;;; M400 P0 ; 料架→料架\n { elsif ace_t_box_vector[curr_t] < 0 && ace_t_box_vector[next_t] >= 0 }\n;;; M400 P40409 ; 料架→盒子\n { elsif ace_t_box_vector[curr_t] >= 0 && ace_t_box_vector[next_t] < 0 }\n;;; M400 P51591 ; 盒子→料架\n { elsif ace_t_box_vector[curr_t] >= 0 && ace_t_box_vector[next_t] == ace_t_box_vector[curr_t] }\n { if 0 < ace_t_slot_vector[curr_t] && ace_t_slot_vector[curr_t] <= 4 && 0 < ace_t_slot_vector[next_t] && ace_t_slot_vector[next_t] <= 4}\n;;; M400 P2700 ; 四进四→料盒\n { else }\n;;; M400 P91279 ; 盒子→盒子\n { endif }\n { elsif ace_t_box_vector[curr_t] >= 0 && ace_t_box_vector[next_t] != ace_t_box_vector[curr_t] }\n;;; M400 P93746 ; 盒子→不同盒子\n { else }\n; 未知\n { endif }\n{ endif }\n\n\nT[next_extruder]\n\n;;; G1 E8 F300\n;;; M400 P3643\n;;; G1 E13 F1200\n;;; M400 P1000\n\n{local flush_length_= flush_length}\n{local loops_=max(1,int((flush_length_-70+150) / 150))}\n{local extrude_length_=flush_length_ / loops_}\n{local EXTRUDE_SPEED_ = 5 * 60}\n{local UNWIND_SPEED_ = 20 *60}\n{local index_ = 0}\n\n{ if (loops_ > 0) }\n{ local loops_ = loops_ - 1}\n{ local index_ = index_ + 1}\n; {index_} + {loops_}\n;;; M106 S0\n;;; M400 P1000\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; M400 P{extrude_length_/0.03}\n;;; G1 E0.000001\n;;; M106 S255\n;;; M400 P2005\n;;; G1 E-2 F{UNWIND_SPEED_}\n;;; M400 P414\n{endif}\n{ if (loops_ > 0) }\n{ loops_ = loops_ - 1}\n{ index_ = index_ + 1}\n; {index_} + {loops_}\n;;; M106 S0\n;;; M400 P1000\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; M400 P{extrude_length_/0.03}\n;;; M106 S255\n;;; M400 P2005\n;;; G1 E-2 F{UNWIND_SPEED_}\n;;; M400 P414\n{endif}\n{ if (loops_ > 0) }\n{ loops_ = loops_ - 1}\n{ index_ = index_ + 1}\n; {index_} + {loops_}\n;;; M106 S0\n;;; M400 P1000\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; M400 P{extrude_length_/0.03}\n;;; M106 S255\n;;; M400 P2005\n;;; G1 E-2 F{UNWIND_SPEED_}\n;;; M400 P414\n{endif}\n\n; G1 X{x_after_toolchange} Y{y_after_toolchange} F12000\n; G1 Z{toolchange_z} F1200\n;;; G1 E2 F1800\n; SET_VELOCITY_LIMIT VELOCITY=450 ACCEL=10000\n;;; M400 P0\n;_GP_INLINE_ESTIMATED_PRINTING_TIME_PLACEHOLDER\n; FLUSH_END", + "change_filament_gcode": "; FLUSH_START\n;@2026-04-03 17:46:45 3+n换料gcode,包括7=3+4,19=3+4×4\n;_GP_INLINE_ESTIMATED_PRINTING_TIME_PLACEHOLDER\n;;; SET_VELOCITY_LIMIT VELOCITY=350 ACCEL=10000\n;;; G1 E-2 F4800\n;;; G1 Z{toolchange_z+0.6} F1800\n;;; M106 S0\n;;; G1 X265 F21000\n;;; M400 P370\n;;; G1 X277.5 F600\n;;; G1 E-2 F300\n;;; M400 P361\n;;; G1 Z{toolchange_z+3} F1200\n;;; G1 X0 F21000\n;;; G1 X-17.5 F5250\n;;; M400 P1569\n\n{local minimal_extrude_ = 5.0}\n{if one_of(filament_type[current_extruder], \"TPU\", \"PVA\")}\n{local minimal_extrude_ = 8.0}\n{endif}\n;;; G1 E{minimal_extrude_} F300\n;;; G1 E-33 F600\n\n{ local curr_t = current_extruder; local next_t = next_extruder;}\n{ local tab_step_time_ = (0, 850, 1350, 850, 850, 0, 1350, 1350, 1350, 1350, 0, 850, 850, 1350, 850, 0)};\n{ if (0 <= curr_t && curr_t < 3) then local step_from_=curr_t else local step_from_=3 endif}; from {step_from_}\n{ if (0 <= next_t && next_t < 3) then local step_into_=next_t else local step_into_=3 endif}; from {step_into_}\n;;; M400 P{tab_step_time_[step_from_ * 4 + step_into_]}\n\n\nT[next_extruder]\n\n;;; G1 E8 F300\n;;; M400 P3643\n;;; G1 E13 F1200\n;;; M400 P1000\n\n{local flush_length_= flush_length}\n{local loops_=max(1,int((flush_length_-70+150) / 150))}\n{local extrude_length_=flush_length_ / loops_}\n{local EXTRUDE_SPEED_ = 5 * 60}\n{local UNWIND_SPEED_ = 20 *60}\n{local index_ = 0}\n\n{ if (loops_ > 0) }\n{ local loops_ = loops_ - 1}\n{ local index_ = index_ + 1}\n; {index_} + {loops_}\n;;; M106 S0\n;;; M400 P1000\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; M400 P{extrude_length_/0.03}\n;;; G1 E0.000001\n;;; M106 S255\n;;; M400 P2005\n;;; G1 E-2 F{UNWIND_SPEED_}\n;;; M400 P414\n{endif}\n{ if (loops_ > 0) }\n{ loops_ = loops_ - 1}\n{ index_ = index_ + 1}\n; {index_} + {loops_}\n;;; M106 S0\n;;; M400 P1000\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; M400 P{extrude_length_/0.03}\n;;; M106 S255\n;;; M400 P2005\n;;; G1 E-2 F{UNWIND_SPEED_}\n;;; M400 P414\n{endif}\n{ if (loops_ > 0) }\n{ loops_ = loops_ - 1}\n{ index_ = index_ + 1}\n; {index_} + {loops_}\n;;; M106 S0\n;;; M400 P1000\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.18} F{EXTRUDE_SPEED_}\n;;; G1 E{extrude_length_*0.02} F{EXTRUDE_SPEED_}\n;;; M400 P{extrude_length_/0.03}\n;;; M106 S255\n;;; M400 P2005\n;;; G1 E-2 F{UNWIND_SPEED_}\n;;; M400 P414\n{endif}\n\n; G1 X{x_after_toolchange} Y{y_after_toolchange} F12000\n; G1 Z{toolchange_z} F1200\n;;; G1 E2 F1800\n; SET_VELOCITY_LIMIT VELOCITY=450 ACCEL=10000\n;;; M400 P0\n;_GP_INLINE_ESTIMATED_PRINTING_TIME_PLACEHOLDER\n; FLUSH_END", "cooling_tube_length": "0", "cooling_tube_retraction": "0", "deretraction_speed": [ From 78fb4f767ad917cc909fe9472adffd851f22a0dc Mon Sep 17 00:00:00 2001 From: SoftFever Date: Tue, 29 Sep 2026 01:26:10 +0800 Subject: [PATCH 2/3] Fix the Wanhao France D12 template custom G-code failing to parse --- resources/profiles/Wanhao France.json | 2 +- .../profiles/Wanhao France/machine/fdm_machine_common.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/profiles/Wanhao France.json b/resources/profiles/Wanhao France.json index 9087899761..6392daea8d 100644 --- a/resources/profiles/Wanhao France.json +++ b/resources/profiles/Wanhao France.json @@ -1,6 +1,6 @@ { "name": "Wanhao France", - "version": "02.04.00.05", + "version": "02.04.00.06", "force_update": "0", "description": "Wanhao France D12 configurations", "machine_model_list": [ diff --git a/resources/profiles/Wanhao France/machine/fdm_machine_common.json b/resources/profiles/Wanhao France/machine/fdm_machine_common.json index e72b821fa1..dc3764c3ef 100644 --- a/resources/profiles/Wanhao France/machine/fdm_machine_common.json +++ b/resources/profiles/Wanhao France/machine/fdm_machine_common.json @@ -112,7 +112,7 @@ "1" ], "support_multi_bed_types": "1", - "template_custom_gcode": ";;;;;;;;;;;;;;;;;;;;;;;;CUSTOM G-CODE;;;;;;;;;;;;;;;;;;;;;;;;\n{if curr_bed_type==\"Textured PEI Plate\"}\n SET_GCODE_OFFSET Z=-0.00\n{else}\nSET_GCODE_OFFSET Z=0.0\n{endif}\n{if curr_bed_type==\"Cool Plate\"}\n\n;available bed types are:\n;\"Cool Plate\"\n;\"Engineering Plate\"\n;\"High Temp Plate\"\n;\"Textured PEI Plate\"\n;;;;;;;;;;;;;;;;;;;;;;;;;CUSTOM G-CODE;;;;;;;;;;;;;;;;;;;;;;;;\n", + "template_custom_gcode": ";;;;;;;;;;;;;;;;;;;;;;;;CUSTOM G-CODE;;;;;;;;;;;;;;;;;;;;;;;;\n{if curr_bed_type==\"Textured PEI Plate\"}\n SET_GCODE_OFFSET Z=-0.00\n{else}\nSET_GCODE_OFFSET Z=0.0\n{endif}\n\n;available bed types are:\n;\"Cool Plate\"\n;\"Engineering Plate\"\n;\"High Temp Plate\"\n;\"Textured PEI Plate\"\n;;;;;;;;;;;;;;;;;;;;;;;;;CUSTOM G-CODE;;;;;;;;;;;;;;;;;;;;;;;;\n", "z_hop_types": "Normal Lift", "before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n", "default_print_profile": "0.16mm Optimal @Bambu Lab X1 Carbon 0.4 nozzle", From 185cfe4323e1b527754031a59bb9fc8b69dad082 Mon Sep 17 00:00:00 2001 From: SoftFever Date: Tue, 29 Sep 2026 01:26:10 +0800 Subject: [PATCH 3/3] Slice-validate every custom G-code and filename_format in system profiles --- .github/workflows/build_all.yml | 8 +- .github/workflows/check_profiles.yml | 6 +- .../OrcaSlicer_profile_validator.cpp | 369 +++++++++++++----- src/libslic3r/PlaceholderParser.cpp | 45 ++- src/libslic3r/PlaceholderParser.hpp | 8 + tests/libslic3r/test_placeholder_parser.cpp | 35 ++ 6 files changed, 359 insertions(+), 112 deletions(-) diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml index 24317b0add..d45ec215b8 100644 --- a/.github/workflows/build_all.yml +++ b/.github/workflows/build_all.yml @@ -183,9 +183,11 @@ jobs: os: ${{ vars.SELF_HOSTED && 'orca-macos-arm64' || 'macos-14' }} artifact: ${{ github.sha }}-tests-macos-arm64 test-dir: build/arm64/tests - # Slice a two-colour cube through every shipped printer so all custom g-code - # (change_filament_gcode, machine start/end, etc.) is expanded - catches - # slicing regressions the static profile checks and unit tests can't see. + # Slice a two-colour cube through every shipped printer, and through every + # system process/filament whose templates no printer's own slice reaches, so + # every custom g-code and filename_format shipped is expanded (names in {if} + # branches not taken included) - catches slicing regressions the static + # profile checks and unit tests can't see. # Profile-only PRs are covered by check_profiles.yml's nightly binary; this # covers src/engine PRs with the PR-built binary. slice_check_linux: diff --git a/.github/workflows/check_profiles.yml b/.github/workflows/check_profiles.yml index e668482bfd..ada123c609 100644 --- a/.github/workflows/check_profiles.yml +++ b/.github/workflows/check_profiles.yml @@ -71,8 +71,10 @@ jobs: set +e ./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 2>&1 | tee ${{ runner.temp }}/validate_system.log exit ${PIPESTATUS[0]} - # Slice a two-colour cube through every printer so all custom g-code (incl. change_filament_gcode) - # is expanded - catches undefined-placeholder / invalid-flow bugs the static checks above cannot see. + # Slice a two-colour cube through every printer, and through every system process/filament whose + # templates no printer's own slice reaches, so every custom g-code and filename_format shipped is + # expanded (names in {if} branches not taken included) - catches undefined-placeholder / + # invalid-flow bugs the static checks above cannot see. - name: validate slice (expand custom g-code) id: validate_slice continue-on-error: true diff --git a/src/dev-utils/OrcaSlicer_profile_validator.cpp b/src/dev-utils/OrcaSlicer_profile_validator.cpp index 557dda7eec..4bb7eab64d 100644 --- a/src/dev-utils/OrcaSlicer_profile_validator.cpp +++ b/src/dev-utils/OrcaSlicer_profile_validator.cpp @@ -20,6 +20,7 @@ #include "libslic3r/Model.hpp" #include "libslic3r/TriangleMesh.hpp" #include "libslic3r/Utils.hpp" +#include #include #include #include @@ -32,6 +33,7 @@ #include #include #include +#include #include using namespace Slic3r; @@ -160,37 +162,62 @@ Vec2d place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d ¢er) return rigid ? move : Vec2d::Zero(); } -// Slice one cube that switches from filament 1 to filament 2 partway up, so exactly one -// filament change fires, then export. The change drives the printer's own change_filament_gcode: on a -// single-nozzle machine it rides the AMS prime tower (append_tcr), on a multi-nozzle machine it routes -// through the nozzle swap (set_extruder / append_tcr2) - the engine picks the path from the printer's -// topology, so one model covers both. An undefined placeholder in any shipped custom g-code throws -// Slic3r::PlaceholderParserError from export. -std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl) +// Slice cubes that switch from filament 1 to filament 2 partway up, so a filament change fires, then +// export. The change drives the printer's own change_filament_gcode: on a single-nozzle machine it rides +// the AMS prime tower (append_tcr), on a multi-nozzle machine it routes through the nozzle swap +// (set_extruder / append_tcr2) - the engine picks the path from the printer's topology, so one model +// covers both. The layer-by-layer slice also pauses at z 2 and runs the template custom g-code at z 7, so +// machine_pause_gcode and template_custom_gcode expand, and turns clumping detection on for a printer that +// ships wrapping_detection_gcode. The by-object slice prints two cubes in turn without a prime tower: +// printing_by_object_gcode fires before the second one, and the filament change goes through set_extruder. +// Either way the output file name is built after export, which expands filename_format with the final +// print statistics. An undefined placeholder in any shipped custom g-code throws +// Slic3r::PlaceholderParserError. +std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl, bool by_object) { - const Vec2d center = printable_area_center(cfg); - const Vec2d cube_min = center - Vec2d(5., 5.) + place_wipe_tower(cfg, center); - TriangleMesh m = make_cube(10, 10, 10); - m.translate(static_cast(cube_min.x()), static_cast(cube_min.y()), 0.f); + const Vec2d center = printable_area_center(cfg); + std::vector cube_mins; + if (by_object) { + // By-object printing fires the hook only without a wipe tower, and rules out clumping detection and + // smooth timelapse. No skirt, so the two cubes' own skirts cannot overlap. + cfg.set_key_value("print_sequence", new ConfigOptionEnum(PrintSequence::ByObject)); + cfg.set_key_value("enable_prime_tower", new ConfigOptionBool(false)); + cfg.set_key_value("enable_wrapping_detection", new ConfigOptionBool(false)); + cfg.set_key_value("timelapse_type", new ConfigOptionEnum(tlTraditional)); + cfg.set_key_value("skirt_loops", new ConfigOptionInt(0)); + cube_mins = {center + Vec2d(-20., -5.), center + Vec2d(10., -5.)}; + } else { + // Clumping detection changes the tower footprint, so turn it on before placing the tower. + if (!cfg.opt_string("wrapping_detection_gcode").empty()) + cfg.set_key_value("enable_wrapping_detection", new ConfigOptionBool(true)); + cube_mins = {center - Vec2d(5., 5.) + place_wipe_tower(cfg, center)}; + } Model model; Print print; - ModelObject *obj = model.add_object(); - obj->name = "cube"; // populates [input_filename_base] the way a loaded model does - obj->add_volume(m); - obj->add_instance(); - // Filament 2 is used only above z=4, so the upper layers carry a single filament change. - DynamicPrintConfig range_config; - range_config.set_key_value("extruder", new ConfigOptionInt(2)); - // Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm - // printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that - // trips Flow::with_spacing. - range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height"))); - obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config)); - print.is_BBL_printer() = is_bbl; - obj->ensure_on_bed(); - print.auto_assign_extruders(obj); + for (const Vec2d &cube_min : cube_mins) { + TriangleMesh m = make_cube(10, 10, 10); + m.translate(static_cast(cube_min.x()), static_cast(cube_min.y()), 0.f); + ModelObject *obj = model.add_object(); + obj->name = "cube"; // populates [input_filename_base] the way a loaded model does + obj->add_volume(m); + obj->add_instance(); + // Filament 2 is used only above z=4, so the upper layers carry a single filament change. + DynamicPrintConfig range_config; + range_config.set_key_value("extruder", new ConfigOptionInt(2)); + // Every range must carry a layer_height; use the process's own so a fine nozzle (e.g. 0.15 mm + // printing ~0.1 mm layers) isn't forced to a height its extrusion width can't support - that + // trips Flow::with_spacing. + range_config.set_key_value("layer_height", new ConfigOptionFloat(cfg.opt_float("layer_height"))); + obj->layer_config_ranges[{4.0, 10.0}].assign_config(std::move(range_config)); + obj->ensure_on_bed(); + print.auto_assign_extruders(obj); + } + // Custom g-codes per print_z apply to layer-by-layer printing only (ToolOrdering::assign_custom_gcodes). + if (!by_object) + model.plates_custom_gcodes[model.curr_plate_index].gcodes = {{2., CustomGCode::PausePrint, 1, "", ""}, + {7., CustomGCode::Template, 1, "", ""}}; print.apply(model, cfg); print.validate(); @@ -205,6 +232,7 @@ std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl) in.close(); boost::system::error_code ec; fs::remove(tmp, ec); + print.output_filename(); // names the export as the app does, expanding filename_format return out; } @@ -264,18 +292,119 @@ void install_slice_context_log_sink() logging::core::get()->add_sink(sink); } +// Size the filament slots for the current selection and build the config every slice uses. +DynamicPrintConfig slice_config(PresetBundle &bundle) +{ + // Grow to a 2nd filament so the cube can change colour; never shrink a multi-nozzle printer + // below its nozzle count, or full_config()'s flush-volume matrix no longer matches validate(). + const size_t nozzles = bundle.printers.get_selected_preset().config.option("nozzle_diameter")->size(); + bundle.set_num_filaments((unsigned int) std::max(2, nozzles)); + + // Mirror the app's manual filament->nozzle assignment for a multi-nozzle BBL printer: put each + // filament on its own nozzle and pin the map (fmmManual) so full_config() collapses every filament to + // the variant of the nozzle it actually prints from, and the engine keeps that assignment instead of + // auto-remapping it during process(). Without this the synthetic 2nd filament keeps nozzle 1's variant + // while the auto map moves it to nozzle 2 - harmless, but on the one printer whose nozzles differ in + // type (Direct Drive + Bowden) the mismatched lookup spams [error] lines. Single-nozzle and non-BBL + // printers keep the default map (their toolchange rides the AMS/tool-changer path unchanged). + const bool pin_filament_map = bundle.is_bbl_vendor() && nozzles > 1; + if (pin_filament_map) { + auto &fmap = bundle.project_config.option("filament_map", true)->values; + for (size_t i = 0; i < fmap.size(); ++i) + fmap[i] = int(i % nozzles) + 1; + } + + DynamicPrintConfig cfg = bundle.full_config(); + cfg.set_key_value("enable_prime_tower", new ConfigOptionBool(true)); // force a purge tower so the change is detectable + // The map above drives full_config()'s per-filament variant collapse; fmmManual on the sliced config + // stops process() from auto-remapping filaments back onto a different nozzle (which would re-introduce + // the variant mismatch this pinning avoids). + if (pin_filament_map) + cfg.set_key_value("filament_map_mode", new ConfigOptionEnum(fmmManual)); + + // full_config() grows filament_extruder_variant to one entry per filament, but because the synthetic + // 2nd filament is a duplicate of the first (set_num_filaments copies the same preset), it leaves + // filament_self_index at size 1. That makes update_values_to_printer_extruders_for_multiple_filaments + // fail to resolve the 2nd filament's variant - a benign fallback that spams [error] lines. A real + // 2-colour project ships filament_self_index = 1,2,...; mirror that so the sweep log stays clean. The + // slice output is unaffected: the duplicated filament's per-variant values are identical to the first. + if (auto *variants = cfg.option("filament_extruder_variant")) { + auto &self_index = cfg.option("filament_self_index", true)->values; + if (self_index.size() != variants->size()) { + self_index.resize(variants->size()); + for (size_t i = 0; i < self_index.size(); ++i) + self_index[i] = int(i) + 1; + } + } + return cfg; +} + +// Slice the current selection and log any failure against `what` (the printer, and the extra preset if +// any). With an outdir, the g-code is also saved there as ".gcode". Returns the g-code, or an +// empty string on failure. +std::string slice_selection(PresetBundle &bundle, const std::string &what, bool by_object, const std::string &outdir, const std::string &file_base) +{ + try { + const std::string out = slice_two_color_cube_and_export(slice_config(bundle), bundle.is_bbl_vendor(), by_object); + if (!outdir.empty() && !out.empty()) + save_string_file(fs::path(outdir) / (file_base + ".gcode"), out); + if (out.empty() || out.find("G1") == std::string::npos) { + BOOST_LOG_TRIVIAL(error) << what << " produced no g-code"; + return {}; + } + return out; + } catch (const std::exception &ex) { + BOOST_LOG_TRIVIAL(error) << what << " failed to slice: " << ex.what(); + return {}; + } +} + +// The templates a preset carries, as (key, text): every non-empty *_gcode text, and filename_format. Each +// value of a per-variant key counts, though a slice expands only the values of the variants its printer uses. +using Templates = std::set>; +Templates preset_templates(const DynamicPrintConfig &cfg) +{ + Templates out; + for (const std::string &key : cfg.keys()) { + if (key != "filename_format" && !boost::algorithm::ends_with(key, "_gcode")) + continue; + const ConfigOption *opt = cfg.option(key); + if (opt->type() == coString) { + if (const std::string &text = static_cast(opt)->value; !text.empty()) + out.emplace(key, text); + } else if (opt->type() == coStrings) { + for (const std::string &text : static_cast(opt)->values) + if (!text.empty()) + out.emplace(key, text); + } + } + return out; +} + // Slice-and-export a two-colour cube through every shipped printer (optionally scoped to one vendor via // -v). Unlike the static reference/placeholder checks, this expands every custom *_gcode - including // change_filament_gcode at the one filament change - against the printer's fully-resolved config, so -// undefined-placeholder / invalid-flow bugs surface here. Reports every offending printer and returns 1 -// if any failed, 0 otherwise. When outdir is non-empty, each printer's g-code is also written there as -// "__.gcode" for manual inspection. The sweep is SEQUENTIAL by necessity: -// Print::process() keeps process-global state, so slicing printers concurrently in one process races -// even with per-slice Model+Print. Load in validation mode so the vendors are read straight from the -p -// profiles dir (no data_dir/system tree) and -v scoping is honoured for free. +// undefined-placeholder / invalid-flow bugs surface here. PlaceholderParser::check_inactive_branches is +// set for the sweep, so the names in {if} branches a slice does not take must resolve too. +// +// Each printer is sliced with its own default process and filament. Every other compatible system +// process and filament whose templates (filename_format, filament_start_gcode, ...) no slice has expanded +// yet is then sliced once on that printer - a filament that compatible_prints limits to another process +// with that process - so each template text shipped in any system preset is expanded once. That check runs on the first compatible printer the sweep reaches only, in that printer's hook +// contexts: a value-dependent error on another printer, or a hook call site there that sets fewer +// variables (a multi-nozzle tool change, say), is not covered. The first printer shipping each +// printing_by_object_gcode also slices two cubes by object, the only way that hook fires. +// +// Reports every failure and returns 1 if any slice failed, 0 otherwise. When outdir is non-empty, each +// g-code is also written there as "__[__].gcode" for manual inspection. The +// sweep is SEQUENTIAL by necessity: Print::process() keeps process-global state, so slicing printers +// concurrently in one process races even with per-slice Model+Print. Load in validation mode so the +// vendors are read straight from the -p profiles dir (no data_dir/system tree) and -v scoping is honoured +// for free. int slice_all_printers(const std::string &vendor, const std::string &outdir) { install_slice_context_log_sink(); + PlaceholderParser::check_inactive_branches = true; if (!outdir.empty()) { boost::system::error_code ec; @@ -323,7 +452,16 @@ int slice_all_printers(const std::string &vendor, const std::string &outdir) std::cout << "Slicing " << printers.size() << " printer preset(s)" << (vendor.empty() ? "" : " for vendor " + vendor) << "..." << std::endl; - int failures = 0; + Templates covered; // the process and filament templates some slice has expanded + auto cover = [&covered](const Preset &preset) { + const Templates templates = preset_templates(preset.config); + covered.insert(templates.begin(), templates.end()); + }; + auto is_covered = [&covered](const Preset &preset) { + const Templates templates = preset_templates(preset.config); + return std::includes(covered.begin(), covered.end(), templates.begin(), templates.end()); + }; + int failures = 0, extra_processes = 0, extra_filaments = 0, by_object_slices = 0; for (const auto &[vendor_name, printer] : printers) { g_slice_context = vendor_name + " / " + printer; // tag every engine log line from this slice const bool selected = bundle.printers.select_preset_by_name(printer, /*force=*/true); @@ -347,76 +485,115 @@ int slice_all_printers(const std::string &vendor, const std::string &outdir) continue; } - // Grow to a 2nd filament so the cube can change colour; never shrink a multi-nozzle printer - // below its nozzle count, or full_config()'s flush-volume matrix no longer matches validate(). - const size_t nozzles = bundle.printers.get_selected_preset().config.option("nozzle_diameter")->size(); - bundle.set_num_filaments((unsigned int) std::max(2, nozzles)); - - // Mirror the app's manual filament->nozzle assignment for a multi-nozzle BBL printer: put each - // filament on its own nozzle and pin the map (fmmManual) so full_config() collapses every filament to - // the variant of the nozzle it actually prints from, and the engine keeps that assignment instead of - // auto-remapping it during process(). Without this the synthetic 2nd filament keeps nozzle 1's variant - // while the auto map moves it to nozzle 2 - harmless, but on the one printer whose nozzles differ in - // type (Direct Drive + Bowden) the mismatched lookup spams [error] lines. Single-nozzle and non-BBL - // printers keep the default map (their toolchange rides the AMS/tool-changer path unchanged). - const bool pin_filament_map = bundle.is_bbl_vendor() && nozzles > 1; - if (pin_filament_map) { - auto &fmap = bundle.project_config.option("filament_map", true)->values; - for (size_t i = 0; i < fmap.size(); ++i) - fmap[i] = int(i % nozzles) + 1; - } - - DynamicPrintConfig cfg = bundle.full_config(); - cfg.set_key_value("enable_prime_tower", new ConfigOptionBool(true)); // force a purge tower so the change is detectable - // The map above drives full_config()'s per-filament variant collapse; fmmManual on the sliced config - // stops process() from auto-remapping filaments back onto a different nozzle (which would re-introduce - // the variant mismatch this pinning avoids). - if (pin_filament_map) - cfg.set_key_value("filament_map_mode", new ConfigOptionEnum(fmmManual)); - - // full_config() grows filament_extruder_variant to one entry per filament, but because the synthetic - // 2nd filament is a duplicate of the first (set_num_filaments copies the same preset), it leaves - // filament_self_index at size 1. That makes update_values_to_printer_extruders_for_multiple_filaments - // fail to resolve the 2nd filament's variant - a benign fallback that spams [error] lines. A real - // 2-colour project ships filament_self_index = 1,2,...; mirror that so the sweep log stays clean. The - // slice output is unaffected: the duplicated filament's per-variant values are identical to the first. - if (auto *variants = cfg.option("filament_extruder_variant")) { - auto &self_index = cfg.option("filament_self_index", true)->values; - if (self_index.size() != variants->size()) { - self_index.resize(variants->size()); - for (size_t i = 0; i < self_index.size(); ++i) - self_index[i] = int(i) + 1; - } - } - - try { - const std::string out = slice_two_color_cube_and_export(cfg, bundle.is_bbl_vendor()); - if (!outdir.empty() && !out.empty()) { - const fs::path f = fs::path(outdir) / (sanitize_filename(vendor_name) + "__" + sanitize_filename(printer) + ".gcode"); - save_string_file(f, out); - } - if (out.empty() || out.find("G1") == std::string::npos) { - BOOST_LOG_TRIVIAL(error) << "Printer \"" << printer << "\" produced no g-code"; - ++failures; - } else if (out.find("CP TOOLCHANGE START") == std::string::npos) { - // The filament change never rode the tower, so change_filament_gcode was not exercised. - BOOST_LOG_TRIVIAL(error) << "Printer \"" << printer - << "\" sliced but the filament change never fired (no CP TOOLCHANGE START)"; - ++failures; - } - } catch (const std::exception &ex) { - BOOST_LOG_TRIVIAL(error) << "Printer \"" << printer << "\" failed to slice: " << ex.what(); + const std::string print_name = bundle.prints.get_selected_preset_name(); + const std::string filament_name = bundle.filaments.get_selected_preset_name(); + const std::string what = "Printer \"" + printer + "\""; + const std::string file_base = sanitize_filename(vendor_name) + "__" + sanitize_filename(printer); + if (const std::string out = slice_selection(bundle, what, false, outdir, file_base); out.empty()) + ++failures; + else if (out.find("CP TOOLCHANGE START") == std::string::npos) { + // The filament change never rode the tower, so change_filament_gcode was not exercised. + BOOST_LOG_TRIVIAL(error) << what << " sliced but the filament change never fired (no CP TOOLCHANGE START)"; ++failures; } + cover(bundle.prints.get_selected_preset()); + cover(bundle.filaments.get_selected_preset()); + + const std::string &by_object_gcode = bundle.printers.get_selected_preset().config.opt_string("printing_by_object_gcode"); + if (!by_object_gcode.empty() && covered.emplace("printing_by_object_gcode", by_object_gcode).second) { + ++by_object_slices; + g_slice_context = vendor_name + " / " + printer + " / by object"; + if (slice_selection(bundle, what + " printing by object", true, outdir, file_base + "__by_object").empty()) + ++failures; + } + + // The first process of this printer that compatible_prints lets an otherwise incompatible filament + // use, judged as update_compatible() would with that process selected; empty if there is none. + const PresetWithVendorProfile printer_with_vendor = bundle.printers.get_edited_preset_with_vendor_profile(); + auto limiting_process = [&bundle, &printer_with_vendor](const Preset &filament) -> std::string { + const auto *processes = filament.config.option("compatible_prints"); + if (processes == nullptr || processes->values.empty()) + return {}; + const PresetWithVendorProfile filament_with_vendor = bundle.filaments.get_preset_with_vendor_profile(filament); + if (!is_compatible_with_printer(filament_with_vendor, printer_with_vendor)) + return {}; + for (const std::string &name : processes->values) + if (const Preset *process = bundle.prints.find_preset(name); + process != nullptr && process->is_visible && process->is_compatible && + is_compatible_with_print(filament_with_vendor, bundle.prints.get_preset_with_vendor_profile(*process), printer_with_vendor)) + return name; + return {}; + }; + // The compatibility flags are still this printer's, from update_compatible() above, which judged the + // filaments against the printer's own process. A hidden preset cannot be selected, so it is counted, + // not sliced. + std::vector> extras; // (extra preset, process to slice it with) + size_t hidden = 0; + for (const PresetCollection *presets : {&bundle.prints, &bundle.filaments}) + for (const Preset &preset : presets->get_presets()) { + if (!preset.is_system || preset.is_default) + continue; + std::string process; + if (preset.is_compatible) + process = preset.type == Preset::TYPE_PRINT ? preset.name : print_name; + else if (preset.type == Preset::TYPE_FILAMENT) + process = limiting_process(preset); + if (process.empty() || is_covered(preset)) + continue; + if (preset.is_visible) + extras.emplace_back(&preset, process); + else + ++hidden; + } + if (hidden > 0) + BOOST_LOG_TRIVIAL(warning) << "Printer \"" << printer << "\": " << hidden + << " compatible system preset(s) with unexpanded templates are hidden and were not sliced"; + + // Each extra slice swaps one preset into the printer's own selection. update_compatible() is not + // run again: it could swap filament slots. The filament change is not required to ride the tower + // here (a vase-mode process has none): the printer's own slice above already expanded its hooks. + for (const auto &[preset, process] : extras) { + if (is_covered(*preset)) + continue; // an earlier extra slice expanded the same texts + const bool is_print = preset->type == Preset::TYPE_PRINT; + const std::string &filament = is_print ? filament_name : preset->name; + std::string extra = std::string(is_print ? "process" : "filament") + " \"" + preset->name + "\""; + if (!is_print && process != print_name) + extra += " and process \"" + process + "\""; + g_slice_context = vendor_name + " / " + printer + " / " + extra; + if (is_print) + ++extra_processes; + else + ++extra_filaments; + cover(*preset); + bundle.prints.select_preset_by_name(process, /*force=*/true); + bundle.filaments.select_preset_by_name(filament, /*force=*/true); + bundle.filament_presets.assign(1, bundle.filaments.get_selected_preset_name()); + bundle.update_multi_material_filament_presets(); + // select_preset_by_name() falls back to another preset, and still returns true, when it cannot select this one. + if (bundle.prints.get_selected_preset_name() != process || bundle.filaments.get_selected_preset_name() != filament) { + BOOST_LOG_TRIVIAL(error) << what << " could not select " << extra; + ++failures; + continue; + } + if (slice_selection(bundle, what + " with " + extra, false, outdir, file_base + "__" + sanitize_filename(preset->name)).empty()) + ++failures; + } + // Leave the printer's own selection for the next printer to start from, as without the extra slices. + bundle.prints.select_preset_by_name(print_name, /*force=*/true); + bundle.filaments.select_preset_by_name(filament_name, /*force=*/true); } g_slice_context.clear(); + const int slices = int(printers.size()) + extra_processes + extra_filaments + by_object_slices; + std::cout << "Sliced " << printers.size() << " printer preset(s), " << extra_processes << " more process preset(s), " + << extra_filaments << " more filament preset(s) and " << by_object_slices << " printer(s) by object" << std::endl; if (failures > 0) { - std::cout << failures << " of " << printers.size() << " printer preset(s) failed to slice" << std::endl; + std::cout << failures << " of " << slices << " slice(s) failed" << std::endl; std::cout << "Validation failed" << std::endl; return 1; } - std::cout << "All " << printers.size() << " printer preset(s) sliced successfully" << std::endl; + std::cout << "All " << slices << " slice(s) succeeded" << std::endl; std::cout << "Validation completed successfully" << std::endl; return 0; } @@ -435,8 +612,8 @@ int main(int argc, char* argv[]) #endif ("vendor,v", po::value()->default_value(""), "Vendor name. Optional, all profiles present in the folder will be validated if not specified") ("generate_presets,g", po::value()->default_value(false), "Generate user presets for mock test") - ("slice,s", po::bool_switch()->default_value(false), "Slice a two-colour cube through every printer to expand all custom g-code (catches placeholder/flow errors that static checks miss). Off unless this flag is present.") - ("outdir,o", po::value()->default_value(""), "With -s, also save each printer's g-code to this folder (as __.gcode) for manual inspection. Optional.") + ("slice,s", po::bool_switch()->default_value(false), "Slice a two-colour cube through every printer, and through every other system process and filament preset whose templates no printer's own slice expands, so every custom g-code and filename_format shipped is expanded, names in {if} branches not taken included (catches placeholder/flow errors that static checks miss). Off unless this flag is present.") + ("outdir,o", po::value()->default_value(""), "With -s, also save each slice's g-code to this folder (as __[__].gcode) for manual inspection. Optional.") ("check_filament_subtypes,f", po::bool_switch()->default_value(true), "Also flag printers with duplicate (ambiguous) filament subtypes. Off unless this flag is present.") ("log_level,l", po::value()->default_value(2), "Log level. Optional, default is 2 (warning). Higher values produce more detailed logs."); // clang-format on diff --git a/src/libslic3r/PlaceholderParser.cpp b/src/libslic3r/PlaceholderParser.cpp index 3e29e0c172..1d7abcd5de 100644 --- a/src/libslic3r/PlaceholderParser.cpp +++ b/src/libslic3r/PlaceholderParser.cpp @@ -851,6 +851,8 @@ namespace client // If true, the macro processor will evaluate just a boolean condition using the full expressive power of the macro processor. bool just_boolean_expression = false; std::string error_message; + // Local variables declared in {if} branches that were not taken, see PlaceholderParser::check_inactive_branches. + mutable std::set inactive_local_variables; // Table to translate symbol tag to a human readable error message. static std::map tag_to_error_message; @@ -892,6 +894,8 @@ namespace client } // Inside a block, which is conditionally suppressed? bool skipping() const { return m_depth_suppressed > 0; } + // Are variable names resolved inside the suppressed blocks too? + bool check_inactive_names() const { return PlaceholderParser::check_inactive_branches && ! just_boolean_expression; } const ConfigOption* optptr(const t_config_option_key &opt_key) const override { @@ -927,7 +931,7 @@ namespace client static void legacy_variable_expansion(const MyContext *ctx, IteratorRange &opt_key, std::string &output) { - if (ctx->skipping()) + if (ctx->skipping() && ! ctx->check_inactive_names()) return; std::string opt_key_str(opt_key.begin(), opt_key.end()); @@ -949,7 +953,9 @@ namespace client } } if (opt == nullptr) - ctx->throw_exception("Variable does not exist", opt_key); + ctx->throw_exception(ctx->skipping() ? "Variable does not exist (in an inactive branch)" : "Variable does not exist", opt_key); + if (ctx->skipping()) + return; if (opt->is_scalar()) { if (opt->is_nil()) ctx->throw_exception("Trying to reference an undefined (nil) optional variable", opt_key); @@ -972,9 +978,10 @@ namespace client IteratorRange &opt_vector_index, std::string &output) { - if (ctx->skipping()) + if (ctx->skipping() && ! ctx->check_inactive_names()) return; + const char *not_found = ctx->skipping() ? "Variable does not exist (in an inactive branch)" : "Variable does not exist"; std::string opt_key_str(opt_key.begin(), opt_key.end()); const ConfigOption *opt = ctx->resolve_symbol(opt_key_str); if (opt == nullptr) { @@ -984,18 +991,20 @@ namespace client opt = ctx->resolve_symbol(opt_key_str); } if (opt == nullptr) - ctx->throw_exception("Variable does not exist", opt_key); + ctx->throw_exception(not_found, opt_key); } if (! opt->is_vector()) ctx->throw_exception("Trying to index a scalar variable", opt_key); + const ConfigOption *opt_index = ctx->resolve_symbol(std::string(opt_vector_index.begin(), opt_vector_index.end())); + if (opt_index == nullptr) + ctx->throw_exception(not_found, opt_key); + if (opt_index->type() != coInt) + ctx->throw_exception("Indexing variable has to be integer", opt_key); + if (ctx->skipping()) + return; const ConfigOptionVectorBase *vec = static_cast(opt); if (vec->empty()) ctx->throw_exception("Indexing an empty vector variable", opt_key); - const ConfigOption *opt_index = ctx->resolve_symbol(std::string(opt_vector_index.begin(), opt_vector_index.end())); - if (opt_index == nullptr) - ctx->throw_exception("Variable does not exist", opt_key); - if (opt_index->type() != coInt) - ctx->throw_exception("Indexing variable has to be integer", opt_key); int idx = opt_index->getInt(); if (idx < 0) ctx->throw_exception("Negative vector index", opt_key); @@ -1021,6 +1030,13 @@ namespace client output.writable = true; } output.opt = opt; + } else if (ctx->check_inactive_names()) { + // Only check the name. Back tracking may resolve the same identifier twice, so there are no side effects. + const std::string key{ opt_key.begin(), opt_key.end() }; + if (ctx->resolve_symbol(key) == nullptr && ctx->resolve_output_symbol(key) == nullptr && + ctx->inactive_local_variables.count(key) == 0 && + (ctx->context_data == nullptr || ctx->context_data->inactive_global_variables.count(key) == 0)) + ctx->throw_exception("Not a variable name (in an inactive branch)", opt_key); } output.it_range = opt_key; } @@ -1426,6 +1442,13 @@ namespace client out.opt = ctx->config_local.optptr(key); } out.name = std::move(key); + } else if (ctx->check_inactive_names()) { + // Declared in a branch that is not taken: the name still counts as defined for the names that follow. + std::string key(it_range.begin(), it_range.end()); + if (global_variable && ctx->context_data != nullptr) + ctx->context_data->inactive_global_variables.insert(std::move(key)); + else + ctx->inactive_local_variables.insert(std::move(key)); } out.it_range = it_range; } @@ -1660,8 +1683,8 @@ namespace client const OptWithPos &rhs) { if (ctx->skipping()) - // Skipping, continue parsing. - return true; + // Skipping, let conditional_expression parse the whole right hand side, which may continue after the variable reference. + return false; if (lhs.opt) { assert(lhs.opt->is_vector()); diff --git a/src/libslic3r/PlaceholderParser.hpp b/src/libslic3r/PlaceholderParser.hpp index 3f3998dc5a..e32a4de9dc 100644 --- a/src/libslic3r/PlaceholderParser.hpp +++ b/src/libslic3r/PlaceholderParser.hpp @@ -4,6 +4,7 @@ #include "libslic3r.h" #include #include +#include #include #include #include @@ -24,8 +25,15 @@ public: // If defined, then this dictionary is used by the scripts to define user variables and persist them // between PlaceholderParser evaluations. std::unique_ptr global_config; + // Global variables declared in {if} branches that were not taken, see check_inactive_branches. + std::set inactive_global_variables; }; + // Orca: when set, variable names inside {if} branches that are not taken must resolve too, so a single + // expansion checks every branch of a template. Only the profile validator's slice sweep sets it. + // It does not apply to evaluate_boolean_expression(), where an error reads as "compatible". + static inline bool check_inactive_branches = false; + PlaceholderParser(const DynamicConfig *external_config = nullptr); void clear_config() { m_config.clear(); } diff --git a/tests/libslic3r/test_placeholder_parser.cpp b/tests/libslic3r/test_placeholder_parser.cpp index 14716b0b5e..4a95156c34 100644 --- a/tests/libslic3r/test_placeholder_parser.cpp +++ b/tests/libslic3r/test_placeholder_parser.cpp @@ -356,3 +356,38 @@ SCENARIO("Placeholder parser coFloatsOrPercents vector access", "[PlaceholderPar REQUIRE(std::stod(parser.process("{pressure_advance[2]}")) == Catch::Approx(3.0)); } } + +SCENARIO("Placeholder parser names in branches that are not taken", "[PlaceholderParser]") { + PlaceholderParser parser; + auto config = DynamicPrintConfig::full_print_config(); + parser.apply_config(config); + parser.set("idx", 0); + PlaceholderParser::ContextData context; + context.global_config = std::make_unique(); + auto process = [&parser, &context](const std::string &templ) { return parser.process(templ, 0, nullptr, nullptr, &context); }; + + SECTION("a declaration continuing after a variable reference parses when not taken") { + REQUIRE(process("{if false}{local a = layer_height + 1}{endif}ok") == "ok"); + } + SECTION("names are not checked by default") { + REQUIRE(process("{if false}{no_such_var}[no_such_var]{endif}ok") == "ok"); + } + SECTION("names must resolve when check_inactive_branches is set") { + struct Restore { ~Restore() { PlaceholderParser::check_inactive_branches = false; } } restore; + PlaceholderParser::check_inactive_branches = true; + + CHECK_THROWS_WITH(process("{if false}{no_such_var}{endif}"), Catch::Matchers::ContainsSubstring("Not a variable name (in an inactive branch)")); + CHECK_THROWS_WITH(process("{if false}[no_such_var]{endif}"), Catch::Matchers::ContainsSubstring("Variable does not exist (in an inactive branch)")); + CHECK_THROWS_WITH(process("{if false}[nozzle_temperature[no_such_var]]{endif}"), Catch::Matchers::ContainsSubstring("Variable does not exist (in an inactive branch)")); + CHECK_THROWS(process("{if true}{else}{no_such_var}{endif}")); + CHECK_THROWS(process("{if false}{local a = no_such_var + 1}{endif}")); + + CHECK(process("{if false}{layer_height}[layer_height][nozzle_temperature_0][nozzle_temperature[idx]]{endif}ok") == "ok"); + CHECK(process("{if false}{local a = 1}{a = a + 1}{a}{endif}{if false}{a}{endif}ok") == "ok"); + // A global declared in a branch that is not taken counts as defined for later expansions sharing the context. + CHECK(process("{if false}{global g = 1}{endif}{if false}{g}{endif}ok") == "ok"); + CHECK(process("{if false}{g}{endif}ok") == "ok"); + // Boolean expressions are not checked, so compatibility conditions keep their behaviour. + CHECK(PlaceholderParser::evaluate_boolean_expression("false ? no_such_var == 1 : true", config)); + } +}