mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-30 12:21:05 +00:00
Slice-validate every custom G-code and filename_format in system profiles (#15966)
# Description Follow-up to #15950, where a `filename_format` using `initial_no_support_extruder` shipped broken because the profile validator's slice sweep never expands `filename_format`. The sweep now expands every custom G-code and `filename_format` text shipped in any system profile. Each printer's slice also fires the pause, template custom G-code and clumping-detection hooks and names the output file, and the first printer shipping a `printing_by_object_gcode` also slices by object. Beyond each printer's default process and filament, every compatible system process and filament carrying a template text no earlier slice has expanded is sliced once, which takes the sweep from 1,110 to 1,248 slices (about 49 s locally, up from 43 s). While the sweep runs, the placeholder parser also resolves variable names inside `{if}` branches a slice does not take, so one expansion checks every branch. The stricter sweep found two profile bugs, fixed here: the Anycubic Kobra X filament change G-code carried an unreachable block reading variables only Anycubic's own slicer defines, and the Wanhao France D12 template custom G-code had an unterminated `{if}`, so adding a template custom G-code on those printers failed the slice. It also fixes a parser bug where a declaration such as `{local a = layer_height + 1}` failed to parse inside a branch that is not taken. No change to slicing output for templates that already worked: the untaken-branch check is enabled only by the validator's slice mode, and the parser fix only lets previously rejected templates parse. # Screenshots/Recordings/Graphs <!-- > Please attach relevant screenshots to showcase the UI changes. > Please attach images that can help explain the changes. --> ## Tests New placeholder-parser cases cover the parse fix and the untaken-branch check: off by default it changes nothing; on, it rejects undefined names in branches not taken, accepts names declared there, and leaves boolean expressions (compatibility conditions) alone. The full sweep passes on all system profiles, and planting an undefined variable in an untaken branch of a printer's start G-code, of a non-default filament's start G-code or of a non-default process's `filename_format`, or reverting #15950, each fails it and names the preset. `scripts/check_profile.sh`, `libslic3r_tests` and `fff_print_tests` pass. <!-- > A guide for users on how to download the artifacts from this PR. --> [How to Download Pull Requests Artifacts for Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
This commit is contained in:
@@ -183,9 +183,11 @@ jobs:
|
|||||||
os: ${{ vars.SELF_HOSTED && 'orca-macos-arm64' || 'macos-14' }}
|
os: ${{ vars.SELF_HOSTED && 'orca-macos-arm64' || 'macos-14' }}
|
||||||
artifact: ${{ github.sha }}-tests-macos-arm64
|
artifact: ${{ github.sha }}-tests-macos-arm64
|
||||||
test-dir: build/arm64/tests
|
test-dir: build/arm64/tests
|
||||||
# Slice a two-colour cube through every shipped printer so all custom g-code
|
# Slice a two-colour cube through every shipped printer, and through every
|
||||||
# (change_filament_gcode, machine start/end, etc.) is expanded - catches
|
# system process/filament whose templates no printer's own slice reaches, so
|
||||||
# slicing regressions the static profile checks and unit tests can't see.
|
# 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
|
# Profile-only PRs are covered by check_profiles.yml's nightly binary; this
|
||||||
# covers src/engine PRs with the PR-built binary.
|
# covers src/engine PRs with the PR-built binary.
|
||||||
slice_check_linux:
|
slice_check_linux:
|
||||||
|
|||||||
@@ -74,8 +74,10 @@ jobs:
|
|||||||
set +e
|
set +e
|
||||||
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 2>&1 | tee ${{ runner.temp }}/validate_system.log
|
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 2>&1 | tee ${{ runner.temp }}/validate_system.log
|
||||||
exit ${PIPESTATUS[0]}
|
exit ${PIPESTATUS[0]}
|
||||||
# Slice a two-colour cube through every printer so all custom g-code (incl. change_filament_gcode)
|
# Slice a two-colour cube through every printer, and through every system process/filament whose
|
||||||
# is expanded - catches undefined-placeholder / invalid-flow bugs the static checks above cannot see.
|
# 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)
|
- name: validate slice (expand custom g-code)
|
||||||
id: validate_slice
|
id: validate_slice
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -112,7 +112,7 @@
|
|||||||
"1"
|
"1"
|
||||||
],
|
],
|
||||||
"support_multi_bed_types": "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",
|
"z_hop_types": "Normal Lift",
|
||||||
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n",
|
"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",
|
"default_print_profile": "0.16mm Optimal @Bambu Lab X1 Carbon 0.4 nozzle",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#include "libslic3r/Model.hpp"
|
#include "libslic3r/Model.hpp"
|
||||||
#include "libslic3r/TriangleMesh.hpp"
|
#include "libslic3r/TriangleMesh.hpp"
|
||||||
#include "libslic3r/Utils.hpp"
|
#include "libslic3r/Utils.hpp"
|
||||||
|
#include <boost/algorithm/string/predicate.hpp>
|
||||||
#include <boost/filesystem/operations.hpp>
|
#include <boost/filesystem/operations.hpp>
|
||||||
#include <boost/log/trivial.hpp>
|
#include <boost/log/trivial.hpp>
|
||||||
#include <boost/log/core.hpp>
|
#include <boost/log/core.hpp>
|
||||||
@@ -32,6 +33,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
using namespace Slic3r;
|
using namespace Slic3r;
|
||||||
@@ -160,37 +162,62 @@ Vec2d place_wipe_tower(DynamicPrintConfig &cfg, const Vec2d ¢er)
|
|||||||
return rigid ? move : Vec2d::Zero();
|
return rigid ? move : Vec2d::Zero();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slice one cube that switches from filament 1 to filament 2 partway up, so exactly one
|
// Slice cubes that switch from filament 1 to filament 2 partway up, so a filament change fires, then
|
||||||
// filament change fires, then export. The change drives the printer's own change_filament_gcode: on a
|
// export. The change drives the printer's own change_filament_gcode: on a single-nozzle machine it rides
|
||||||
// single-nozzle machine it rides the AMS prime tower (append_tcr), on a multi-nozzle machine it routes
|
// the AMS prime tower (append_tcr), on a multi-nozzle machine it routes through the nozzle swap
|
||||||
// through the nozzle swap (set_extruder / append_tcr2) - the engine picks the path from the printer's
|
// (set_extruder / append_tcr2) - the engine picks the path from the printer's topology, so one model
|
||||||
// topology, so one model covers both. An undefined placeholder in any shipped custom g-code throws
|
// covers both. The layer-by-layer slice also pauses at z 2 and runs the template custom g-code at z 7, so
|
||||||
// Slic3r::PlaceholderParserError from export.
|
// machine_pause_gcode and template_custom_gcode expand, and turns clumping detection on for a printer that
|
||||||
std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl)
|
// 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 center = printable_area_center(cfg);
|
||||||
const Vec2d cube_min = center - Vec2d(5., 5.) + place_wipe_tower(cfg, center);
|
std::vector<Vec2d> cube_mins;
|
||||||
TriangleMesh m = make_cube(10, 10, 10);
|
if (by_object) {
|
||||||
m.translate(static_cast<float>(cube_min.x()), static_cast<float>(cube_min.y()), 0.f);
|
// 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>(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<TimelapseType>(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;
|
Model model;
|
||||||
Print print;
|
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;
|
print.is_BBL_printer() = is_bbl;
|
||||||
obj->ensure_on_bed();
|
for (const Vec2d &cube_min : cube_mins) {
|
||||||
print.auto_assign_extruders(obj);
|
TriangleMesh m = make_cube(10, 10, 10);
|
||||||
|
m.translate(static_cast<float>(cube_min.x()), static_cast<float>(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.apply(model, cfg);
|
||||||
print.validate();
|
print.validate();
|
||||||
|
|
||||||
@@ -205,6 +232,7 @@ std::string slice_two_color_cube_and_export(DynamicPrintConfig cfg, bool is_bbl)
|
|||||||
in.close();
|
in.close();
|
||||||
boost::system::error_code ec;
|
boost::system::error_code ec;
|
||||||
fs::remove(tmp, ec);
|
fs::remove(tmp, ec);
|
||||||
|
print.output_filename(); // names the export as the app does, expanding filename_format
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,18 +292,119 @@ void install_slice_context_log_sink()
|
|||||||
logging::core::get()->add_sink(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<ConfigOptionFloats>("nozzle_diameter")->size();
|
||||||
|
bundle.set_num_filaments((unsigned int) std::max<size_t>(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<ConfigOptionInts>("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<FilamentMapMode>(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<ConfigOptionStrings>("filament_extruder_variant")) {
|
||||||
|
auto &self_index = cfg.option<ConfigOptionInts>("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 "<file_base>.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<std::pair<std::string, std::string>>;
|
||||||
|
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<const ConfigOptionString *>(opt)->value; !text.empty())
|
||||||
|
out.emplace(key, text);
|
||||||
|
} else if (opt->type() == coStrings) {
|
||||||
|
for (const std::string &text : static_cast<const ConfigOptionStrings *>(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
|
// 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
|
// -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
|
// 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
|
// undefined-placeholder / invalid-flow bugs surface here. PlaceholderParser::check_inactive_branches is
|
||||||
// if any failed, 0 otherwise. When outdir is non-empty, each printer's g-code is also written there as
|
// set for the sweep, so the names in {if} branches a slice does not take must resolve too.
|
||||||
// "<vendor>__<printer>.gcode" for manual inspection. The sweep is SEQUENTIAL by necessity:
|
//
|
||||||
// Print::process() keeps process-global state, so slicing printers concurrently in one process races
|
// Each printer is sliced with its own default process and filament. Every other compatible system
|
||||||
// even with per-slice Model+Print. Load in validation mode so the vendors are read straight from the -p
|
// process and filament whose templates (filename_format, filament_start_gcode, ...) no slice has expanded
|
||||||
// profiles dir (no data_dir/system tree) and -v scoping is honoured for free.
|
// 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 "<vendor>__<printer>[__<preset>].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)
|
int slice_all_printers(const std::string &vendor, const std::string &outdir)
|
||||||
{
|
{
|
||||||
install_slice_context_log_sink();
|
install_slice_context_log_sink();
|
||||||
|
PlaceholderParser::check_inactive_branches = true;
|
||||||
|
|
||||||
if (!outdir.empty()) {
|
if (!outdir.empty()) {
|
||||||
boost::system::error_code ec;
|
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)"
|
std::cout << "Slicing " << printers.size() << " printer preset(s)"
|
||||||
<< (vendor.empty() ? "" : " for vendor " + vendor) << "..." << std::endl;
|
<< (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) {
|
for (const auto &[vendor_name, printer] : printers) {
|
||||||
g_slice_context = vendor_name + " / " + printer; // tag every engine log line from this slice
|
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);
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grow to a 2nd filament so the cube can change colour; never shrink a multi-nozzle printer
|
const std::string print_name = bundle.prints.get_selected_preset_name();
|
||||||
// below its nozzle count, or full_config()'s flush-volume matrix no longer matches validate().
|
const std::string filament_name = bundle.filaments.get_selected_preset_name();
|
||||||
const size_t nozzles = bundle.printers.get_selected_preset().config.option<ConfigOptionFloats>("nozzle_diameter")->size();
|
const std::string what = "Printer \"" + printer + "\"";
|
||||||
bundle.set_num_filaments((unsigned int) std::max<size_t>(2, nozzles));
|
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())
|
||||||
// Mirror the app's manual filament->nozzle assignment for a multi-nozzle BBL printer: put each
|
++failures;
|
||||||
// filament on its own nozzle and pin the map (fmmManual) so full_config() collapses every filament to
|
else if (out.find("CP TOOLCHANGE START") == std::string::npos) {
|
||||||
// the variant of the nozzle it actually prints from, and the engine keeps that assignment instead of
|
// The filament change never rode the tower, so change_filament_gcode was not exercised.
|
||||||
// auto-remapping it during process(). Without this the synthetic 2nd filament keeps nozzle 1's variant
|
BOOST_LOG_TRIVIAL(error) << what << " sliced but the filament change never fired (no CP TOOLCHANGE START)";
|
||||||
// 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<ConfigOptionInts>("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<FilamentMapMode>(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<ConfigOptionStrings>("filament_extruder_variant")) {
|
|
||||||
auto &self_index = cfg.option<ConfigOptionInts>("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();
|
|
||||||
++failures;
|
++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<ConfigOptionStrings>("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<std::pair<const Preset *, std::string>> 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();
|
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) {
|
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;
|
std::cout << "Validation failed" << std::endl;
|
||||||
return 1;
|
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;
|
std::cout << "Validation completed successfully" << std::endl;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -435,8 +612,8 @@ int main(int argc, char* argv[])
|
|||||||
#endif
|
#endif
|
||||||
("vendor,v", po::value<std::string>()->default_value(""), "Vendor name. Optional, all profiles present in the folder will be validated if not specified")
|
("vendor,v", po::value<std::string>()->default_value(""), "Vendor name. Optional, all profiles present in the folder will be validated if not specified")
|
||||||
("generate_presets,g", po::value<bool>()->default_value(false), "Generate user presets for mock test")
|
("generate_presets,g", po::value<bool>()->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.")
|
("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<std::string>()->default_value(""), "With -s, also save each printer's g-code to this folder (as <vendor>__<printer>.gcode) for manual inspection. Optional.")
|
("outdir,o", po::value<std::string>()->default_value(""), "With -s, also save each slice's g-code to this folder (as <vendor>__<printer>[__<preset>].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.")
|
("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<int>()->default_value(2), "Log level. Optional, default is 2 (warning). Higher values produce more detailed logs.");
|
("log_level,l", po::value<int>()->default_value(2), "Log level. Optional, default is 2 (warning). Higher values produce more detailed logs.");
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|||||||
@@ -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.
|
// 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;
|
bool just_boolean_expression = false;
|
||||||
std::string error_message;
|
std::string error_message;
|
||||||
|
// Local variables declared in {if} branches that were not taken, see PlaceholderParser::check_inactive_branches.
|
||||||
|
mutable std::set<std::string> inactive_local_variables;
|
||||||
|
|
||||||
// Table to translate symbol tag to a human readable error message.
|
// Table to translate symbol tag to a human readable error message.
|
||||||
static std::map<std::string, std::string> tag_to_error_message;
|
static std::map<std::string, std::string> tag_to_error_message;
|
||||||
@@ -892,6 +894,8 @@ namespace client
|
|||||||
}
|
}
|
||||||
// Inside a block, which is conditionally suppressed?
|
// Inside a block, which is conditionally suppressed?
|
||||||
bool skipping() const { return m_depth_suppressed > 0; }
|
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
|
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)
|
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;
|
return;
|
||||||
|
|
||||||
std::string opt_key_str(opt_key.begin(), opt_key.end());
|
std::string opt_key_str(opt_key.begin(), opt_key.end());
|
||||||
@@ -949,7 +953,9 @@ namespace client
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (opt == nullptr)
|
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_scalar()) {
|
||||||
if (opt->is_nil())
|
if (opt->is_nil())
|
||||||
ctx->throw_exception("Trying to reference an undefined (nil) optional variable", opt_key);
|
ctx->throw_exception("Trying to reference an undefined (nil) optional variable", opt_key);
|
||||||
@@ -972,9 +978,10 @@ namespace client
|
|||||||
IteratorRange &opt_vector_index,
|
IteratorRange &opt_vector_index,
|
||||||
std::string &output)
|
std::string &output)
|
||||||
{
|
{
|
||||||
if (ctx->skipping())
|
if (ctx->skipping() && ! ctx->check_inactive_names())
|
||||||
return;
|
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());
|
std::string opt_key_str(opt_key.begin(), opt_key.end());
|
||||||
const ConfigOption *opt = ctx->resolve_symbol(opt_key_str);
|
const ConfigOption *opt = ctx->resolve_symbol(opt_key_str);
|
||||||
if (opt == nullptr) {
|
if (opt == nullptr) {
|
||||||
@@ -984,18 +991,20 @@ namespace client
|
|||||||
opt = ctx->resolve_symbol(opt_key_str);
|
opt = ctx->resolve_symbol(opt_key_str);
|
||||||
}
|
}
|
||||||
if (opt == nullptr)
|
if (opt == nullptr)
|
||||||
ctx->throw_exception("Variable does not exist", opt_key);
|
ctx->throw_exception(not_found, opt_key);
|
||||||
}
|
}
|
||||||
if (! opt->is_vector())
|
if (! opt->is_vector())
|
||||||
ctx->throw_exception("Trying to index a scalar variable", opt_key);
|
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<const ConfigOptionVectorBase*>(opt);
|
const ConfigOptionVectorBase *vec = static_cast<const ConfigOptionVectorBase*>(opt);
|
||||||
if (vec->empty())
|
if (vec->empty())
|
||||||
ctx->throw_exception("Indexing an empty vector variable", opt_key);
|
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();
|
int idx = opt_index->getInt();
|
||||||
if (idx < 0)
|
if (idx < 0)
|
||||||
ctx->throw_exception("Negative vector index", opt_key);
|
ctx->throw_exception("Negative vector index", opt_key);
|
||||||
@@ -1021,6 +1030,13 @@ namespace client
|
|||||||
output.writable = true;
|
output.writable = true;
|
||||||
}
|
}
|
||||||
output.opt = opt;
|
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;
|
output.it_range = opt_key;
|
||||||
}
|
}
|
||||||
@@ -1426,6 +1442,13 @@ namespace client
|
|||||||
out.opt = ctx->config_local.optptr(key);
|
out.opt = ctx->config_local.optptr(key);
|
||||||
}
|
}
|
||||||
out.name = std::move(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;
|
out.it_range = it_range;
|
||||||
}
|
}
|
||||||
@@ -1660,8 +1683,8 @@ namespace client
|
|||||||
const OptWithPos &rhs)
|
const OptWithPos &rhs)
|
||||||
{
|
{
|
||||||
if (ctx->skipping())
|
if (ctx->skipping())
|
||||||
// Skipping, continue parsing.
|
// Skipping, let conditional_expression parse the whole right hand side, which may continue after the variable reference.
|
||||||
return true;
|
return false;
|
||||||
|
|
||||||
if (lhs.opt) {
|
if (lhs.opt) {
|
||||||
assert(lhs.opt->is_vector());
|
assert(lhs.opt->is_vector());
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "libslic3r.h"
|
#include "libslic3r.h"
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <random>
|
#include <random>
|
||||||
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -24,8 +25,15 @@ public:
|
|||||||
// If defined, then this dictionary is used by the scripts to define user variables and persist them
|
// If defined, then this dictionary is used by the scripts to define user variables and persist them
|
||||||
// between PlaceholderParser evaluations.
|
// between PlaceholderParser evaluations.
|
||||||
std::unique_ptr<DynamicConfig> global_config;
|
std::unique_ptr<DynamicConfig> global_config;
|
||||||
|
// Global variables declared in {if} branches that were not taken, see check_inactive_branches.
|
||||||
|
std::set<std::string> 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);
|
PlaceholderParser(const DynamicConfig *external_config = nullptr);
|
||||||
|
|
||||||
void clear_config() { m_config.clear(); }
|
void clear_config() { m_config.clear(); }
|
||||||
|
|||||||
@@ -356,3 +356,38 @@ SCENARIO("Placeholder parser coFloatsOrPercents vector access", "[PlaceholderPar
|
|||||||
REQUIRE(std::stod(parser.process("{pressure_advance[2]}")) == Catch::Approx(3.0));
|
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<DynamicConfig>();
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user