Keep painting after cut (#13472)

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)


This can be enabled in Preference->Developer->Keep painted feature after
mesh change.

<img width="731" height="633" alt="init"
src="https://github.com/user-attachments/assets/8b195486-538e-4eda-9e77-bfdf1a794306"
/>


TODO:
- [ ] Bug fixes
- [ ] Make it faster
- Keep painting after other mesh operations such as reload from
disk/simplify/boolean operation etc:
  - [x] Planar cut
  - [x] Dovetail cut
  - [x] Cut with part assigned to other side
  - [x] Split to parts
  - [x] Split to objects
  - [x] Mesh boolean gizmo
  - [x] Mesh boolean in right click
- [x] Reload from disk/replace stl (won't work well if mesh changed too
much)
  - [x] Fix model
- [x] Simplify/smooth (this two won't work well due to too much mesh
changes)
- [x] Add options in settings since I think this will be experimental
for a long time until being tested by a lot of ppl
This commit is contained in:
Noisyfox
2026-05-17 14:38:37 +08:00
committed by GitHub
18 changed files with 665 additions and 92 deletions

View File

@@ -2849,7 +2849,7 @@ void ObjectList::split()
take_snapshot("Split to parts");
volume->split(filament_cnt);
volume->split(filament_cnt, wxGetApp().app_config->get_bool("keep_painting"));
wxBusyCursor wait;
@@ -3041,7 +3041,6 @@ void ObjectList::merge(bool to_multipart_object)
auto opt = object->config.option("extruder");
if (opt) { new_volume->config.set_key_value("extruder", new ConfigOptionInt(opt->getInt())); }
}
new_volume->mmu_segmentation_facets.assign(std::move(volume->mmu_segmentation_facets));
}
new_object->sort_volumes(true);
@@ -3226,6 +3225,22 @@ void ObjectList::boolean()
Plater::TakeSnapshot snapshot(wxGetApp().plater(), "boolean");
ModelObject* object = (*m_objects)[obj_idxs.front()];
const bool keep_painting = wxGetApp().app_config->get_bool("keep_painting");
std::vector<std::optional<TriangleSelector::SavedPainting>> saved_paintings;
if (keep_painting) {
// Save painting of all the positive parts
saved_paintings.reserve(object->volumes.size());
for (const ModelVolume* vol : object->volumes) {
if (vol && vol->mesh_ptr() && vol->is_model_part() && vol->is_any_painted()) {
saved_paintings.emplace_back(vol->save_painting());
if (saved_paintings.back()) {
saved_paintings.back()->mesh.transform(vol->get_matrix(), true);
}
}
}
}
TriangleMesh mesh = Plater::combine_mesh_fff(*object, -1, [this](const std::string& msg) {return wxGetApp().notification_manager()->push_plater_error_notification(msg); });
// add mesh to model as a new object, keep the original object's name and config
@@ -3237,6 +3252,29 @@ void ObjectList::boolean()
new_object->add_instance();
ModelVolume* new_volume = new_object->add_volume(mesh);
// Remap paint
if (keep_painting) {
for (auto& saved_painting : saved_paintings) {
if (saved_painting) {
// For each original painted volume, we need to apply to each instance
// because we merged all instances into one in `combine_mesh_fff`
// First we save the non-instance-translated mesh
TriangleMesh vols_mesh(std::move(saved_painting->mesh));
for (const ModelInstance* i : object->instances) {
// Then for each instance, we apply the paint at the given instance place
saved_painting->mesh = vols_mesh;
saved_painting->mesh.transform(i->get_matrix());
// Then paint it
new_volume->restore_painting(saved_painting, true);
}
}
}
}
// BBS: ensure on bed but no need to ensure locate in the center around origin
new_object->ensure_on_bed();
new_object->center_around_origin();
@@ -6048,10 +6086,13 @@ void ObjectList::fix_through_cgal()
msg += "\n";
}
plater->clear_before_change_mesh(obj_idx);
const bool keep_painting = GUI::wxGetApp().app_config->get_bool("keep_painting");
if (!keep_painting) {
plater->clear_before_change_mesh(obj_idx);
}
const size_t volumes_before = object(obj_idx)->volumes.size();
std::string res;
if (!fix_model_with_cgal_gui(*(object(obj_idx)), vol_idx, progress_dlg, msg, res))
if (!fix_model_with_cgal_gui(*(object(obj_idx)), vol_idx, progress_dlg, msg, res, keep_painting))
return false;
//wxGetApp().plater()->changed_mesh(obj_idx);
object(obj_idx)->ensure_on_bed();
@@ -6169,6 +6210,7 @@ void GUI::ObjectList::smooth_mesh()
WarningDialog dlg(static_cast<wxWindow *>(wxGetApp().mainframe), content, wxEmptyString, wxOK);
dlg.ShowModal();
};
const bool keep_painting = GUI::wxGetApp().app_config->get_bool("keep_painting");
bool has_show_smooth_mesh_error_dlg = false;
if (vol_idxs.empty()) {
obj = object(object_idx);
@@ -6180,8 +6222,11 @@ void GUI::ObjectList::smooth_mesh()
bool ok;
auto result_mesh = TriangleMeshDeal::smooth_triangle_mesh(mv->mesh(), ok);
if (ok) {
const std::optional<TriangleSelector::SavedPainting> saved_painting = keep_painting ?
mv->save_painting() :
std::optional<TriangleSelector::SavedPainting>{};
mv->set_mesh(result_mesh);
mv->reset_extra_facets(); // reset paint color
mv->restore_painting(saved_painting);
mv->calculate_convex_hull();
mv->invalidate_convex_hull_2d();
mv->set_new_unique_id();
@@ -6206,8 +6251,11 @@ void GUI::ObjectList::smooth_mesh()
bool ok;
auto result_mesh = TriangleMeshDeal::smooth_triangle_mesh(mv->mesh(),ok);
if (ok) {
const std::optional<TriangleSelector::SavedPainting> saved_painting = keep_painting ?
mv->save_painting() :
std::optional<TriangleSelector::SavedPainting>{};
mv->set_mesh(result_mesh);
mv->reset_extra_facets(); // reset paint color
mv->restore_painting(saved_painting);
mv->calculate_convex_hull();
mv->invalidate_convex_hull_2d();
mv->set_new_unique_id();

View File

@@ -1908,7 +1908,7 @@ GLGizmoCut3D::PartSelection::PartSelection(const ModelObject* mo, const Transfor
// split to parts
for (int id = int(volumes.size())-1; id >= 0; id--)
if (volumes[id]->is_splittable())
volumes[id]->split(1);
volumes[id]->split(1, false); // No need to remap paint here, we do it later in perform_by_contour
m_parts.clear();
for (const ModelVolume* volume : volumes) {
@@ -3321,6 +3321,7 @@ void GLGizmoCut3D::perform_cut(const Selection& selection)
wxBusyCursor wait;
const bool keep_painting = GUI::wxGetApp().app_config->get_bool("keep_painting");
ModelObjectCutAttributes attributes = only_if(has_connectors ? true : m_keep_upper, ModelObjectCutAttribute::KeepUpper) |
only_if(has_connectors ? true : m_keep_lower, ModelObjectCutAttribute::KeepLower) |
only_if(has_connectors ? false : m_keep_as_parts, ModelObjectCutAttribute::KeepAsParts) |
@@ -3329,13 +3330,14 @@ void GLGizmoCut3D::perform_cut(const Selection& selection)
only_if(m_rotate_upper, ModelObjectCutAttribute::FlipUpper) |
only_if(m_rotate_lower, ModelObjectCutAttribute::FlipLower) |
only_if(dowels_count > 0, ModelObjectCutAttribute::CreateDowels) |
only_if(!has_connectors && !cut_with_groove && cut_mo->cut_id.id().invalid(), ModelObjectCutAttribute::InvalidateCutInfo);
only_if(!has_connectors && !cut_with_groove && cut_mo->cut_id.id().invalid(), ModelObjectCutAttribute::InvalidateCutInfo) |
only_if(keep_painting, ModelObjectCutAttribute::KeepPaint);
// update cut_id for the cut object in respect to the attributes
update_object_cut_id(cut_mo->cut_id, attributes, dowels_count);
Cut cut(cut_mo, instance_idx, get_cut_matrix(selection), attributes);
const ModelObjectPtrs& new_objects = cut_by_contour ? cut.perform_by_contour(m_part_selection.get_cut_parts(), dowels_count):
const ModelObjectPtrs& new_objects = cut_by_contour ? cut.perform_by_contour(mo, m_part_selection.get_cut_parts(), dowels_count):
cut_with_groove ? cut.perform_with_groove(m_groove, m_rotation_m) :
cut.perform_with_plane();
@@ -3362,12 +3364,12 @@ void GLGizmoCut3D::perform_cut(const Selection& selection)
// model_name failing reason
std::vector<std::pair<std::string, std::string>> failed_models;
auto plater = wxGetApp().plater();
auto fix_and_update_progress = [this, plater](ModelObject *model_object, const int vol_idx, const string &model_name, ProgressDialog &progress_dlg,
auto fix_and_update_progress = [this, plater, keep_painting](ModelObject *model_object, const int vol_idx, const string &model_name, ProgressDialog &progress_dlg,
std::vector<std::string> &succes_models, std::vector<std::pair<std::string, std::string>> &failed_models) {
wxString msg = _L("Repairing model object");
msg += ": " + from_u8(model_name) + "\n";
std::string res;
if (!fix_model_with_cgal_gui(*model_object, vol_idx, progress_dlg, msg, res)) return false;
if (!fix_model_with_cgal_gui(*model_object, vol_idx, progress_dlg, msg, res, keep_painting)) return false;
return true;
};
ProgressDialog progress_dlg(_L("Repairing model object"), "", 100, find_toplevel_parent(plater), wxPD_AUTO_HIDE | wxPD_APP_MODAL | wxPD_CAN_ABORT, true);

View File

@@ -188,6 +188,19 @@ CommonGizmosDataID GLGizmoMeshBoolean::on_get_requirements() const
| int(CommonGizmosDataID::ObjectClipper));
}
std::optional<TriangleSelector::SavedPainting> VolumeInfo::save_painting() const
{
if (wxGetApp().app_config->get_bool("keep_painting")) {
std::optional<TriangleSelector::SavedPainting> saved_painting = mv->save_painting();
if (saved_painting) {
saved_painting->mesh.transform(trafo);
}
return saved_painting;
}
return {};
}
void GLGizmoMeshBoolean::on_render_input_window(float x, float y, float bottom_limit)
{
y = std::min(y, bottom_limit - ImGui::GetWindowHeight());
@@ -346,7 +359,9 @@ void GLGizmoMeshBoolean::on_render_input_window(float x, float y, float bottom_l
std::vector<TriangleMesh> temp_mesh_resuls;
Slic3r::MeshBoolean::mcut::make_boolean(temp_src_mesh, temp_tool_mesh, temp_mesh_resuls, "UNION");
if (temp_mesh_resuls.size() != 0) {
generate_new_volume(true, *temp_mesh_resuls.begin());
// For union, we want to keep paint from both meshes
std::vector<std::optional<TriangleSelector::SavedPainting>> saved_paintings{m_src.save_painting(), m_tool.save_painting()};
generate_new_volume(true, *temp_mesh_resuls.begin(), saved_paintings);
wxGetApp().notification_manager()->close_plater_warning_notification(warning_text);
}
else {
@@ -364,7 +379,9 @@ void GLGizmoMeshBoolean::on_render_input_window(float x, float y, float bottom_l
std::vector<TriangleMesh> temp_mesh_resuls;
Slic3r::MeshBoolean::mcut::make_boolean(temp_src_mesh, temp_tool_mesh, temp_mesh_resuls, "A_NOT_B");
if (temp_mesh_resuls.size() != 0) {
generate_new_volume(m_diff_delete_input, *temp_mesh_resuls.begin());
// For diff, we only need paint from src
std::vector<std::optional<TriangleSelector::SavedPainting>> saved_paintings{m_src.save_painting()};
generate_new_volume(m_diff_delete_input, *temp_mesh_resuls.begin(), saved_paintings);
wxGetApp().notification_manager()->close_plater_warning_notification(warning_text);
}
else {
@@ -382,7 +399,9 @@ void GLGizmoMeshBoolean::on_render_input_window(float x, float y, float bottom_l
std::vector<TriangleMesh> temp_mesh_resuls;
Slic3r::MeshBoolean::mcut::make_boolean(temp_src_mesh, temp_tool_mesh, temp_mesh_resuls, "INTERSECTION");
if (temp_mesh_resuls.size() != 0) {
generate_new_volume(m_inter_delete_input, *temp_mesh_resuls.begin());
// For intersection, we want to keep paint from both meshes
std::vector<std::optional<TriangleSelector::SavedPainting>> saved_paintings{m_src.save_painting(), m_tool.save_painting()};
generate_new_volume(m_inter_delete_input, *temp_mesh_resuls.begin(), saved_paintings);
wxGetApp().notification_manager()->close_plater_warning_notification(warning_text);
}
else {
@@ -420,7 +439,7 @@ void GLGizmoMeshBoolean::on_save(cereal::BinaryOutputArchive &ar) const
ar(m_enable, m_operation_mode, m_selecting_state, m_diff_delete_input, m_inter_delete_input, m_src, m_tool);
}
void GLGizmoMeshBoolean::generate_new_volume(bool delete_input, const TriangleMesh& mesh_result) {
void GLGizmoMeshBoolean::generate_new_volume(const bool delete_input, TriangleMesh& mesh_result, const std::vector<std::optional<TriangleSelector::SavedPainting>>& saved_paintings) {
wxGetApp().plater()->take_snapshot("Mesh Boolean");
@@ -429,6 +448,11 @@ void GLGizmoMeshBoolean::generate_new_volume(bool delete_input, const TriangleMe
// generate new volume
ModelVolume* new_volume = curr_model_object->add_volume(std::move(mesh_result));
// Remap paintings
for (const auto& saved_painting : saved_paintings) {
new_volume->restore_painting(saved_painting, true);
}
// assign to new_volume from old_volume
ModelVolume* old_volume = m_src.mv;
std::string suffix;

View File

@@ -34,6 +34,8 @@ struct VolumeInfo {
void serialize(Archive& ar) {
ar(volume_idx, trafo);
}
std::optional<TriangleSelector::SavedPainting> save_painting() const;
};
class GLGizmoMeshBoolean : public GLGizmoBase
{
@@ -87,7 +89,7 @@ private:
VolumeInfo m_src;
VolumeInfo m_tool;
void generate_new_volume(bool delete_input, const TriangleMesh& mesh_result);
void generate_new_volume(bool delete_input, TriangleMesh& mesh_result, const std::vector<std::optional<TriangleSelector::SavedPainting>>& saved_paintings);
};
} // namespace GUI

View File

@@ -535,12 +535,20 @@ void GLGizmoSimplify::apply_simplify() {
auto plater = wxGetApp().plater();
plater->take_snapshot(GUI::format("Simplify %1%", m_volume->name));
plater->clear_before_change_mesh(object_idx);
const bool keep_painting = GUI::wxGetApp().app_config->get_bool("keep_painting");
if (!keep_painting) {
plater->clear_before_change_mesh(object_idx);
}
ModelVolume* mv = get_model_volume(selection, wxGetApp().model());
assert(mv == m_volume);
// Save paint
std::optional<TriangleSelector::SavedPainting> saved_painting = keep_painting ? mv->save_painting() :
std::optional<TriangleSelector::SavedPainting>{};
mv->set_mesh(std::move(*m_state.result));
// Remap paint
mv->restore_painting(saved_painting);
m_state.result.reset();
mv->calculate_convex_hull();
mv->invalidate_convex_hull_2d();

View File

@@ -7627,7 +7627,7 @@ void Plater::priv::split_object(int obj_idx, bool auto_drop /* = true */)
wxBusyCursor wait;
ModelObjectPtrs new_objects;
current_model_object->split(&new_objects);
current_model_object->split(&new_objects, wxGetApp().app_config->get_bool("keep_painting"));
if (new_objects.size() == 1)
// #ysFIXME use notification
Slic3r::GUI::warning_catcher(q, _L("The selected object couldn't be split."));
@@ -8375,10 +8375,20 @@ bool Plater::priv::replace_volume_with_stl(int object_idx, int volume_idx, const
new_volume->convert_from_imperial_units();
else if (old_volume->source.is_converted_from_meters)
new_volume->convert_from_meters();
new_volume->supported_facets.assign(old_volume->supported_facets);
new_volume->seam_facets.assign(old_volume->seam_facets);
new_volume->mmu_segmentation_facets.assign(old_volume->mmu_segmentation_facets);
new_volume->fuzzy_skin_facets.assign(old_volume->fuzzy_skin_facets);
if (wxGetApp().app_config->get_bool("keep_painting")) {
// Proper paint remapping
auto saved_painting = old_volume->save_painting();
if (saved_painting) {
saved_painting->mesh.transform(Geometry::translation_transform(new_volume->mesh().get_init_shift()));
new_volume->restore_painting(saved_painting);
}
} else {
// Won't work well if mesh changed, but kept for old behavior
new_volume->supported_facets.assign(old_volume->supported_facets);
new_volume->seam_facets.assign(old_volume->seam_facets);
new_volume->mmu_segmentation_facets.assign(old_volume->mmu_segmentation_facets);
new_volume->fuzzy_skin_facets.assign(old_volume->fuzzy_skin_facets);
}
std::swap(old_model_object->volumes[volume_idx], old_model_object->volumes.back());
old_model_object->delete_volume(old_model_object->volumes.size() - 1);
if (!sinking)
@@ -8904,6 +8914,16 @@ void Plater::priv::reload_from_disk()
new_volume->convert_from_imperial_units();
else if (old_volume->source.is_converted_from_meters)
new_volume->convert_from_meters();
// Remap paint
if (wxGetApp().app_config->get_bool("keep_painting")) {
auto saved_painting = old_volume->save_painting();
if (saved_painting) {
saved_painting->mesh.transform(Geometry::translation_transform(new_volume->mesh().get_init_shift()));
new_volume->restore_painting(saved_painting);
}
}
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();
@@ -15111,7 +15131,7 @@ TriangleMesh Plater::combine_mesh_fff(const ModelObject& mo, int instance_id, st
}
if (instance_id == -1) {
TriangleMesh vols_mesh(mesh);
TriangleMesh vols_mesh(std::move(mesh));
mesh = TriangleMesh();
for (const ModelInstance* i : mo.instances) {
TriangleMesh m = vols_mesh;

View File

@@ -969,7 +969,7 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too
if (m_bambu_cloud_checkbox) m_bambu_cloud_checkbox->Enable(!enabled);
}
#ifdef __WXMSW__
#ifdef __WXMSW__
if (param == "associate_3mf") {
bool pbool = app_config->get("associate_3mf") == "true" ? true : false;
if (pbool) {
@@ -1853,6 +1853,9 @@ void PreferencesDialog::create_items()
auto item_ams_blacklist = create_item_checkbox(_L("Skip AMS blacklist check"), "", "skip_ams_blacklist_check");
g_sizer->Add(item_ams_blacklist);
auto item_keep_painting = create_item_checkbox(_L("(Experimental) Keep painted feature after mesh change"), _L("Attempt to keep painted features (color/seam/support/fuzzy etc.) after changing the object mesh (such as cut/reload from disk/simplify/fix etc.)\nHighly experimental! Slow and may create artifact."), "keep_painting");
g_sizer->Add(item_keep_painting);
g_sizer->Add(create_item_title(_L("Storage")), 1, wxEXPAND);
auto item_allow_abnormal_storage = create_item_checkbox(_L("Allow Abnormal Storage"), _L("This allows the use of Storage that is marked as abnormal by the Printer.\nUse at your own risk, can cause issues!"), "allow_abnormal_storage");
g_sizer->Add(item_allow_abnormal_storage);