From 036bd7bcec1e28c79dcba1ef6b40218657ea58b7 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 #13753 a bug made it fall back to the first object's name when a project was 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 | 68 ++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/PrintBase.cpp b/src/libslic3r/PrintBase.cpp index 821cf8a7f8..4448c12c76 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 c86cfa71dd..0791d4f7f1 100644 --- a/tests/fff_print/test_print.cpp +++ b/tests/fff_print/test_print.cpp @@ -183,6 +183,74 @@ void trigger_precise_wall_warning(DynamicPrintConfig& c) } // namespace +// --------------------------------------------------------------------------- +// {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"); +} + TEST_CASE("Print::validate stacks independent warnings", "[Print][validate]") { // Two unrelated checks (region precise-wall + machine acceleration) must each