diff --git a/src/libslic3r/PlaceholderParser.cpp b/src/libslic3r/PlaceholderParser.cpp index f03a5cee38..865b70434b 100644 --- a/src/libslic3r/PlaceholderParser.cpp +++ b/src/libslic3r/PlaceholderParser.cpp @@ -836,9 +836,11 @@ namespace client static std::map tag_to_error_message; size_t get_extruder_id() const { - const ConfigOptionInts * filament_map_opt = external_config->option("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("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(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(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(opt_parent), *ctx, static_cast(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(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(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(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(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(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(opt.opt); + const ConfigOptionFloatsNullable* opt_floatsnullable = dynamic_cast(opt.opt); if (opt_floatsnullable) { if (opt_floatsnullable->size() == 1) { // old version - output.set_d(static_cast(opt.opt)->get_at(0)); + output.set_d(opt_floatsnullable->get_at(0)); } else { - output.set_d(static_cast(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(opt.opt); if (opt_floats->size() == 1) { // old version - output.set_d(static_cast(opt.opt)->get_at(0)); + output.set_d(opt_floats->get_at(0)); } else { - output.set_d(static_cast(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(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(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(opt.opt)->values[idx])); break; case coBools: output.set_b(static_cast(opt.opt)->values[idx] != 0); break; case coEnums: output.set_i(static_cast(opt.opt)->values[idx]); break; + case coFloatsOrPercents: { + const ConfigOptionFloatsOrPercentsNullable *opt_vec_nullable = dynamic_cast(opt.opt); + if (opt_vec_nullable) { + resolve_float_or_percent(opt_vec_nullable->values[idx], idx); + } else { + resolve_float_or_percent(static_cast(opt.opt)->values[idx], idx); + } + break; + } default: ctx->throw_exception("Unsupported vector variable type", opt.it_range); } diff --git a/tests/libslic3r/test_placeholder_parser.cpp b/tests/libslic3r/test_placeholder_parser.cpp index 93c848b3a2..df00423386 100644 --- a/tests/libslic3r/test_placeholder_parser.cpp +++ b/tests/libslic3r/test_placeholder_parser.cpp @@ -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("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("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)); + } +}