Support accessing coFloatsOrPercents values in gcode template (#14526)

* Support accessing `coFloatsOrPercents` values in gcode template (OrcaSlicer/OrcaSlicer#14522)

* Vector option values are separated by comma

* Fix wrong cast used for checking nullability
This commit is contained in:
Noisyfox
2026-07-02 23:51:20 +08:00
committed by SoftFever
parent a38d9ccb21
commit fa0a08d8f3
2 changed files with 199 additions and 12 deletions

View File

@@ -836,9 +836,11 @@ namespace client
static std::map<std::string, std::string> tag_to_error_message;
size_t get_extruder_id() const {
const ConfigOptionInts * filament_map_opt = external_config->option<ConfigOptionInts>("filament_map");
if (filament_map_opt && current_extruder_id < filament_map_opt->values.size()) {
return filament_map_opt->values[current_extruder_id];
if (external_config != nullptr) {
const ConfigOptionInts * filament_map_opt = external_config->option<ConfigOptionInts>("filament_map");
if (filament_map_opt && current_extruder_id < filament_map_opt->values.size()) {
return filament_map_opt->values[current_extruder_id];
}
}
return 0;
}
@@ -1096,27 +1098,109 @@ namespace client
const ConfigOptionVectorBase* vec = static_cast<const ConfigOptionVectorBase*>(opt.opt);
if (vec->empty())
ctx->throw_exception("Indexing an empty vector variable", opt.it_range);
// Helper to resolve a FloatOrPercent value (handles ratio_over chain for percent values).
// elem_index: the element index used to access this vector element, so that
// parent vectors (via ratio_over) use the same index rather than the current extruder.
auto resolve_float_or_percent = [ctx, &opt, &output](const FloatOrPercent &fop, size_t elem_index) {
std::string opt_key(opt.it_range.begin(), opt.it_range.end());
if (boost::ends_with(opt_key, "line_width")) {
// Line width supports defaults and a complex graph of dependencies.
output.set_d(Flow::extrusion_width(opt_key, *ctx, static_cast<unsigned int>(ctx->current_extruder_id)));
} else if (! fop.percent) {
// Not a percent, just return the value.
output.set_d(fop.value);
} else {
// Resolve dependencies using the "ratio_over" link to a parent value.
const ConfigOptionDef *opt_def = print_config_def.get(opt_key);
assert(opt_def != nullptr);
double v = fop.value * 0.01; // percent to ratio
for (;;) {
const ConfigOption *opt_parent = opt_def->ratio_over.empty() ? nullptr : ctx->resolve_symbol(opt_def->ratio_over);
if (opt_parent == nullptr)
ctx->throw_exception("FloatOrPercent variable failed to resolve the \"ratio_over\" dependencies", opt.it_range);
if (boost::ends_with(opt_def->ratio_over, "line_width")) {
// Line width supports defaults and a complex graph of dependencies.
assert(opt_parent->type() == coFloatOrPercent);
v *= Flow::extrusion_width(opt_def->ratio_over, static_cast<const ConfigOptionFloatOrPercent*>(opt_parent), *ctx, static_cast<unsigned int>(ctx->current_extruder_id));
break;
}
if (opt_parent->type() == coFloat || opt_parent->type() == coFloatOrPercent) {
v *= opt_parent->getFloat();
if (opt_parent->type() == coFloat || ! static_cast<const ConfigOptionFloatOrPercent*>(opt_parent)->percent)
break;
v *= 0.01; // percent to ratio
} else if (opt_parent->type() == coFloats) {
// Vector parent: extract the value for the current extruder.
const ConfigOptionFloatsNullable *parent_nullable = dynamic_cast<const ConfigOptionFloatsNullable *>(opt_parent);
if (parent_nullable) {
v *= (parent_nullable->size() == 1) ? parent_nullable->get_at(0) : parent_nullable->get_at(elem_index);
} else {
const ConfigOptionFloats *parent_vec = static_cast<const ConfigOptionFloats *>(opt_parent);
v *= (parent_vec->size() == 1) ? parent_vec->get_at(0) : parent_vec->get_at(elem_index);
}
break;
} else if (opt_parent->type() == coFloatsOrPercents) {
// Vector parent with percent support: extract the FloatOrPercent for the current extruder.
const ConfigOptionFloatsOrPercentsNullable *parent_nullable = dynamic_cast<const ConfigOptionFloatsOrPercentsNullable *>(opt_parent);
if (parent_nullable) {
const FloatOrPercent &parent_fop = (parent_nullable->size() == 1) ? parent_nullable->get_at(0) : parent_nullable->get_at(elem_index);
if (! parent_fop.percent) {
v *= parent_fop.value;
break;
}
v *= parent_fop.value * 0.01; // percent to ratio
} else {
const ConfigOptionFloatsOrPercents *parent_vec = static_cast<const ConfigOptionFloatsOrPercents *>(opt_parent);
const FloatOrPercent &parent_fop = (parent_vec->size() == 1) ? parent_vec->get_at(0) : parent_vec->get_at(elem_index);
if (! parent_fop.percent) {
v *= parent_fop.value;
break;
}
v *= parent_fop.value * 0.01; // percent to ratio
}
}
// Continue one level up in the "ratio_over" hierarchy.
opt_def = print_config_def.get(opt_def->ratio_over);
assert(opt_def != nullptr);
}
output.set_d(v);
}
};
if (!opt.has_index()) {
// Allow omitting extruder id when referencing vectors
switch (opt.opt->type()) {
case coFloats: {
const ConfigOptionFloatsNullable* opt_floatsnullable = static_cast<const ConfigOptionFloatsNullable *>(opt.opt);
const ConfigOptionFloatsNullable* opt_floatsnullable = dynamic_cast<const ConfigOptionFloatsNullable *>(opt.opt);
if (opt_floatsnullable) {
if (opt_floatsnullable->size() == 1) { // old version
output.set_d(static_cast<const ConfigOptionFloatsNullable*>(opt.opt)->get_at(0));
output.set_d(opt_floatsnullable->get_at(0));
} else {
output.set_d(static_cast<const ConfigOptionFloatsNullable*>(opt.opt)->get_at(ctx->get_extruder_id()));
output.set_d(opt_floatsnullable->get_at(ctx->get_extruder_id()));
}
} else {
const ConfigOptionFloats* opt_floats = static_cast<const ConfigOptionFloats*>(opt.opt);
if (opt_floats->size() == 1) { // old version
output.set_d(static_cast<const ConfigOptionFloats*>(opt.opt)->get_at(0));
output.set_d(opt_floats->get_at(0));
} else {
output.set_d(static_cast<const ConfigOptionFloats*>(opt.opt)->get_at(ctx->get_extruder_id()));
output.set_d(opt_floats->get_at(ctx->get_extruder_id()));
}
}
break;
}
case coFloatsOrPercents: {
const ConfigOptionFloatsOrPercentsNullable *opt_vec_nullable = dynamic_cast<const ConfigOptionFloatsOrPercentsNullable *>(opt.opt);
if (opt_vec_nullable) {
size_t elem_index = (opt_vec_nullable->size() == 1) ? 0 : ctx->get_extruder_id();
resolve_float_or_percent(opt_vec_nullable->get_at(elem_index), elem_index);
} else {
const ConfigOptionFloatsOrPercents *opt_vec = static_cast<const ConfigOptionFloatsOrPercents *>(opt.opt);
size_t elem_index = (opt_vec->size() == 1) ? 0 : ctx->get_extruder_id();
resolve_float_or_percent(opt_vec->get_at(elem_index), elem_index);
}
break;
}
default: ctx->throw_exception("Referencing a vector variable when scalar is expected", opt.it_range);
}
} else {
@@ -1131,6 +1215,15 @@ namespace client
case coPoints: output.set_s(to_string(static_cast<const ConfigOptionPoints*>(opt.opt)->values[idx])); break;
case coBools: output.set_b(static_cast<const ConfigOptionBools*>(opt.opt)->values[idx] != 0); break;
case coEnums: output.set_i(static_cast<const ConfigOptionInts *>(opt.opt)->values[idx]); break;
case coFloatsOrPercents: {
const ConfigOptionFloatsOrPercentsNullable *opt_vec_nullable = dynamic_cast<const ConfigOptionFloatsOrPercentsNullable *>(opt.opt);
if (opt_vec_nullable) {
resolve_float_or_percent(opt_vec_nullable->values[idx], idx);
} else {
resolve_float_or_percent(static_cast<const ConfigOptionFloatsOrPercents *>(opt.opt)->values[idx], idx);
}
break;
}
default:
ctx->throw_exception("Unsupported vector variable type", opt.it_range);
}

View File

@@ -11,8 +11,8 @@ SCENARIO("Placeholder parser scripting", "[PlaceholderParser]") {
config.set_deserialize_strict( {
{ "printer_notes", " PRINTER_VENDOR_PRUSA3D PRINTER_MODEL_MK2 " },
{ "nozzle_diameter", "0.6;0.6;0.6;0.6" },
{ "nozzle_temperature", "357;359;363;378" }
{ "nozzle_diameter", "0.6,0.6,0.6,0.6" },
{ "nozzle_temperature", "357,359,363,378" }
});
// To test the "min_width_top_surface" over "inner_wall_line_width".
config.option<ConfigOptionFloatOrPercent>("inner_wall_line_width")->value = 150.;
@@ -120,8 +120,8 @@ SCENARIO("Placeholder parser variables", "[PlaceholderParser]") {
config.set_deserialize_strict({
{ "filament_notes", "testnotes" },
{ "enable_pressure_advance", "1" },
{ "nozzle_diameter", "0.6;0.6;0.6;0.6" },
{ "nozzle_temperature", "357;359;363;378" }
{ "nozzle_diameter", "0.6,0.6,0.6,0.6" },
{ "nozzle_temperature", "357,359,363,378" }
});
PlaceholderParser::ContextData context_with_global_dict;
@@ -240,3 +240,97 @@ SCENARIO("Placeholder parser variables", "[PlaceholderParser]") {
}
SECTION("if else completely empty") { REQUIRE(parser.process("{if false then elsif false then else endif}", 0, nullptr, nullptr, nullptr) == ""); }
}
SCENARIO("Placeholder parser coFloatsOrPercents vector access", "[PlaceholderParser]") {
PlaceholderParser parser;
auto config = DynamicPrintConfig::full_print_config();
// outer_wall_speed is the ratio_over target for small_perimeter_speed.
// Different values per extruder to verify parent resolves at the same element index.
config.set_deserialize_strict({
{ "outer_wall_speed", "60,70,80,90" },
{ "nozzle_diameter", "0.4,0.4,0.4,0.4" },
{ "pressure_advance", "1.5,2.0,3.0,4.0" } // coFloats non-nullable
});
// small_perimeter_speed:
// [0] = 50% of outer_wall_speed[0] (= 60) → 30
// [1] = 80% of outer_wall_speed[1] (= 70) → 56
// [2] = 0 absolute
// [3] = 50% of outer_wall_speed[3] (= 90) → 45
config.option<ConfigOptionFloatsOrPercentsNullable>("small_perimeter_speed")->values = {
FloatOrPercent{50.0, true}, // 50% of outer_wall_speed[0] (60) = 30
FloatOrPercent{80.0, true}, // 80% of outer_wall_speed[1] (70) = 56
FloatOrPercent{0.0, false}, // absolute: 0
FloatOrPercent{50.0, true}, // 50% of outer_wall_speed[3] (90) = 45
};
parser.apply_config(config);
parser.set("foo", 0);
parser.set("bar", 1);
parser.set("baz", 3);
parser.set("num_extruders", 4);
SECTION("Indexed access - percent resolved against parent at same index [0]") {
// 50% of outer_wall_speed[0] (60) = 30
REQUIRE(std::stod(parser.process("{small_perimeter_speed[0]}")) == Catch::Approx(30.0));
}
SECTION("Indexed access - percent resolved against parent at same index [1]") {
// 80% of outer_wall_speed[1] (70) = 56
REQUIRE(std::stod(parser.process("{small_perimeter_speed[1]}")) == Catch::Approx(56.0));
}
SECTION("Indexed access - percent resolved against parent at same index [3]") {
// 50% of outer_wall_speed[3] (90) = 45
REQUIRE(std::stod(parser.process("{small_perimeter_speed[3]}")) == Catch::Approx(45.0));
}
SECTION("Variable-indexed access via foo (=0) - percent value") {
// 50% of outer_wall_speed[0] (60) = 30
REQUIRE(std::stod(parser.process("{small_perimeter_speed[foo]}")) == Catch::Approx(30.0));
}
SECTION("Variable-indexed access via bar (=1) - percent value") {
// 80% of outer_wall_speed[1] (70) = 56
REQUIRE(std::stod(parser.process("{small_perimeter_speed[bar]}")) == Catch::Approx(56.0));
}
SECTION("Variable-indexed access via baz (=3) - percent value") {
// 50% of outer_wall_speed[3] (90) = 45
REQUIRE(std::stod(parser.process("{small_perimeter_speed[baz]}")) == Catch::Approx(45.0));
}
SECTION("Literal-indexed access - absolute value") {
REQUIRE(std::stod(parser.process("{small_perimeter_speed[2]}")) == Catch::Approx(0.0));
}
SECTION("No-index (extruder-based) access - percent resolved via current extruder") {
// Extruder 0 = 50% of outer_wall_speed[0] (60) = 30
REQUIRE(std::stod(parser.process("{small_perimeter_speed}")) == Catch::Approx(30.0));
}
SECTION("Out-of-range index clamps to index 0") {
// Index 99 is out of range, clamps to 0: 50% of outer_wall_speed[0] (60) = 30
REQUIRE(std::stod(parser.process("{small_perimeter_speed[99]}")) == Catch::Approx(30.0));
}
SECTION("coFloats no-index access - nullable (outer_wall_speed)") {
// outer_wall_speed is ConfigOptionFloatsNullable, exercises the 'if' branch
REQUIRE(std::stod(parser.process("{outer_wall_speed}")) == Catch::Approx(60.0));
}
SECTION("coFloats indexed access - nullable") {
// outer_wall_speed[2] = 80
REQUIRE(std::stod(parser.process("{outer_wall_speed[2]}")) == Catch::Approx(80.0));
}
SECTION("coFloats no-index access - non-nullable (pressure_advance)") {
// pressure_advance is ConfigOptionFloats (non-nullable), exercises the 'else' branch
REQUIRE(std::stod(parser.process("{pressure_advance}")) == Catch::Approx(1.5));
}
SECTION("coFloats indexed access - non-nullable") {
// pressure_advance[2] = 3.0
REQUIRE(std::stod(parser.process("{pressure_advance[2]}")) == Catch::Approx(3.0));
}
}