Skip to content

Commit 045e0a6

Browse files
Cstm3DBldrclaude
authored andcommitted
Preserve per-triangle painting through merge, split, merge-to-multipart, and repair
Per-triangle paint (color/MMU, support, seam, fuzzy-skin) was dropped when merging objects, splitting them, merging to a multi-part object, or repairing a mesh. This carries the facet annotations through those operations so painted data survives, matching the existing behavior for cut (#10613). - merge / split / merge-to-multipart: exact carry-through of the annotations. - repair: best-effort transfer across the re-projected mesh, with a cancelable warning dialog before a repair that may alter painted data. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d7236d3 commit 045e0a6

6 files changed

Lines changed: 178 additions & 6 deletions

File tree

‎src/libslic3r/Model.cpp‎

Lines changed: 144 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
#include "MTUtils.hpp"
1111
#include "TriangleMeshSlicer.hpp"
1212
#include "TriangleSelector.hpp"
13+
#include "AABBTreeIndirect.hpp"
14+
#include <queue>
1315

1416
#include "Format/AMF.hpp"
1517
#include "Format/svg.hpp"
@@ -2786,9 +2788,21 @@ ModelObjectPtrs ModelObject::merge_volumes(std::vector<int>& vol_indeces)
27862788

27872789
#if 1
27882790
TriangleMesh mesh;
2791+
// BBS: preserve painting across the merge. its_merge() appends faces in
2792+
// order (only vertex indices are offset), so face f of the merged mesh maps
2793+
// exactly to the captured per-part triangles below. Capture before
2794+
// reset_mesh() empties the source volumes.
2795+
std::vector<std::string> merged_supported, merged_seam, merged_mmu, merged_fuzzy;
27892796
for (int i : vol_indeces) {
27902797
auto volume = volumes[i];
27912798
if (!volume->mesh().empty()) {
2799+
const size_t nf = volume->mesh().its.indices.size();
2800+
for (size_t f = 0; f < nf; ++f) {
2801+
merged_supported.emplace_back(volume->supported_facets.get_triangle_as_string((int)f));
2802+
merged_seam.emplace_back(volume->seam_facets.get_triangle_as_string((int)f));
2803+
merged_mmu.emplace_back(volume->mmu_segmentation_facets.get_triangle_as_string((int)f));
2804+
merged_fuzzy.emplace_back(volume->fuzzy_skin_facets.get_triangle_as_string((int)f));
2805+
}
27922806
const auto volume_matrix = volume->get_matrix();
27932807
TriangleMesh mesh_(volume->mesh());
27942808
mesh_.transform(volume_matrix, true);
@@ -2808,6 +2822,13 @@ ModelObjectPtrs ModelObject::merge_volumes(std::vector<int>& vol_indeces)
28082822
#endif
28092823

28102824
ModelVolume* vol = upper->add_volume(mesh);
2825+
// BBS: re-apply the painting captured above onto the merged volume.
2826+
for (size_t f = 0; f < merged_mmu.size() && f < mesh.its.indices.size(); ++f) {
2827+
if (!merged_supported[f].empty()) vol->supported_facets.set_triangle_from_string((int)f, merged_supported[f]);
2828+
if (!merged_seam[f].empty()) vol->seam_facets.set_triangle_from_string((int)f, merged_seam[f]);
2829+
if (!merged_mmu[f].empty()) vol->mmu_segmentation_facets.set_triangle_from_string((int)f, merged_mmu[f]);
2830+
if (!merged_fuzzy[f].empty()) vol->fuzzy_skin_facets.set_triangle_from_string((int)f, merged_fuzzy[f]);
2831+
}
28112832
for (int i = 0; i < volumes.size();i++) {
28122833
if (std::find(vol_indeces.begin(), vol_indeces.end(), i) != vol_indeces.end()) {
28132834
vol->name = "Merged Parts";
@@ -3148,6 +3169,106 @@ void ModelVolume::reset_extra_facets() {
31483169
this->mmu_segmentation_facets.reset();
31493170
}
31503171

3172+
// ---- BBS: best-effort paint re-projection across mesh-rebuilding ops ----------
3173+
// Walk a TriangleSelector's split tree for source face s; return the first
3174+
// non-NONE leaf state (collapses sub-triangle painting to the dominant intent).
3175+
static EnforcerBlockerType reproj_first_leaf(TriangleSelector &sel, int root_idx)
3176+
{
3177+
const auto &tris = sel.get_triangles();
3178+
if (root_idx < 0 || root_idx >= (int)tris.size()) return EnforcerBlockerType::NONE;
3179+
std::queue<int> q;
3180+
q.push(root_idx);
3181+
while (!q.empty()) {
3182+
int i = q.front(); q.pop();
3183+
const auto &t = tris[i];
3184+
if (!t.valid()) continue;
3185+
if (!t.is_split()) {
3186+
if (t.get_state() != EnforcerBlockerType::NONE)
3187+
return t.get_state();
3188+
} else {
3189+
for (int c : t.children) if (c >= 0) q.push(c);
3190+
}
3191+
}
3192+
return EnforcerBlockerType::NONE;
3193+
}
3194+
3195+
// Per-face dominant state of one annotation layer over `mesh`.
3196+
static void reproj_per_face_states(const TriangleMesh &mesh, const FacetsAnnotation &ann,
3197+
std::vector<EnforcerBlockerType> &out)
3198+
{
3199+
out.assign(mesh.its.indices.size(), EnforcerBlockerType::NONE);
3200+
if (ann.empty() || mesh.its.indices.empty()) return;
3201+
TriangleSelector sel(mesh);
3202+
sel.deserialize(ann.get_data(), true);
3203+
for (int f = 0; f < (int)mesh.its.indices.size(); ++f)
3204+
out[f] = reproj_first_leaf(sel, f);
3205+
}
3206+
3207+
// Write per-face states onto `mesh`'s annotation layer (NONE entries skipped).
3208+
static void reproj_apply_states(const TriangleMesh &mesh,
3209+
const std::vector<EnforcerBlockerType> &states,
3210+
FacetsAnnotation &out)
3211+
{
3212+
out.reset();
3213+
bool any = false;
3214+
TriangleSelector sel(mesh);
3215+
const int n = std::min<int>((int)states.size(), (int)mesh.its.indices.size());
3216+
for (int f = 0; f < n; ++f) {
3217+
if (states[f] != EnforcerBlockerType::NONE) { sel.set_facet(f, states[f]); any = true; }
3218+
}
3219+
if (any) out.set(sel);
3220+
}
3221+
3222+
// Map each face of new_mesh to the nearest face of old_mesh (by centroid), then
3223+
// transfer the four precomputed old per-face state vectors onto new annotations.
3224+
static void reproj_transfer(const TriangleMesh &old_mesh, const TriangleMesh &new_mesh,
3225+
const std::vector<EnforcerBlockerType> &o_sup,
3226+
const std::vector<EnforcerBlockerType> &o_seam,
3227+
const std::vector<EnforcerBlockerType> &o_mmu,
3228+
const std::vector<EnforcerBlockerType> &o_fuzzy,
3229+
FacetsAnnotation &d_sup, FacetsAnnotation &d_seam,
3230+
FacetsAnnotation &d_mmu, FacetsAnnotation &d_fuzzy)
3231+
{
3232+
d_sup.reset(); d_seam.reset(); d_mmu.reset(); d_fuzzy.reset();
3233+
if (old_mesh.its.indices.empty() || new_mesh.its.indices.empty()) return;
3234+
auto tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(
3235+
old_mesh.its.vertices, old_mesh.its.indices);
3236+
const int nf = (int)new_mesh.its.indices.size();
3237+
std::vector<EnforcerBlockerType> n_sup(nf, EnforcerBlockerType::NONE), n_seam = n_sup, n_mmu = n_sup, n_fuzzy = n_sup;
3238+
for (int f = 0; f < nf; ++f) {
3239+
const Vec3i &idx = new_mesh.its.indices[f];
3240+
Vec3f c = (new_mesh.its.vertices[idx[0]] + new_mesh.its.vertices[idx[1]] + new_mesh.its.vertices[idx[2]]) / 3.f;
3241+
size_t hit = size_t(-1);
3242+
Vec3f hp;
3243+
double d2 = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(
3244+
old_mesh.its.vertices, old_mesh.its.indices, tree, c, hit, hp);
3245+
if (d2 < 0. || hit == size_t(-1) || hit >= o_sup.size()) continue;
3246+
n_sup[f] = o_sup[hit];
3247+
n_seam[f] = o_seam[hit];
3248+
n_mmu[f] = o_mmu[hit];
3249+
n_fuzzy[f] = o_fuzzy[hit];
3250+
}
3251+
reproj_apply_states(new_mesh, n_sup, d_sup);
3252+
reproj_apply_states(new_mesh, n_seam, d_seam);
3253+
reproj_apply_states(new_mesh, n_mmu, d_mmu);
3254+
reproj_apply_states(new_mesh, n_fuzzy, d_fuzzy);
3255+
}
3256+
3257+
void ModelVolume::set_mesh_keep_paint(TriangleMesh &&mesh_in)
3258+
{
3259+
const TriangleMesh old_mesh = this->mesh(); // copy before replacing
3260+
std::vector<EnforcerBlockerType> o_sup, o_seam, o_mmu, o_fuzzy;
3261+
reproj_per_face_states(old_mesh, this->supported_facets, o_sup);
3262+
reproj_per_face_states(old_mesh, this->seam_facets, o_seam);
3263+
reproj_per_face_states(old_mesh, this->mmu_segmentation_facets, o_mmu);
3264+
reproj_per_face_states(old_mesh, this->fuzzy_skin_facets, o_fuzzy);
3265+
this->set_mesh(std::move(mesh_in));
3266+
reproj_transfer(old_mesh, this->mesh(), o_sup, o_seam, o_mmu, o_fuzzy,
3267+
this->supported_facets, this->seam_facets,
3268+
this->mmu_segmentation_facets, this->fuzzy_skin_facets);
3269+
}
3270+
// ------------------------------------------------------------------------------
3271+
31513272
ModelMaterial* ModelVolume::material() const
31523273
{
31533274
return this->object->get_model()->get_material(m_material_id);
@@ -3523,10 +3644,19 @@ size_t ModelVolume::split(unsigned int max_extruders, float scale_det)
35233644
const bool src_assemble_initialized = this->is_assemble_initialized();
35243645
const Transform3d src_assemble_matrix = this->get_assemble_transformation().get_matrix();
35253646
std::vector<std::string> tris_split_strs;
3647+
// BBS: also carry support/seam/fuzzy-skin painting across the split (the
3648+
// ships[] relationship maps each split face back to its source face).
3649+
std::vector<std::string> tris_sup_strs, tris_seam_strs, tris_fuzzy_strs;
35263650
auto face_count = m_mesh->its.indices.size();
35273651
tris_split_strs.reserve(face_count);
3652+
tris_sup_strs.reserve(face_count);
3653+
tris_seam_strs.reserve(face_count);
3654+
tris_fuzzy_strs.reserve(face_count);
35283655
for (size_t i = 0; i < face_count; i++) {
35293656
tris_split_strs.emplace_back(mmu_segmentation_facets.get_triangle_as_string(i));
3657+
tris_sup_strs.emplace_back(supported_facets.get_triangle_as_string(i));
3658+
tris_seam_strs.emplace_back(seam_facets.get_triangle_as_string(i));
3659+
tris_fuzzy_strs.emplace_back(fuzzy_skin_facets.get_triangle_as_string(i));
35303660
}
35313661
int last_all_mesh_face_count = 0;
35323662
for (TriangleMesh &mesh : meshes) {
@@ -3552,9 +3682,14 @@ size_t ModelVolume::split(unsigned int max_extruders, float scale_det)
35523682
for (size_t i = 0; i < cur_face_count; i++) {
35533683
if (ships[idx].find(i) != ships[idx].end()) {
35543684
auto index = ships[idx][i];
3555-
if (tris_split_strs[index].size() > 0) {
3685+
if (tris_split_strs[index].size() > 0)
35563686
mmu_segmentation_facets.set_triangle_from_string(i, tris_split_strs[index]);
3557-
}
3687+
if (tris_sup_strs[index].size() > 0)
3688+
supported_facets.set_triangle_from_string(i, tris_sup_strs[index]);
3689+
if (tris_seam_strs[index].size() > 0)
3690+
seam_facets.set_triangle_from_string(i, tris_seam_strs[index]);
3691+
if (tris_fuzzy_strs[index].size() > 0)
3692+
fuzzy_skin_facets.set_triangle_from_string(i, tris_fuzzy_strs[index]);
35583693
}
35593694
}
35603695
} else {
@@ -3563,9 +3698,14 @@ size_t ModelVolume::split(unsigned int max_extruders, float scale_det)
35633698
for (size_t i = 0; i < new_mv->mesh_ptr()->its.indices.size(); i++) {
35643699
if (ships[idx].find(i) != ships[idx].end()) {
35653700
auto index = ships[idx][i];
3566-
if (tris_split_strs[index].size() > 0) {
3701+
if (tris_split_strs[index].size() > 0)
35673702
new_mv->mmu_segmentation_facets.set_triangle_from_string(i, tris_split_strs[index]);
3568-
}
3703+
if (tris_sup_strs[index].size() > 0)
3704+
new_mv->supported_facets.set_triangle_from_string(i, tris_sup_strs[index]);
3705+
if (tris_seam_strs[index].size() > 0)
3706+
new_mv->seam_facets.set_triangle_from_string(i, tris_seam_strs[index]);
3707+
if (tris_fuzzy_strs[index].size() > 0)
3708+
new_mv->fuzzy_skin_facets.set_triangle_from_string(i, tris_fuzzy_strs[index]);
35693709
}
35703710
}
35713711
}

‎src/libslic3r/Model.hpp‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -991,6 +991,11 @@ class ModelVolume final : public ObjectBase
991991
t_model_material_id material_id() const { return m_material_id; }
992992
void set_material_id(t_model_material_id material_id);
993993
void reset_extra_facets();
994+
// BBS: best-effort paint preservation across mesh-rebuilding operations
995+
// (repair/simplify/smooth). Replaces the mesh, then re-projects all four
996+
// paint layers from the OLD mesh onto the new one by nearest-surface lookup.
997+
// Approximate: a remeshed surface has no exact face correspondence.
998+
void set_mesh_keep_paint(TriangleMesh &&mesh);
994999
ModelMaterial* material() const;
9951000
void set_material(t_model_material_id material_id, const ModelMaterial &material);
9961001
// Extract the current extruder ID based on this ModelVolume's config and the parent ModelObject's config.

‎src/slic3r/GUI/GUI_App.cpp‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2498,6 +2498,18 @@ static LogEncOptions s_get_log_enc_opts()
24982498
return enc_options;
24992499
};
25002500

2501+
bool GUI_App::confirm_mesh_paint_warning()
2502+
{
2503+
MessageDialog dlg(nullptr,
2504+
_L("This operation rebuilds the model's mesh. Painted color, supports, seam and "
2505+
"fuzzy-skin will be transferred to the new mesh by a best-effort approximation, "
2506+
"so the result may be slightly off and, in rare cases, some painting may be lost.\n\n"
2507+
"Do you want to continue?"),
2508+
_L("Painting may change"),
2509+
wxICON_WARNING | wxYES_NO | wxNO_DEFAULT);
2510+
return dlg.ShowModal() == wxID_YES;
2511+
}
2512+
25012513
void GUI_App::init_app_config()
25022514
{
25032515
// Profiles for the alpha are stored into the PrusaSlicer-alpha directory to not mix with the current release.

‎src/slic3r/GUI/GUI_App.hpp‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,10 @@ class GUI_App : public wxApp
396396

397397
bool get_app_conf_exists() { return m_app_conf_exists; }
398398
void show_message_box(std::string msg) { wxMessageBox(msg); }
399+
// BBS: warn before a mesh-rebuilding op (repair/simplify/smooth/boolean) that
400+
// painting is transferred by best-effort approximation and may be imperfect.
401+
// Returns true if the user chooses to continue.
402+
bool confirm_mesh_paint_warning();
399403
EAppMode get_app_mode() const { return m_app_mode; }
400404
Slic3r::DeviceManager* getDeviceManager() { return m_device_manager; }
401405
bool is_blocking_printing(MachineObject *obj_ = nullptr);

‎src/slic3r/GUI/GUI_ObjectList.cpp‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3247,6 +3247,11 @@ void ObjectList::merge(bool to_multipart_object)
32473247
}
32483248
}
32493249
new_volume->mmu_segmentation_facets.assign(std::move(volume->mmu_segmentation_facets));
3250+
// BBS: also carry support/seam/fuzzy-skin painting across the merge
3251+
// (each part keeps its own mesh, so the per-triangle data maps 1:1).
3252+
new_volume->supported_facets.assign(std::move(volume->supported_facets));
3253+
new_volume->seam_facets.assign(std::move(volume->seam_facets));
3254+
new_volume->fuzzy_skin_facets.assign(std::move(volume->fuzzy_skin_facets));
32503255
}
32513256
new_object->sort_volumes(true);
32523257

@@ -6243,6 +6248,10 @@ void ObjectList::fix_through_netfabb()
62436248
if (!wxGetApp().plater()->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::Undefined))
62446249
return;
62456250

6251+
// BBS: repair rebuilds the mesh; warn that painting is transferred approximately.
6252+
if (!wxGetApp().confirm_mesh_paint_warning())
6253+
return;
6254+
62466255
// model_name
62476256
std::vector<std::string> succes_models;
62486257
// model_name failing reason
@@ -6300,7 +6309,9 @@ void ObjectList::fix_through_netfabb()
63006309
msg += "\n";
63016310
}
63026311

6303-
plater->clear_before_change_mesh(obj_idx);
6312+
// BBS: do NOT wipe painting here; the repair below re-projects it
6313+
// (best-effort) via set_mesh_keep_paint instead.
6314+
// plater->clear_before_change_mesh(obj_idx);
63046315
std::string res;
63056316
if (!fix_model_by_win10_sdk_gui(*(object(obj_idx)), vol_idx, progress_dlg, msg, res))
63066317
return false;

‎src/slic3r/Utils/FixModelByWin10.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,7 @@ bool fix_model_by_win10_sdk_gui(ModelObject &model_object, int volume_idx, GUI::
486486
meshes_repaired.emplace_back(std::move(repaired_its));
487487
}
488488
for (size_t i = 0; i < volumes.size(); ++ i) {
489-
volumes[i]->set_mesh(std::move(meshes_repaired[i]));
489+
volumes[i]->set_mesh_keep_paint(std::move(meshes_repaired[i])); // BBS: best-effort paint transfer
490490
volumes[i]->calculate_convex_hull();
491491
volumes[i]->invalidate_convex_hull_2d();
492492
volumes[i]->set_new_unique_id();

0 commit comments

Comments
 (0)