diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt index baf8ab21f3..21cb7ba35e 100644 --- a/src/libslic3r/CMakeLists.txt +++ b/src/libslic3r/CMakeLists.txt @@ -248,6 +248,8 @@ set(lisbslic3r_sources Arrange.cpp NormalUtils.cpp NormalUtils.hpp + ImportNamingRules.cpp + ImportNamingRules.hpp ObjColorUtils.cpp ObjColorUtils.hpp Orient.hpp diff --git a/src/libslic3r/Format/STEP.cpp b/src/libslic3r/Format/STEP.cpp index 4a8ed0fe53..e9a8fa5d28 100644 --- a/src/libslic3r/Format/STEP.cpp +++ b/src/libslic3r/Format/STEP.cpp @@ -32,7 +32,6 @@ #include "TDataStd_Name.hxx" #include "BRepBuilderAPI_Transform.hxx" #include "TopExp_Explorer.hxx" -#include "TopExp_Explorer.hxx" #include "BRep_Tool.hxx" #include "BRepTools.hxx" #include @@ -166,6 +165,17 @@ int StepPreProcessor::preNum(const unsigned char byte) { return num; } +// OCCT assigns these shape-type strings as default label names when no user +// name is available (e.g. for geometry that belongs to an assembly but has no +// separate PRODUCT entity). Treat them as "no name" and fall back to the +// parent component name so that tags stay on the component, not the body. +static bool isOcctShapeTypeName(const std::string& name) +{ + return name == "SOLID" || name == "COMPOUND" || name == "COMPSOLID" || + name == "SHELL" || name == "FACE" || name == "WIRE" || + name == "EDGE" || name == "VERTEX"; +} + static void getNamedSolids(const TopLoc_Location& location, const std::string& prefix, unsigned int& id, @@ -180,11 +190,35 @@ static void getNamedSolids(const TopLoc_Location& location, std::string name; Handle(TDataStd_Name) shapeName; if (referredLabel.FindAttribute(TDataStd_Name::GetID(), shapeName) || - label.FindAttribute(TDataStd_Name::GetID(), shapeName)) - name = TCollection_AsciiString(shapeName->Get()).ToCString(); + label.FindAttribute(TDataStd_Name::GetID(), shapeName)) { + // Manually flatten the OCCT extended (UTF-16) string to printable ASCII, + // replacing any non-printable / non-ASCII codepoint with a regular space. + // OCCT's TCollection_AsciiString conversion preserves raw high bytes which + // then fail StepPreProcessor::isUtf8 (a lone 0xA0/0xE9/etc. is invalid as + // UTF-8 start), causing the name to fall back to the parent component + // name and losing any [tag] the user typed. Normalize here so STEP escapes + // like \X\A0 (NBSP) or \X\E9 (é) become a benign space. + const TCollection_ExtendedString& ext = shapeName->Get(); + std::string ascii; + ascii.reserve(static_cast(ext.Length())); + for (Standard_Integer i = 1; i <= ext.Length(); ++i) { + Standard_ExtCharacter ch = ext.Value(i); + if (ch >= 0x20 && ch < 0x7F) + ascii.push_back(static_cast(ch)); + else + ascii.push_back(' '); + } + name = ascii; + } - if (name == "" || !StepPreProcessor::isUtf8(name)) - name = std::to_string(id++); + // Fall back to the parent component name when OCCT assigned a shape-type + // default (e.g. "SOLID") instead of a real user name. + if (name.empty() || !StepPreProcessor::isUtf8(name) || isOcctShapeTypeName(name)) { + if (!prefix.empty()) + name = prefix; + else + name = std::to_string(id++); + } std::string fullName{name}; TopLoc_Location localLocation = location * shapeTool->GetLocation(label); diff --git a/src/libslic3r/Format/STL.cpp b/src/libslic3r/Format/STL.cpp index 1888f5cd18..885dc61f9a 100644 --- a/src/libslic3r/Format/STL.cpp +++ b/src/libslic3r/Format/STL.cpp @@ -4,6 +4,8 @@ #include "STL.hpp" +#include +#include #include #ifdef _WIN32 @@ -32,6 +34,15 @@ bool load_stl(const char *path, Model *model, const char *object_name_in, Import if (object_name_in == nullptr) { const char *last_slash = strrchr(path, DIR_SEPARATOR); object_name.assign((last_slash == nullptr) ? path : last_slash + 1); + // Strip the .stl/.STL extension so naming-rule tags (e.g. "MyPart [f3] [neg].stl") + // display cleanly in the object/volume name and match STEP body-name behavior. + if (object_name.size() >= 4) { + std::string ext = object_name.substr(object_name.size() - 4); + std::transform(ext.begin(), ext.end(), ext.begin(), + [](unsigned char c) { return std::tolower(c); }); + if (ext == ".stl") + object_name.resize(object_name.size() - 4); + } } else object_name.assign(object_name_in); diff --git a/src/libslic3r/ImportNamingRules.cpp b/src/libslic3r/ImportNamingRules.cpp new file mode 100644 index 0000000000..4e4a2e0905 --- /dev/null +++ b/src/libslic3r/ImportNamingRules.cpp @@ -0,0 +1,107 @@ +#include "ImportNamingRules.hpp" +#include +#include + +namespace Slic3r { + +std::vector default_import_naming_rules() +{ + std::vector rules; + + // Part type rules + rules.push_back({"part", 0, ModelVolumeType::MODEL_PART}); + rules.push_back({"neg", 0, ModelVolumeType::NEGATIVE_VOLUME}); + rules.push_back({"mod", 0, ModelVolumeType::PARAMETER_MODIFIER}); + rules.push_back({"blk", 0, ModelVolumeType::SUPPORT_BLOCKER}); + rules.push_back({"enf", 0, ModelVolumeType::SUPPORT_ENFORCER}); + + // Filament rules f1–f9 + for (int i = 1; i <= 16; ++i) { + ImportNamingRule r; + r.tag = "f" + std::to_string(i); + r.filament = i; + r.volume_type = ModelVolumeType::INVALID; + rules.push_back(r); + } + + return rules; +} + +static std::string to_lower(const std::string& s) +{ + std::string out = s; + std::transform(out.begin(), out.end(), out.begin(), + [](unsigned char c) { return std::tolower(c); }); + return out; +} + +ImportNameTags parse_import_name_tags(const std::string& name, + const std::vector& rules) +{ + ImportNameTags result; + + // Extract all [...] tokens from the name (case-insensitive) + std::string lower_name = to_lower(name); + std::regex tag_re(R"(\[([a-z0-9]+)\])"); + auto begin = std::sregex_iterator(lower_name.begin(), lower_name.end(), tag_re); + auto end = std::sregex_iterator(); + + for (auto it = begin; it != end; ++it) { + std::string token = (*it)[1].str(); // content inside brackets, already lower + for (const auto& rule : rules) { + if (to_lower(rule.tag) == token) { + if (rule.filament > 0) + result.filament = rule.filament; + if (rule.volume_type != ModelVolumeType::INVALID) + result.volume_type = rule.volume_type; + break; + } + } + } + + return result; +} + +void apply_import_name_tags(ModelVolume& volume, const ImportNameTags& tags) +{ + // Always apply filament and type, defaulting to 1 / MODEL_PART when no + // recognized tag was found. This is the "user opted into naming rules" + // path — see apply_naming_rules_to_volume for the gating logic. + int filament = tags.filament > 0 ? tags.filament : 1; + volume.config.set_key_value("extruder", new ConfigOptionInt(filament)); + + ModelVolumeType type = (tags.volume_type != ModelVolumeType::INVALID) + ? tags.volume_type : ModelVolumeType::MODEL_PART; + volume.set_type(type); +} + +void apply_naming_rules_to_volume(ModelVolume& volume, + const std::vector& rules) +{ + // Only apply naming rules if the volume name contains at least one bracket + // tag, e.g. "Rectangle[f2]" or "text [nego]". This signals that the user + // is using the naming-rules system for this body, so removing a [mod] or + // [neg] tag should revert the type to PART and removing a [fN] tag should + // revert filament to 1. Names with no brackets at all are left alone so + // manual filament/type changes the user made in BambuStudio survive reload. + static const std::regex bracket_re(R"(\[[^\]]*\])"); + if (!std::regex_search(volume.name, bracket_re)) + return; + + ImportNameTags tags = parse_import_name_tags(volume.name, rules); + apply_import_name_tags(volume, tags); +} + +void apply_naming_rules_to_objects(const ModelObjectPtrs& objects, + const std::vector& rules) +{ + for (ModelObject* obj : objects) { + if (!obj) continue; + for (ModelVolume* vol : obj->volumes) { + if (!vol) continue; + apply_naming_rules_to_volume(*vol, rules); + } + } +} + +} // namespace Slic3r diff --git a/src/libslic3r/ImportNamingRules.hpp b/src/libslic3r/ImportNamingRules.hpp new file mode 100644 index 0000000000..d66ad3389f --- /dev/null +++ b/src/libslic3r/ImportNamingRules.hpp @@ -0,0 +1,43 @@ +#pragma once +#include +#include +#include +#include "Model.hpp" + +namespace Slic3r { + +// Result of parsing tags from a body/volume name. +struct ImportNameTags { + int filament = 0; // 0 = not specified + ModelVolumeType volume_type = ModelVolumeType::INVALID; // INVALID = not specified +}; + +// Default tag vocabulary (shipped defaults, user-configurable). +// Tags are matched case-insensitively inside square brackets. +struct ImportNamingRule { + std::string tag; // e.g. "neg", "f1" + int filament = 0; // >0 means this rule sets the filament + ModelVolumeType volume_type = ModelVolumeType::INVALID; +}; + +// Returns the default ruleset. +std::vector default_import_naming_rules(); + +// Parse all [tag] tokens from a name string and return combined result. +// Rules are checked in order; last filament/type tag wins. +ImportNameTags parse_import_name_tags(const std::string& name, + const std::vector& rules); + +// Apply parsed tags to a volume in-place. +// Only sets filament/type if the tag was actually found (non-zero / non-INVALID). +void apply_import_name_tags(ModelVolume& volume, const ImportNameTags& tags); + +// Convenience: parse and apply in one call. +void apply_naming_rules_to_volume(ModelVolume& volume, + const std::vector& rules); + +// Apply naming rules to all volumes of all objects in a list. +void apply_naming_rules_to_objects(const ModelObjectPtrs& objects, + const std::vector& rules); + +} // namespace Slic3r diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index c9f7ee461f..08d05fc349 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -59,6 +59,7 @@ #include "libslic3r/GCode/ThumbnailData.hpp" #include "Gizmos/GLGizmoAlignment.hpp" #include "libslic3r/Model.hpp" +#include "libslic3r/ImportNamingRules.hpp" #include "libslic3r/SLA/Hollowing.hpp" #include "libslic3r/SLA/SupportPoint.hpp" #include "libslic3r/SLA/ReprojectPointsOnMesh.hpp" @@ -8972,8 +8973,10 @@ std::vector Plater::priv::load_model_objects(const ModelObjectPtrs& mode #ifdef AUTOPLACEMENT_ON_LOAD ModelInstancePtrs new_instances; #endif /* AUTOPLACEMENT_ON_LOAD */ + const auto naming_rules = Slic3r::default_import_naming_rules(); for (ModelObject *model_object : model_objects) { auto *object = model.add_object(*model_object); + apply_naming_rules_to_objects({object}, naming_rules); object->sort_volumes(true); std::string object_name = object->name.empty() ? fs::path(object->input_file).filename().string() : object->name; obj_idxs.push_back(obj_count++); @@ -10577,11 +10580,34 @@ void Plater::priv::reload_from_disk() } + BOOST_LOG_TRIVIAL(info) << "[RELOAD_DEBUG] new_model has " << new_model.objects.size() << " object(s)"; + for (size_t oi = 0; oi < new_model.objects.size(); ++oi) { + ModelObject* mo = new_model.objects[oi]; + BOOST_LOG_TRIVIAL(info) << "[RELOAD_DEBUG] obj[" << oi << "] name='" << mo->name + << "' input_file='" << mo->input_file << "' volumes=" << mo->volumes.size(); + for (size_t vi = 0; vi < mo->volumes.size(); ++vi) { + ModelVolume* mv = mo->volumes[vi]; + Vec3d off = mv->get_transformation().get_offset(); + Vec3d mo_off = mv->source.mesh_offset; + BOOST_LOG_TRIVIAL(info) << "[RELOAD_DEBUG] vol[" << vi << "] name='" << mv->name + << "' pre_center_offset=(" << off.x() << "," << off.y() << "," << off.z() << ")" + << " mesh_offset=(" << mo_off.x() << "," << mo_off.y() << "," << mo_off.z() << ")"; + } + } for (ModelObject* model_object : new_model.objects) { model_object->center_around_origin(); model_object->ensure_on_bed(); } + BOOST_LOG_TRIVIAL(info) << "[RELOAD_DEBUG] After center_around_origin:"; + for (size_t oi = 0; oi < new_model.objects.size(); ++oi) { + ModelObject* mo = new_model.objects[oi]; + for (size_t vi = 0; vi < mo->volumes.size(); ++vi) { + Vec3d off = mo->volumes[vi]->get_transformation().get_offset(); + BOOST_LOG_TRIVIAL(info) << "[RELOAD_DEBUG] obj[" << oi << "] vol[" << vi << "] '" << mo->volumes[vi]->name + << "' post_center_offset=(" << off.x() << "," << off.y() << "," << off.z() << ")"; + } + } if (plate_data.size() > 0) { @@ -10599,6 +10625,19 @@ void Plater::priv::reload_from_disk() } #if ENABLE_RELOAD_FROM_DISK_REWORK + // Track which new_model volumes were matched so we can add new ones afterwards. + std::set> used_new_volumes; + int target_obj_idx = -1; + std::set refreshed_obj_idxs; + // old vol_idxs that had no match (body deleted from STEP) — removed after the loop + // so that sort_volumes inside the loop doesn't corrupt subsequent vol_idx values. + std::map> deleted_vol_idxs; // obj_idx → sorted list of vol_idxs + // Coordinate shift between new_model (post center_around_origin) and the existing model. + // Captured from the first matched volume: old_offset - new_model_offset. + // Applied to new bodies so they land in the same coordinate frame as existing volumes. + Vec3d coord_shift = Vec3d::Zero(); + bool coord_shift_set = false; + for (auto [obj_idx, vol_idx] : selected_volumes) { ModelObject *old_model_object = model.objects[obj_idx]; ModelVolume *old_volume = old_model_object->volumes[vol_idx]; @@ -10609,14 +10648,21 @@ void Plater::priv::reload_from_disk() boost::algorithm::iequals(fs::path(old_volume->source.input_file).filename().string(), fs::path(path).filename().string()); bool has_name = !old_volume->name.empty() && boost::algorithm::iequals(old_volume->name, fs::path(path).filename().string()); if (has_source || has_name) { + if (target_obj_idx < 0) target_obj_idx = obj_idx; + int new_volume_idx = -1; int new_object_idx = -1; bool match_found = false; - // take idxs from the matching volume + + // Pass 1: source-index + name — handles the common case where nothing changed. if (has_source && old_volume->source.object_idx < int(new_model.objects.size())) { const ModelObject *obj = new_model.objects[old_volume->source.object_idx]; if (old_volume->source.volume_idx < int(obj->volumes.size())) { - if (obj->volumes[old_volume->source.volume_idx]->source.input_file == old_volume->source.input_file) { + const ModelVolume *candidate = obj->volumes[old_volume->source.volume_idx]; + if (candidate->source.input_file == old_volume->source.input_file && + candidate->name == old_volume->name && + !used_new_volumes.count({(int)old_volume->source.object_idx, + (int)old_volume->source.volume_idx})) { new_volume_idx = old_volume->source.volume_idx; new_object_idx = old_volume->source.object_idx; match_found = true; @@ -10624,35 +10670,54 @@ void Plater::priv::reload_from_disk() } } - if (!match_found && has_name) { - // take idxs from the 1st matching volume - for (size_t o = 0; o < new_model.objects.size(); ++o) { - ModelObject *obj = new_model.objects[o]; - bool found = false; + // Pass 2: name search — handles bodies that moved position but kept their name. + // Run whenever the volume has a name; the outer has_source/has_name gate + // already ensured this volume belongs to the file being reloaded. + if (!match_found && !old_volume->name.empty()) { + for (size_t o = 0; o < new_model.objects.size() && !match_found; ++o) { + ModelObject *obj = new_model.objects[o]; for (size_t v = 0; v < obj->volumes.size(); ++v) { - if (obj->volumes[v]->name == old_volume->name) { + if (obj->volumes[v]->name == old_volume->name && + !used_new_volumes.count({(int)o, (int)v})) { new_volume_idx = (int) v; new_object_idx = (int) o; - found = true; + match_found = true; break; } } - if (found) break; - // BBS: step model,object loaded as a volume. GUI_ObfectList.cpp load_modifier() - if (obj->name == old_volume->name) { - new_object_idx = (int) o; - break; + if (!match_found) { + // BBS: step model,object loaded as a volume. GUI_ObfectList.cpp load_modifier() + if (obj->name == old_volume->name) { + new_object_idx = (int) o; + break; + } } } } - if (new_object_idx < 0 || int(new_model.objects.size()) <= new_object_idx) { - fail_list.push_back(from_u8(has_source ? old_volume->source.input_file : old_volume->name)); + // Pass 3: source-index only — handles renamed bodies (same position, new name). + // Skip if the slot is already claimed by a previous iteration. + if (!match_found && has_source && + old_volume->source.object_idx < int(new_model.objects.size())) { + const ModelObject *obj = new_model.objects[old_volume->source.object_idx]; + if (old_volume->source.volume_idx < int(obj->volumes.size()) && + !used_new_volumes.count({(int)old_volume->source.object_idx, + (int)old_volume->source.volume_idx})) { + new_volume_idx = old_volume->source.volume_idx; + new_object_idx = old_volume->source.object_idx; + match_found = true; + } + } + + // No match means this body was deleted from the STEP file. + // Mark for silent removal — do NOT add to fail_list. + if (!match_found || new_object_idx < 0 || int(new_model.objects.size()) <= new_object_idx) { + deleted_vol_idxs[obj_idx].push_back(vol_idx); continue; } ModelObject *new_model_object = new_model.objects[new_object_idx]; if (int(new_model_object->volumes.size()) <= new_volume_idx) { - fail_list.push_back(from_u8(has_source ? old_volume->source.input_file : old_volume->name)); + deleted_vol_idxs[obj_idx].push_back(vol_idx); continue; } @@ -10663,9 +10728,25 @@ void Plater::priv::reload_from_disk() new_volume = old_model_object->add_volume(std::move(mesh)); new_volume->name = new_model_object->name; new_volume->source.input_file = new_model_object->input_file; - }else { - new_volume = old_model_object->add_volume(*new_model_object->volumes[new_volume_idx]); - // new_volume = old_model_object->volumes.back(); + } else { + const ModelVolume *new_model_vol = new_model_object->volumes[new_volume_idx]; + // Capture coordinate shift from the first matched volume so that new + // bodies added to the STEP can be placed in the existing coordinate frame. + // shift = old_vol_offset - new_model_vol_offset (both measured from their + // respective center_around_origin calls, so the difference is the drift in + // bounding-box center between the initial import and this reload). + if (!coord_shift_set) { + Vec3d old_off = old_volume->get_transformation().get_offset(); + Vec3d new_off = new_model_vol->get_transformation().get_offset(); + coord_shift = old_off - new_off; + coord_shift_set = true; + BOOST_LOG_TRIVIAL(info) << "[RELOAD_DEBUG] coord_shift captured from volume '" << old_volume->name << "'" + << " old_offset=(" << old_off.x() << "," << old_off.y() << "," << old_off.z() << ")" + << " new_model_offset=(" << new_off.x() << "," << new_off.y() << "," << new_off.z() << ")" + << " coord_shift=(" << coord_shift.x() << "," << coord_shift.y() << "," << coord_shift.z() << ")"; + } + new_volume = old_model_object->add_volume(*new_model_vol); + used_new_volumes.insert({new_object_idx, new_volume_idx}); } new_volume->set_new_unique_id(); @@ -10673,11 +10754,25 @@ void Plater::priv::reload_from_disk() new_volume->set_type(old_volume->type()); new_volume->set_material_id(old_volume->material_id()); + // Re-apply naming rules so tag changes in the reloaded file take effect. + apply_naming_rules_to_volume(*new_volume, default_import_naming_rules()); + new_volume->source.mesh_offset = old_volume->source.mesh_offset; - new_volume->set_transformation(old_volume->get_transformation()); + // Use the new STEP position translated into the existing scene's coordinate frame + // (via coord_shift). For the first matched volume this yields exactly old_volume's + // offset; for subsequent volumes it tracks Fusion's repositioning while preserving + // the existing object's plate anchor. + if (coord_shift_set) + new_volume->set_offset(new_volume->get_transformation().get_offset() + coord_shift); + { + Vec3d off = new_volume->get_transformation().get_offset(); + BOOST_LOG_TRIVIAL(info) << "[RELOAD_DEBUG] matched volume '" << new_volume->name << "'" + << " final_offset=(" << off.x() << "," << off.y() << "," << off.z() << ")"; + } - new_volume->source.object_idx = old_volume->source.object_idx; - new_volume->source.volume_idx = old_volume->source.volume_idx; + // Update source indices to reflect the new model layout so the next reload is correct. + new_volume->source.object_idx = new_object_idx >= 0 ? new_object_idx : old_volume->source.object_idx; + new_volume->source.volume_idx = new_volume_idx >= 0 ? new_volume_idx : old_volume->source.volume_idx; assert(!old_volume->source.is_converted_from_inches || !old_volume->source.is_converted_from_meters); if (old_volume->source.is_converted_from_inches) new_volume->convert_from_imperial_units(); @@ -10685,15 +10780,70 @@ void Plater::priv::reload_from_disk() new_volume->convert_from_meters(); std::swap(old_model_object->volumes[vol_idx], old_model_object->volumes.back()); old_model_object->delete_volume(old_model_object->volumes.size() - 1); - if (!sinking) old_model_object->ensure_on_bed(); - old_model_object->sort_volumes(wxGetApp().app_config->get("order_volumes") == "1"); + // Do NOT call sort_volumes here — it would corrupt subsequent vol_idx values + // in this loop. A single sort_volumes call happens after the loop. sla::reproject_points_and_holes(old_model_object); - // Fix warning icon in object list - wxGetApp().obj_list()->update_item_error_icon(obj_idx, vol_idx); + refreshed_obj_idxs.insert(obj_idx); + } + } + + // Remove volumes whose bodies were deleted from the STEP file. + // Delete in reverse index order so earlier indices stay valid. + for (auto& [obj_idx, vol_idxs] : deleted_vol_idxs) { + ModelObject *obj = model.objects[obj_idx]; + std::sort(vol_idxs.begin(), vol_idxs.end(), std::greater()); + for (int vi : vol_idxs) + obj->delete_volume(vi); + refreshed_obj_idxs.insert(obj_idx); + } + + // Sort and reposition all modified objects now that the loop is done. + // (sort_volumes was intentionally omitted from the loop to keep vol_idx values stable.) + const bool full_sort = wxGetApp().app_config->get("order_volumes") == "1"; + for (int oidx : refreshed_obj_idxs) { + ModelObject *obj = model.objects[oidx]; + obj->sort_volumes(full_sort); + obj->ensure_on_bed(); + } + + // Add volumes that are new in the reloaded file (no matching old volume). + if (target_obj_idx >= 0) { + ModelObject *target_object = model.objects[target_obj_idx]; + const auto naming_rules = default_import_naming_rules(); + bool added_any = false; + for (int o = 0; o < (int)new_model.objects.size(); ++o) { + for (int v = 0; v < (int)new_model.objects[o]->volumes.size(); ++v) { + if (used_new_volumes.count({o, v})) continue; + ModelVolume *added = target_object->add_volume(*new_model.objects[o]->volumes[v]); + added->set_new_unique_id(); + added->source.object_idx = o; + added->source.volume_idx = v; + // Shift into the existing model's coordinate frame. + Vec3d pre_shift_off = added->get_transformation().get_offset(); + if (coord_shift_set) + added->set_offset(pre_shift_off + coord_shift); + Vec3d post_shift_off = added->get_transformation().get_offset(); + BOOST_LOG_TRIVIAL(info) << "[RELOAD_DEBUG] NEW body '" << added->name << "'" + << " pre_shift=(" << pre_shift_off.x() << "," << pre_shift_off.y() << "," << pre_shift_off.z() << ")" + << " post_shift=(" << post_shift_off.x() << "," << post_shift_off.y() << "," << post_shift_off.z() << ")" + << " coord_shift_set=" << coord_shift_set; + apply_naming_rules_to_volume(*added, naming_rules); + added_any = true; + } + } + if (added_any) { + target_object->sort_volumes(full_sort); + target_object->ensure_on_bed(); + refreshed_obj_idxs.insert(target_obj_idx); } } + + // Refresh object list entries for all modified objects so names, types, + // and filament columns reflect the reloaded volumes. + for (int oidx : refreshed_obj_idxs) + wxGetApp().obj_list()->add_volumes_to_object_in_list(oidx); #else // update the selected volumes whose source is the current file for (const SelectedVolume& sel_v : selected_volumes) { @@ -10746,6 +10896,10 @@ void Plater::priv::reload_from_disk() new_volume->config.apply(old_volume->config); new_volume->set_type(old_volume->type()); new_volume->set_material_id(old_volume->material_id()); + + // Re-apply naming rules so tag changes in the reloaded file take effect. + apply_naming_rules_to_volume(*new_volume, default_import_naming_rules()); + new_volume->set_transformation(old_volume->get_transformation()); new_volume->translate(new_volume->get_transformation().get_matrix(true) * (new_volume->source.mesh_offset - old_volume->source.mesh_offset)); new_volume->source.object_idx = old_volume->source.object_idx;