Add Precise Seam placement feature (#12974)

Co-authored-by: Ioannis Giannakas <59056762+igiannakas@users.noreply.github.com>
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
This commit is contained in:
Damir Galeev
2026-09-28 14:40:55 -03:00
committed by GitHub
co-authored by Ioannis Giannakas Rodrigo Faselli Ian Bassi
parent 08f086daf3
commit 1846407e93
47 changed files with 3745 additions and 126 deletions
+131 -10
View File
@@ -1944,6 +1944,12 @@ bool ObjectList::can_drop(const wxDataViewItem& item, int& src_obj_id, int& src_
if (dragged_item_v_type == item_v_type && dragged_item_v_type != ModelVolumeType::MODEL_PART)
return true;
// Use tree item types: hidden cut connectors make tree indices differ from volumes indices.
if (is_precise_seam(dragged_item_v_type) && is_precise_seam(item_v_type))
// Allow reordering only within the strong or weak modifier group.
return is_precise_seam_strong(dragged_item_v_type) == is_precise_seam_strong(item_v_type);
if ((dragged_item_v_type != item_v_type) || // we can't reorder volumes outside of types
item_v_type >= ModelVolumeType::SUPPORT_BLOCKER) // support blockers/enforcers can't change its place
return false;
@@ -2033,10 +2039,43 @@ void ObjectList::OnDrop(wxDataViewEvent &event)
int to_volume_id = m_objects_model->GetVolumeIdByItem(item);
int delta = to_volume_id < from_volume_id ? -1 : 1;
auto &volumes = (*m_objects)[m_dragged_data.obj_idx()]->volumes;
const int obj_idx = m_dragged_data.obj_idx();
// Object-indexed UI maps may be stale after another object is removed or reordered.
if (obj_idx < 0 || size_t(obj_idx) >= m_objects->size()) {
event.Veto();
m_dragged_data.clear();
return;
}
const ModelObject *object = (*m_objects)[obj_idx];
auto &volumes = (*m_objects)[obj_idx]->volumes;
std::vector<size_t> visible_volume_indices;
visible_volume_indices.reserve(volumes.size());
for (size_t idx = 0; idx < volumes.size(); ++idx)
// Match add_volumes_to_object_in_list: only connectors of cut objects are hidden.
if (!(object->is_cut() && volumes[idx]->is_cut_connector()))
visible_volume_indices.push_back(idx);
int cnt = 0;
for (int id = from_volume_id; cnt < abs(from_volume_id - to_volume_id); id += delta, cnt++) std::swap(volumes[id], volumes[id + delta]);
// Validate the entire move before any swap; these checks must also protect Release builds.
if (from_volume_id < 0 || to_volume_id < 0 ||
size_t(from_volume_id) >= visible_volume_indices.size() || size_t(to_volume_id) >= visible_volume_indices.size()) {
event.Veto();
m_dragged_data.clear();
return;
}
// Move through visible slots only, keeping hidden cut connectors at their original indices.
// The local mapping stays valid because no hidden volume changes slots during the move.
for (int id = from_volume_id; id != to_volume_id; id += delta) {
const size_t current_idx = visible_volume_indices[id];
const size_t next_idx = visible_volume_indices[id + delta];
std::swap(volumes[current_idx], volumes[next_idx]);
}
// Later selection/filament handlers use this cache; repair it for the moved object too.
auto &ui_to_model = m_objects_model->get_ui_and_3d_volume_map()[obj_idx];
ui_to_model.clear();
for (size_t idx = 0; idx < visible_volume_indices.size(); ++idx)
ui_to_model[int(idx)] = int(visible_volume_indices[idx]);
select_item(m_objects_model->ReorganizeChildren(from_volume_id, to_volume_id, m_objects_model->GetParent(item)));
@@ -3855,7 +3894,8 @@ wxDataViewItem ObjectList::add_settings_item(wxDataViewItem parent_item, const D
const bool is_layer_settings = m_objects_model->GetItemType(parent_item) == itLayer;
if (!is_object_settings) {
ModelVolumeType volume_type = m_objects_model->GetVolumeType(parent_item);
if (volume_type == ModelVolumeType::NEGATIVE_VOLUME || volume_type == ModelVolumeType::SUPPORT_BLOCKER || volume_type == ModelVolumeType::SUPPORT_ENFORCER)
// Precise Seam is non-printing helper geometry — no per-volume settings
if (volume_type == ModelVolumeType::NEGATIVE_VOLUME || volume_type == ModelVolumeType::SUPPORT_BLOCKER || volume_type == ModelVolumeType::SUPPORT_ENFORCER || is_precise_seam(volume_type))
return ret;
}
@@ -5712,7 +5752,6 @@ void ObjectList::change_part_type()
return;
}
#endif
ModelVolumeType ObjectList::get_selected_volume_type()
{
ModelVolume* volume = get_selected_model_volume();
@@ -5721,13 +5760,49 @@ ModelVolumeType ObjectList::get_selected_volume_type()
return ModelVolumeType::INVALID;
}
void ObjectList::set_volume_type(ModelVolumeType new_type)
// ---------------- Helpers for Precise Seam group-aware type changes ----------------
// Used by set_volume_type() and the "Precise Seam Type" subtype submenu handler.
// Detects whether a type change crosses a Precise Seam "group boundary" that requires
// manual repositioning inside ModelObject::volumes[]:
// - between any non-PS type and any PS subtype, or
// - between strong PS (CENTER/LEFT/RIGHT) and weak PS (ENFORCED/BLOCKED/NEUTRAL).
// sort_volumes() is stable and groups strong PS before weak PS. When a volume crosses
// a group, moving it to the end of volumes[] lets the subsequent stable sort place it
// at the end of its new group. Without this, a strong→weak transition would leave the
// volume stuck in the strong segment and break the modifier-application order.
static bool precise_seam_group_changed(ModelVolumeType old_type, ModelVolumeType new_type)
{
const bool old_is_ps = is_precise_seam(old_type);
const bool new_is_ps = is_precise_seam(new_type);
if (old_is_ps != new_is_ps)
return true; // non-PS ↔ PS transition
if (!old_is_ps)
return false; // both non-PS — regular enum ordering is enough
return is_precise_seam_strong(old_type) != is_precise_seam_strong(new_type);
}
// Moves a volume to the end of ModelObject::volumes[]. Combined with stable sort_volumes(),
// this places it at the end of its new group while preserving relative order of others.
static void move_volume_to_end(ModelObject* obj, ModelVolume* volume)
{
if (obj == nullptr || volume == nullptr)
return;
auto it = std::find(obj->volumes.begin(), obj->volumes.end(), volume);
if (it != obj->volumes.end()) {
obj->volumes.erase(it);
obj->volumes.push_back(volume);
}
}
void ObjectList::set_volume_type(ModelVolumeType new_type, bool preserve_ps_subtype)
{
struct VolumeSelection {
int object_idx;
ModelVolume* volume;
};
// --- Collect selected volumes from the object tree, falling back to the 3D canvas ---
std::vector<VolumeSelection> volumes;
auto add_volume = [&volumes](int obj_idx, ModelVolume* volume) {
if (volume == nullptr)
@@ -5792,25 +5867,49 @@ void ObjectList::set_volume_type(ModelVolumeType new_type)
// and historically crashed (originally fixed in the now-disabled change_part_type() by hiding
// Support entries in the old choice dialog; the UI-side guard for the current submenu lives
// in MenuFactory::append_menu_item_change_type, see #13120).
// The same rationale applies to any Precise Seam subtype.
// This block must never be reachable under a healthy UI; if it ever logs, the UI guard has
// been bypassed (new entry point, refactor, plugin, etc.) and should be investigated.
if (new_type == ModelVolumeType::SUPPORT_BLOCKER || new_type == ModelVolumeType::SUPPORT_ENFORCER) {
if (new_type == ModelVolumeType::SUPPORT_BLOCKER || new_type == ModelVolumeType::SUPPORT_ENFORCER
|| is_precise_seam(new_type)) {
const bool has_text_or_svg = std::any_of(volumes.begin(), volumes.end(),
[](const VolumeSelection& sel) { return sel.volume->is_svg() || sel.volume->is_text(); });
if (has_text_or_svg) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__
<< ": blocked attempt to set SUPPORT_BLOCKER/ENFORCER on SVG/text volume; "
<< ": blocked attempt to set SUPPORT_BLOCKER/ENFORCER or Precise Seam on SVG/text volume; "
<< "UI guard should have prevented this -- possible regression in the Change Type menu";
return;
}
}
// --- Subtype preservation for the generic "Precise Seam" entry ---
// The Change Type submenu uses PRECISE_SEAM_CENTER as the single "Precise Seam" entry
// and calls with preserve_ps_subtype=true: volumes that are already Precise Seam keep
// their subtype (LEFT/RIGHT/etc.); only non-PS volumes get converted to the default
// CENTER subtype.
//
// The "Precise Seam Type" subtype submenu calls with preserve_ps_subtype=false: the
// user explicitly picked CENTER and all selected PS volumes must be set to CENTER
// verbatim (otherwise CENTER would become unreachable for mixed-subtype selections,
// since preservation would keep every volume at its current subtype and any_diff would
// be false — the reason for this two-parameter split).
auto effective_new_type = [new_type, preserve_ps_subtype](const ModelVolume* v) -> ModelVolumeType {
if (preserve_ps_subtype && new_type == ModelVolumeType::PRECISE_SEAM_CENTER && v->is_precise_seam())
return v->type();
return new_type;
};
// --- Any-change check using the effective target type ---
const bool any_diff = std::any_of(volumes.begin(), volumes.end(),
[new_type](const VolumeSelection& sel) { return sel.volume->type() != new_type; });
[&effective_new_type](const VolumeSelection& sel) {
return sel.volume->type() != effective_new_type(sel.volume);
});
if (!any_diff)
return;
// --- Last-solid-part guard (pre-existing behavior) ---
// When converting away from MODEL_PART, ensure at least one solid part remains per object.
if (new_type != ModelVolumeType::MODEL_PART) {
std::map<int, int> total_part_cnt;
std::map<int, int> selected_part_cnt;
@@ -5838,14 +5937,36 @@ void ObjectList::set_volume_type(ModelVolumeType new_type)
take_snapshot(_u8L("Change part type"));
// --- Apply type changes with Precise Seam group-aware repositioning ---
// Note: `changed_volumes` / `touched_objects` track every processed volume, including
// subtype-preservation no-ops. Tracking no-ops is intentional — it ensures such
// volumes remain selected after the post-apply rebuild. Without that, a cross-object
// mixed selection like [Part, PS_LEFT] clicking "Change type → Precise Seam" would
// silently deselect the preserved PS_LEFT: only the Part's object would enter
// touched_objects, and the final SetSelections(new_selection) would drop PS_LEFT.
std::set<const ModelVolume*> changed_volumes;
std::set<int> touched_objects;
for (const auto& sel : volumes) {
sel.volume->set_type(new_type);
const ModelVolumeType target = effective_new_type(sel.volume);
const ModelVolumeType old_type = sel.volume->type();
// Record the volume for reselection before the no-op short-circuit (see comment above).
changed_volumes.insert(sel.volume);
touched_objects.insert(sel.object_idx);
if (old_type == target)
continue; // subtype-preservation no-op — nothing to write/move
sel.volume->set_type(target);
// If the change crosses a PS group boundary, move the volume to the end of volumes[]
// so the stable sort in reorder_volumes_and_get_selection() places it at the end of
// its new group (see precise_seam_group_changed() for details).
if (precise_seam_group_changed(old_type, target))
move_volume_to_end((*m_objects)[sel.object_idx], sel.volume);
}
// --- Rebuild selection to follow the changed volumes ---
wxDataViewItemArray new_selection;
for (int obj_idx : touched_objects) {
wxDataViewItemArray sel_items = reorder_volumes_and_get_selection(obj_idx, [&changed_volumes](const ModelVolume* volume) {