Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a4848a5
feat: smart import naming rules via body name tags
jomixlaf Apr 25, 2026
5b8821a
Fix STEP import: resolve MANIFOLD_SOLID_BREP body names via entity tr…
jomixlaf Apr 25, 2026
e6ff1eb
Fix STEP import: use component name for bodies in root assembly compo…
jomixlaf Apr 25, 2026
74e3395
Fix: re-apply naming rules on reload from disk
jomixlaf Apr 25, 2026
530fd6c
Fix: reload from disk now picks up new components and name changes
jomixlaf Apr 25, 2026
61b9490
Fix: prevent source-index mismatch when STEP file gains or reorders b…
jomixlaf Apr 25, 2026
fd8397f
Fix: refresh object list sidebar after reload from disk
jomixlaf Apr 25, 2026
38da7b4
Fix: three-pass matching for reload from disk handles renames and moves
jomixlaf Apr 25, 2026
3e36030
Fix: handle deleted bodies on reload; move sort_volumes out of loop
jomixlaf Apr 25, 2026
e046448
Fix: correct position of new STEP bodies on reload
jomixlaf Apr 25, 2026
faf8083
Add RELOAD_DEBUG logging for reload-from-disk diagnostics
jomixlaf Apr 25, 2026
86fc081
Fix: ungate Pass 2 name search in reload-from-disk matching
jomixlaf Apr 25, 2026
478aa03
Fix: reload-from-disk follows Fusion repositioning of matched bodies
jomixlaf Apr 25, 2026
d798549
Fix: STEP loader tolerates non-ASCII bytes in body names
jomixlaf Apr 25, 2026
e3d85da
Fix: STEP loader normalizes non-ASCII codepoints in body names
jomixlaf Apr 25, 2026
b8aacf0
Fix: removing a tag now reverts the property to its default
jomixlaf Apr 25, 2026
42d743e
Merge remote-tracking branch 'upstream/master' into feature/smart-imp…
jomixlaf Apr 26, 2026
84796c9
feat: apply naming rules to STL imports
jomixlaf Apr 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/libslic3r/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,8 @@ set(lisbslic3r_sources
Arrange.cpp
NormalUtils.cpp
NormalUtils.hpp
ImportNamingRules.cpp
ImportNamingRules.hpp
ObjColorUtils.cpp
ObjColorUtils.hpp
Orient.hpp
Expand Down
44 changes: 39 additions & 5 deletions src/libslic3r/Format/STEP.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <IMeshTools_Parameters.hxx>
Expand Down Expand Up @@ -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,
Expand All @@ -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<size_t>(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<char>(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);
Expand Down
11 changes: 11 additions & 0 deletions src/libslic3r/Format/STL.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

#include "STL.hpp"

#include <algorithm>
#include <cctype>
#include <string>

#ifdef _WIN32
Expand Down Expand Up @@ -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);

Expand Down
107 changes: 107 additions & 0 deletions src/libslic3r/ImportNamingRules.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#include "ImportNamingRules.hpp"
#include <algorithm>
#include <cctype>

namespace Slic3r {

std::vector<ImportNamingRule> default_import_naming_rules()
{
std::vector<ImportNamingRule> 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<ImportNamingRule>& 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<ImportNamingRule>& 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<ImportNamingRule>& 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
43 changes: 43 additions & 0 deletions src/libslic3r/ImportNamingRules.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#pragma once
#include <string>
#include <vector>
#include <regex>
#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<ImportNamingRule> 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<ImportNamingRule>& 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<ImportNamingRule>& 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<ImportNamingRule>& rules);

} // namespace Slic3r
Loading
Loading