Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
161 changes: 133 additions & 28 deletions src/slic3r/GUI/Plater.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8637,6 +8637,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<std::pair<int,int>> used_new_volumes;
int target_obj_idx = -1;
std::set<int> 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<int, std::vector<int>> 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];
Expand All @@ -8647,50 +8660,76 @@ 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;
}
}
}

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;
}

Expand All @@ -8701,9 +8740,20 @@ 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) {
coord_shift = old_volume->get_transformation().get_offset()
- new_model_vol->get_transformation().get_offset();
coord_shift_set = true;
}
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();
Expand All @@ -8712,26 +8762,81 @@ void Plater::priv::reload_from_disk()
new_volume->set_material_id(old_volume->material_id());

new_volume->source.mesh_offset = old_volume->source.mesh_offset;
new_volume->set_transformation(old_volume->get_transformation());

new_volume->source.object_idx = old_volume->source.object_idx;
new_volume->source.volume_idx = old_volume->source.volume_idx;
// 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);

// 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();
else if (old_volume->source.is_converted_from_meters)
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<int>());
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.
if (coord_shift_set)
added->set_offset(added->get_transformation().get_offset() + coord_shift);
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) {
Expand Down
Loading