mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-29 13:52:07 +00:00
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:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user