From 7c80ea1fc18c1f002d247cbf924a6c11ae4809c9 Mon Sep 17 00:00:00 2001 From: raistlin7447 Date: Thu, 2 Jul 2026 11:14:32 -0500 Subject: [PATCH] feat: add {first_object_name} filename placeholder (#14497) {input_filename_base} is meant to be the saved project's file name. Before saved; #13753 fixed it to use the project name. Some users relied on the old behavior to get the part name into their output file name and had no placeholder to recover it ({model_name} is the 3mf designer metadata, blank for plain STL imports). Add {first_object_name} as a dedicated placeholder for the first printable object on the current plate, populated in update_object_placeholders() independently of {input_filename_base}. Closes #14493 --- src/libslic3r/PrintBase.cpp | 6 ++- tests/fff_print/test_print.cpp | 69 ++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/PrintBase.cpp b/src/libslic3r/PrintBase.cpp index f5ab988847..8b9e65b616 100644 --- a/src/libslic3r/PrintBase.cpp +++ b/src/libslic3r/PrintBase.cpp @@ -21,11 +21,12 @@ void PrintTryCancel::operator()() size_t PrintStateBase::g_last_timestamp = 0; -// Update "scale", "input_filename", "input_filename_base" placeholders from the current m_objects. +// Update "scale", "input_filename", "input_filename_base", "first_object_name" placeholders from the current m_objects. void PrintBase::update_object_placeholders(DynamicConfig &config, const std::string &default_ext) const { // get the first input file name std::string input_file; + std::string first_object_name; std::vector v_scale; int num_objects = 0; int num_instances = 0; @@ -38,6 +39,8 @@ void PrintBase::update_object_placeholders(DynamicConfig &config, const std::str } if (printable) { ++ num_objects; + if (num_objects == 1) + first_object_name = model_object->name; // CHECK_ME -> Is the following correct ? v_scale.push_back("x:" + boost::lexical_cast(printable->get_scaling_factor(X) * 100) + "% y:" + boost::lexical_cast(printable->get_scaling_factor(Y) * 100) + @@ -51,6 +54,7 @@ void PrintBase::update_object_placeholders(DynamicConfig &config, const std::str config.set_key_value("num_instances", new ConfigOptionInt(num_instances)); config.set_key_value("scale", new ConfigOptionStrings(v_scale)); + config.set_key_value("first_object_name", new ConfigOptionString(first_object_name)); if (! input_file.empty()) { // get basename with and without suffix const std::string input_filename = boost::filesystem::path(input_file).filename().string(); diff --git a/tests/fff_print/test_print.cpp b/tests/fff_print/test_print.cpp index 8c59b30292..3dad5628fa 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -103,3 +103,72 @@ SCENARIO("Print: Brim generation", "[Print]") { } } } + +// --------------------------------------------------------------------------- +// {first_object_name} filename placeholder +// --------------------------------------------------------------------------- +namespace { + +// Add a printable 20mm cube named `name` to `model`; returns it so the caller can tweak it. +ModelObject* add_named_cube(Model& model, const std::string& name) +{ + ModelObject* obj = model.add_object(); + obj->name = name; + obj->add_volume(make_cube(20.0, 20.0, 20.0)); + obj->add_instance(); + obj->ensure_on_bed(); + return obj; +} + +// Resolve `format` to an output file name for a print of `model`. `filename_base`, when set, +// is the saved-project name passed to output_filename(). +std::string resolved_output_name(Model& model, const std::string& format, const std::string& filename_base = {}) +{ + DynamicPrintConfig config = DynamicPrintConfig::full_print_config(); + config.set_key_value("filename_format", new ConfigOptionString(format)); + + Print print; + for (ModelObject* obj : model.objects) + print.auto_assign_extruders(obj); + print.apply(model, config); + return print.output_filename(filename_base); +} + +} // namespace + +TEST_CASE("Print: {first_object_name} names the first printable object on the plate", "[Print]") +{ + Model model; + + SECTION("uses the object's name") { + add_named_cube(model, "WidgetPart"); + CHECK(resolved_output_name(model, "{first_object_name}") == "WidgetPart.gcode"); + } + + SECTION("picks the first when several objects are printable") { + add_named_cube(model, "FirstPart"); + add_named_cube(model, "SecondPart"); + CHECK(resolved_output_name(model, "{first_object_name}") == "FirstPart.gcode"); + } + + SECTION("skips objects outside the print volume (e.g. on another plate)") { + // First in model order, but not on the current plate, so is_printable() is false. + add_named_cube(model, "OtherPlatePart")->instances.front()->print_volume_state = ModelInstancePVS_Fully_Outside; + add_named_cube(model, "OnPlatePart"); + CHECK(resolved_output_name(model, "{first_object_name}") == "OnPlatePart.gcode"); + } + + SECTION("is empty when the object has no name") { + add_named_cube(model, ""); + CHECK(resolved_output_name(model, "part_{first_object_name}") == "part_.gcode"); + } +} + +TEST_CASE("Print: {first_object_name} is not replaced by the saved-project file name", "[Print]") +{ + // Passing a saved-project file name as the filename_base must not change {first_object_name}. + Model model; + add_named_cube(model, "WidgetPart"); + CHECK(resolved_output_name(model, "{first_object_name}", "SavedProject") == "WidgetPart.gcode"); +} +