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

@@ -4456,7 +4456,7 @@ int CLI::run(int argc, char **argv)
size_t num_objects = model.objects.size();
for (size_t i = 0; i < num_objects; ++ i) {
ModelObjectPtrs new_objects;
model.objects.front()->split(&new_objects);
model.objects.front()->split(&new_objects, false); // TODO: add cli option to enable this?
model.delete_object(size_t(0));
}
}

View File

@@ -63,7 +63,7 @@ static void add_cut_volume(TriangleMesh& mesh, ModelObject* object, const ModelV
vol->cut_info = src_volume->cut_info;
}
static void process_volume_cut( ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& cut_matrix,
static void process_volume_cut( const ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& cut_matrix,
ModelObjectCutAttributes attributes, TriangleMesh& upper_mesh, TriangleMesh& lower_mesh)
{
const auto volume_matrix = volume->get_matrix();
@@ -178,7 +178,7 @@ static void process_modifier_cut(ModelVolume* volume, const Transform3d& instanc
lower->add_volume(*volume);
}
static void process_solid_part_cut(ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& cut_matrix,
static void process_solid_part_cut(const ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& cut_matrix,
ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower)
{
// Perform cut
@@ -279,9 +279,22 @@ void Cut::post_process(ModelObject* upper, ModelObject* lower, ModelObjectPtrs&
}
void Cut::finalize(const ModelObjectPtrs& objects)
void Cut::finalize(const ModelObjectPtrs& objects, const std::vector<std::optional<TriangleSelector::SavedPainting>>& saved_paintings)
{
//clear model from temporarry objects
// Paint volumes
for (const auto& saved_painting : saved_paintings) {
if (saved_painting) {
for (const auto object : objects) {
for (const auto volume : object->volumes) {
if (volume->is_model_part() && !volume->is_cut_connector()) {
volume->restore_painting(saved_painting, true);
}
}
}
}
}
//clear model from temporary objects
m_model.clear_objects();
// add to model result objects
@@ -320,7 +333,17 @@ const ModelObjectPtrs& Cut::perform_with_plane()
const Transformation cut_transformation = Transformation(m_cut_matrix);
const Transform3d inverse_cut_matrix = cut_transformation.get_rotation_matrix().inverse() * translation_transform(-1. * cut_transformation.get_offset());
std::vector<std::optional<TriangleSelector::SavedPainting>> saved_paintings;
for (ModelVolume* volume : mo->volumes) {
// Save painting data before reset_extra_facets() discards it.
if (m_attributes.has(ModelObjectCutAttribute::KeepPaint)) {
saved_paintings.emplace_back(volume->save_painting());
if (saved_paintings.back()) {
// Transform mesh to cut space (same transform as process_volume_cut applies)
saved_paintings.back()->mesh.transform(instance_matrix * volume->get_matrix(), true);
}
}
volume->reset_extra_facets();
if (!volume->is_model_part()) {
@@ -376,9 +399,9 @@ const ModelObjectPtrs& Cut::perform_with_plane()
}
}
BOOST_LOG_TRIVIAL(trace) << "ModelObject::cut - end";
finalize(cut_object_ptrs, saved_paintings);
finalize(cut_object_ptrs);
BOOST_LOG_TRIVIAL(trace) << "ModelObject::cut - end";
return m_model.objects;
}
@@ -431,7 +454,7 @@ static void merge_solid_parts_inside_object(ModelObjectPtrs& objects)
}
const ModelObjectPtrs& Cut::perform_by_contour(std::vector<Part> parts, int dowels_count)
const ModelObjectPtrs& Cut::perform_by_contour(const ModelObject* src_object, std::vector<Part> parts, int dowels_count)
{
ModelObject* cut_mo = m_model.objects.front();
@@ -446,6 +469,19 @@ const ModelObjectPtrs& Cut::perform_by_contour(std::vector<Part> parts, int dowe
lower->name = lower->name + "_B";
}
// Save painting data so we later can remap it.
std::vector<std::optional<TriangleSelector::SavedPainting>> saved_paintings;
if (m_attributes.has(ModelObjectCutAttribute::KeepPaint)) {
const auto instance_matrix = src_object->instances[m_instance]->get_transformation().get_matrix_no_offset();
for (const auto volume : src_object->volumes) {
saved_paintings.emplace_back(volume->save_painting());
if (saved_paintings.back()) {
// Transform mesh to cut space (same transform as process_volume_cut applies)
saved_paintings.back()->mesh.transform(instance_matrix * volume->get_matrix(), true);
}
}
}
const size_t cut_parts_cnt = parts.size();
bool has_modifiers = false;
@@ -475,7 +511,7 @@ const ModelObjectPtrs& Cut::perform_by_contour(std::vector<Part> parts, int dowe
merge_solid_parts_inside_object(cut_object_ptrs);
// replace initial objects in model with cut object
finalize(cut_object_ptrs);
finalize(cut_object_ptrs, saved_paintings);
}
else if (volumes.size() > cut_parts_cnt) {
// Means that object is cut with connectors
@@ -506,7 +542,7 @@ const ModelObjectPtrs& Cut::perform_by_contour(std::vector<Part> parts, int dowe
merge_solid_parts_inside_object(cut_object_ptrs);
// replace initial objects in model with cut object
finalize(cut_object_ptrs);
finalize(cut_object_ptrs, saved_paintings);
// Add Dowel-connectors as separate objects to model
if (cut_connectors_obj.size() >= 3)
@@ -532,6 +568,20 @@ const ModelObjectPtrs& Cut::perform_with_groove(const Groove& groove, const Tran
upper->name = upper->name + "_A";
lower->name = lower->name + "_B";
}
// Save painting data so we later can remap it.
std::vector<std::optional<TriangleSelector::SavedPainting>> saved_paintings;
if (m_attributes.has(ModelObjectCutAttribute::KeepPaint)) {
const auto instance_matrix = cut_mo->instances[m_instance]->get_transformation().get_matrix_no_offset();
for (const auto volume : cut_mo->volumes) {
saved_paintings.emplace_back(volume->save_painting());
if (saved_paintings.back()) {
// Transform mesh to cut space (same transform as process_volume_cut applies)
saved_paintings.back()->mesh.transform(instance_matrix * volume->get_matrix(), true);
}
}
}
const double groove_half_depth = 0.5 * double(groove.depth);
Model tmp_model_for_cut = Model();
@@ -566,17 +616,17 @@ const ModelObjectPtrs& Cut::perform_with_groove(const Groove& groove, const Tran
};
// cut by upper plane
const Transform3d cut_matrix_upper = translation_transform(rotation_m * (groove_half_depth * Vec3d::UnitZ())) * m_cut_matrix;
{
const Transform3d cut_matrix_upper = translation_transform(rotation_m * (groove_half_depth * Vec3d::UnitZ())) * m_cut_matrix;
cut(tmp_object, cut_matrix_upper, ModelObjectCutAttribute::KeepLower, tmp_model_for_cut);
add_volumes_from_cut(upper, ModelObjectCutAttribute::KeepUpper, tmp_model_for_cut);
}
// cut by lower plane
const Transform3d cut_matrix_lower = translation_transform(rotation_m * (-groove_half_depth * Vec3d::UnitZ())) * m_cut_matrix;
{
const Transform3d cut_matrix_lower = translation_transform(rotation_m * (-groove_half_depth * Vec3d::UnitZ())) * m_cut_matrix;
cut(tmp_object, cut_matrix_lower, ModelObjectCutAttribute::KeepUpper, tmp_model_for_cut);
add_volumes_from_cut(lower, ModelObjectCutAttribute::KeepLower, tmp_model_for_cut);
}
@@ -658,7 +708,7 @@ const ModelObjectPtrs& Cut::perform_with_groove(const Groove& groove, const Tran
merge_solid_parts_inside_object(cut_object_ptrs);
}
finalize(cut_object_ptrs);
finalize(cut_object_ptrs, saved_paintings);
return m_model.objects;
}

View File

@@ -11,7 +11,7 @@ namespace Slic3r {
using ModelObjectPtrs = std::vector<ModelObject*>;
enum class ModelObjectCutAttribute : int { KeepUpper, KeepLower, KeepAsParts, FlipUpper, FlipLower, PlaceOnCutUpper, PlaceOnCutLower, CreateDowels, InvalidateCutInfo };
enum class ModelObjectCutAttribute : int { KeepUpper, KeepLower, KeepAsParts, FlipUpper, FlipLower, PlaceOnCutUpper, PlaceOnCutLower, CreateDowels, InvalidateCutInfo, KeepPaint };
using ModelObjectCutAttributes = enum_bitmask<ModelObjectCutAttribute>;
ENABLE_ENUM_BITMASK_OPERATORS(ModelObjectCutAttribute);
@@ -25,7 +25,7 @@ class Cut {
void post_process(ModelObject* object, ModelObjectPtrs& objects, bool keep, bool place_on_cut, bool flip);
void post_process(ModelObject* upper_object, ModelObject* lower_object, ModelObjectPtrs& objects);
void finalize(const ModelObjectPtrs& objects);
void finalize(const ModelObjectPtrs& objects, const std::vector<std::optional<TriangleSelector::SavedPainting>>& saved_paintings);
public:
@@ -56,7 +56,7 @@ public:
};
const ModelObjectPtrs& perform_with_plane();
const ModelObjectPtrs& perform_by_contour(std::vector<Part> parts, int dowels_count);
const ModelObjectPtrs& perform_by_contour(const ModelObject* src_object, std::vector<Part> parts, int dowels_count);
const ModelObjectPtrs& perform_with_groove(const Groove& groove, const Transform3d& rotation_m, bool keep_as_parts = false);
}; // namespace Cut

View File

@@ -298,7 +298,11 @@ bool directions_perpendicular(double angle1, double angle2, double max_diff = 0)
template<class T> bool contains(const std::vector<T> &vector, const Point &point);
template<typename T> T rad2deg(T angle) { return T(180.0) * angle / T(PI); }
double rad2deg_dir(double angle);
template<typename T> constexpr T deg2rad(const T angle) { return T(PI) * angle / T(180.0); }
template<typename T> constexpr T deg2rad(const T angle)
{
static_assert(std::is_floating_point<T>::value, "Why do you want to calculate angle in integer?");
return T(PI) * angle / T(180.0);
}
template<typename T> T angle_to_0_2PI(T angle)
{
static const T TWO_PI = T(2) * T(PI);

View File

@@ -1980,6 +1980,49 @@ void ModelVolume::reset_extra_facets()
this->fuzzy_skin_facets.reset();
}
std::optional<TriangleSelector::SavedPainting> ModelVolume::save_painting() const
{
if (is_any_painted() && is_model_part() && !mesh().empty()) {
TriangleSelector::SavedPainting sp;
sp.mesh = mesh();
sp.supported = supported_facets.get_data();
sp.seam = seam_facets.get_data();
sp.mmu = mmu_segmentation_facets.get_data();
sp.fuzzy = fuzzy_skin_facets.get_data();
return sp;
}
return {};
}
void ModelVolume::restore_painting(const std::optional<TriangleSelector::SavedPainting>& saved, const bool keep_existing_paint)
{
if (!keep_existing_paint) {
reset_extra_facets();
}
if (!saved) {
return;
}
auto remap_one = [&](const TriangleSelector::TriangleSplittingData& src_data,
FacetsAnnotation& target_facets) {
if (src_data.bitstream.empty())
return;
auto result =
TriangleSelector::remap_painting(saved->mesh.its, src_data, mesh().its, Geometry::translation_transform(mesh().get_init_shift()),
keep_existing_paint ?
std::optional<std::reference_wrapper<const TriangleSelector::TriangleSplittingData>>{std::ref(target_facets.get_data())} :
std::optional<std::reference_wrapper<const TriangleSelector::TriangleSplittingData>>{});
if (!result.bitstream.empty())
target_facets.set_data(std::move(result));
};
remap_one(saved->supported, supported_facets);
remap_one(saved->seam, seam_facets);
remap_one(saved->mmu, mmu_segmentation_facets);
remap_one(saved->fuzzy, fuzzy_skin_facets);
}
static void invalidate_translations(ModelObject* object, const ModelInstance* src_instance)
{
if (!object->origin_translation.isApprox(Vec3d::Zero()) && src_instance->get_offset().isApprox(Vec3d::Zero())) {
@@ -1993,13 +2036,14 @@ static void invalidate_translations(ModelObject* object, const ModelInstance* sr
}
}
void ModelObject::split(ModelObjectPtrs* new_objects)
void ModelObject::split(ModelObjectPtrs* new_objects, const bool remap_paint)
{
std::vector<TriangleMesh> all_meshes;
std::vector<Transform3d> all_transfos;
std::vector<std::pair<int, int>> volume_mesh_counts;
all_meshes.reserve(this->volumes.size() * 5);
bool is_multi_volume_object = (this->volumes.size() > 1);
std::optional<TriangleSelector::SavedPainting> saved_painting;
for (int volume_idx = 0; volume_idx < this->volumes.size(); volume_idx++) {
ModelVolume* volume = this->volumes[volume_idx];
@@ -2011,6 +2055,10 @@ void ModelObject::split(ModelObjectPtrs* new_objects)
volume->text_configuration.reset();
if (!is_multi_volume_object) {
if (remap_paint) {
// Save painting so we could restore them after the mesh split
saved_painting = volume->save_painting();
}
//BBS: not multi volume object, then split mesh.
std::vector<TriangleMesh> volume_meshes = volume->mesh().split();
int mesh_count = 0;
@@ -2078,10 +2126,19 @@ void ModelObject::split(ModelObjectPtrs* new_objects)
ModelVolume* new_vol = new_object->add_volume(*volume, std::move(mesh));
if (is_multi_volume_object) {
// BBS: volume geometry not changed, so we can keep the color paint facets
if (new_vol->mmu_segmentation_facets.timestamp() == volume->mmu_segmentation_facets.timestamp())
new_vol->mmu_segmentation_facets.reset(); // BBS: let next assign take effect
new_vol->mmu_segmentation_facets.assign(volume->mmu_segmentation_facets);
// BBS: volume geometry not changed, so we can keep the paint facets
#define COPY_FACETS(f) \
if (new_vol->f.timestamp() == volume->f.timestamp()) \
new_vol->f.reset(); /* BBS: let next assign take effect */ \
new_vol->f.assign(volume->f)
COPY_FACETS(supported_facets);
COPY_FACETS(seam_facets);
COPY_FACETS(mmu_segmentation_facets);
COPY_FACETS(fuzzy_skin_facets);
} else if (saved_painting) {
// Geometry changed, attempt to remap them to the new mesh
new_vol->restore_painting(saved_painting);
}
// BBS: clear volume's config, as we already set them into object
@@ -2682,7 +2739,7 @@ std::string ModelVolume::type_to_string(const ModelVolumeType t)
// Split this volume, append the result to the object owning this volume.
// Return the number of volumes created from this one.
// This is useful to assign different materials to different volumes of an object.
size_t ModelVolume::split(unsigned int max_extruders)
size_t ModelVolume::split(unsigned int max_extruders, bool remap_paint)
{
std::vector<TriangleMesh> meshes = this->mesh().split();
if (meshes.size() <= 1)
@@ -2692,6 +2749,9 @@ size_t ModelVolume::split(unsigned int max_extruders)
if (text_configuration.has_value())
text_configuration.reset();
std::optional<TriangleSelector::SavedPainting> saved_painting = remap_paint ? save_painting() :
std::optional<TriangleSelector::SavedPainting>{};
size_t idx = 0;
size_t ivolume = std::find(this->object->volumes.begin(), this->object->volumes.end(), this) - this->object->volumes.begin();
const std::string name = this->name;
@@ -2714,11 +2774,7 @@ size_t ModelVolume::split(unsigned int max_extruders)
this->source = ModelVolume::Source();
// BBS: reset facet annotations
this->mmu_segmentation_facets.reset();
this->exterior_facets.reset();
this->supported_facets.reset();
this->seam_facets.reset();
this->fuzzy_skin_facets.reset();
this->reset_extra_facets();
}
else
this->object->volumes.insert(this->object->volumes.begin() + (++ivolume), new ModelVolume(object, *this, std::move(mesh)));
@@ -2731,6 +2787,8 @@ size_t ModelVolume::split(unsigned int max_extruders)
this->object->volumes[ivolume]->config.set("extruder", this->extruder_id());
//this->object->volumes[ivolume]->config.set("extruder", auto_extruder_id(max_extruders, extruder_counter));
this->object->volumes[ivolume]->m_is_splittable = 0;
this->object->volumes[ivolume]->restore_painting(saved_painting);
++ idx;
}

View File

@@ -516,7 +516,7 @@ public:
void delete_connectors();
void clone_for_cut(ModelObject **obj);
void split(ModelObjectPtrs*new_objects);
void split(ModelObjectPtrs*new_objects, bool remap_paint);
void merge();
// BBS: Boolean opts - Musang King
@@ -731,6 +731,7 @@ public:
void assign(const FacetsAnnotation &rhs) { if (! this->timestamp_matches(rhs)) { m_data = rhs.m_data; this->copy_timestamp(rhs); } }
void assign(FacetsAnnotation &&rhs) { if (! this->timestamp_matches(rhs)) { m_data = std::move(rhs.m_data); this->copy_timestamp(rhs); } }
const TriangleSelector::TriangleSplittingData &get_data() const noexcept { return m_data; }
void set_data(TriangleSelector::TriangleSplittingData &&data) { m_data = std::move(data); this->touch(); }
bool set(const TriangleSelector& selector);
indexed_triangle_set get_facets(const ModelVolume& mv, EnforcerBlockerType type) const;
// BBS
@@ -877,13 +878,18 @@ public:
// List of mesh facets painted for fuzzy skin.
FacetsAnnotation fuzzy_skin_facets;
// Save painting data before reset_extra_facets() discards it.
// Used for replacing mesh without losing painting data.
// Only for model parts (not modifiers/connectors).
std::optional<TriangleSelector::SavedPainting> save_painting() const;
// Remap painting data from previous saved source to this mesh
void restore_painting(const std::optional<TriangleSelector::SavedPainting>& saved, bool keep_existing_paint = false);
// BBS: quick access for volume extruders, 1 based
mutable std::vector<int> mmuseg_extruders;
mutable Timestamp mmuseg_ts;
// List of exterior faces
FacetsAnnotation exterior_facets;
// Is set only when volume is Embossed Text type
// Contain information how to re-create volume
std::optional<TextConfiguration> text_configuration;
@@ -924,7 +930,7 @@ public:
// Split this volume, append the result to the object owning this volume.
// Return the number of volumes created from this one.
// This is useful to assign different materials to different volumes of an object.
size_t split(unsigned int max_extruders);
size_t split(unsigned int max_extruders, bool remap_paint);
void translate(double x, double y, double z) { translate(Vec3d(x, y, z)); }
void translate(const Vec3d& displacement);
void scale(const Vec3d& scaling_factors);
@@ -1007,6 +1013,7 @@ public:
bool is_seam_painted() const { return !this->seam_facets.empty(); }
bool is_mm_painted() const { return !this->mmu_segmentation_facets.empty(); }
bool is_fuzzy_skin_painted() const { return !this->fuzzy_skin_facets.empty(); }
bool is_any_painted() const { return is_fdm_support_painted() || is_seam_painted() || is_mm_painted() || is_fuzzy_skin_painted(); }
// Orca: Implement prusa's filament shrink compensation approach
// Returns 0-based indices of extruders painted by multi-material painting gizmo.

View File

@@ -2348,7 +2348,7 @@ void cut_mesh(const indexed_triangle_set& mesh, float z, indexed_triangle_set* u
IntersectionLines upper_lines, lower_lines;
std::vector<int> upper_slice_vertices, lower_slice_vertices;
std::vector<Vec3i32> facets_edge_ids = its_face_edge_ids(mesh);
std::map<int, Vec3f> section_vertices_map;
std::map<int, Vec3f> section_vertices_map; // vertices on the cut plane
for (int facet_idx = 0; facet_idx < int(mesh.indices.size()); ++ facet_idx) {
const stl_triangle_vertex_indices &facet = mesh.indices[facet_idx];

View File

@@ -1,9 +1,11 @@
#include "TriangleSelector.hpp"
#include "Model.hpp"
#include "AABBTreeIndirect.hpp"
#include <boost/container/small_vector.hpp>
#include <boost/log/trivial.hpp>
#include <cstddef>
#include <functional>
#include <tbb/parallel_for.h>
#ifndef NDEBUG
@@ -168,24 +170,69 @@ void TriangleSelector::Triangle::set_division(int sides_to_split, int special_si
this->special_side_idx = char(special_side_idx);
}
inline bool is_point_inside_triangle(const Vec3f &pt, const Vec3f &p1, const Vec3f &p2, const Vec3f &p3)
// Pre-computed barycentric resolver
// Real-time collision detection, Ericson, Chapter 3.4
// Cache inspired by Don Hatch at https://gamedev.stackexchange.com/questions/23743/whats-the-most-efficient-way-to-find-barycentric-coordinates/23745#comment390123_23745
struct Barycentric
{
// Real-time collision detection, Ericson, Chapter 3.4
auto barycentric = [&pt, &p1, &p2, &p3]() -> Vec3f {
std::array<Vec3f, 3> v = {p2 - p1, p3 - p1, pt - p1};
float d00 = v[0].dot(v[0]);
float d01 = v[0].dot(v[1]);
float d11 = v[1].dot(v[1]);
float d20 = v[2].dot(v[0]);
float d21 = v[2].dot(v[1]);
float denom = d00 * d11 - d01 * d01;
public:
Barycentric(const Vec3f& a, const Vec3f& b, const Vec3f& c)
:a_(a)
{
// Pre-compute denominator
const Vec3f v0 = b - a;
const Vec3f v1 = c - a;
const float d00 = v0.dot(v0);
const float d01 = v0.dot(v1);
const float d11 = v1.dot(v1);
const float inv_denom_ = 1.0f / (d00 * d11 - d01 * d01);
x1_ = (d11 * v0 - d01 * v1) * inv_denom_;
x2_ = (d00 * v1 - d01 * v0) * inv_denom_;
}
Vec3f barycentric_cords(1.f, (d11 * d20 - d01 * d21) / denom, (d00 * d21 - d01 * d20) / denom);
barycentric_cords.x() = barycentric_cords.x() - barycentric_cords.y() - barycentric_cords.z();
return barycentric_cords;
};
Vec3f calc(const Vec3f& p) const
{
const Vec3f v2 = p - a_;
const float v = v2.dot(x1_);
const float w = v2.dot(x2_);
const float u = 1.f - v - w;
Vec3f barycentric_cords = barycentric();
return {u, v, w};
}
bool is_point_inside_triangle(const Vec3f& p) const
{
const Vec3f barycentric_cords = calc(p);
return std::all_of(begin(barycentric_cords), end(barycentric_cords), [](float cord) { return 0.f <= cord && cord <= 1.0; });
}
static Vec3f calc(const Vec3f& pt, const Vec3f& p1, const Vec3f& p2, const Vec3f& p3)
{
const std::array<Vec3f, 3> vec = {p2 - p1, p3 - p1, pt - p1};
const float d00 = vec[0].dot(vec[0]);
const float d01 = vec[0].dot(vec[1]);
const float d11 = vec[1].dot(vec[1]);
const float d20 = vec[2].dot(vec[0]);
const float d21 = vec[2].dot(vec[1]);
const float denom = d00 * d11 - d01 * d01;
const float v = (d11 * d20 - d01 * d21) / denom;
const float w = (d00 * d21 - d01 * d20) / denom;
const float u = 1.f - v - w;
return {u, v, w};
}
private:
Vec3f a_;
Vec3f x1_;
Vec3f x2_;
};
static bool is_point_inside_triangle(const Vec3f &pt, const Vec3f &p1, const Vec3f &p2, const Vec3f &p3)
{
Vec3f barycentric_cords = Barycentric::calc(pt, p1, p2, p3);
return std::all_of(begin(barycentric_cords), end(barycentric_cords), [](float cord) { return 0.f <= cord && cord <= 1.0; });
}
@@ -233,7 +280,7 @@ int TriangleSelector::select_unsplit_triangle(const Vec3f &hit, int facet_idx) c
return this->select_unsplit_triangle(hit, facet_idx, neighbors);
}
void TriangleSelector::select_patch(int facet_start, std::unique_ptr<Cursor> &&cursor, EnforcerBlockerType new_state, const Transform3d& trafo_no_translate, bool triangle_splitting, float highlight_by_angle_deg)
void TriangleSelector::select_patch(int facet_start, std::unique_ptr<Cursor> &&cursor, EnforcerBlockerType new_state, const Transform3d& trafo_no_translate, bool triangle_splitting, float highlight_by_angle_deg, const bool select_partially)
{
assert(facet_start < m_orig_size_indices);
@@ -296,7 +343,7 @@ void TriangleSelector::select_patch(int facet_start, std::unique_ptr<Cursor> &&c
Matrix3f normal_matrix = static_cast<Matrix3f>(trafo_no_translate.matrix().block(0, 0, 3, 3).inverse().transpose().cast<float>());
float world_normal_z = (normal_matrix* facet_normal).normalized().z();
if (!visited[facet] && (highlight_by_angle_deg == 0.f || world_normal_z < highlight_angle_limit)) {
if (select_triangle(facet, new_state, triangle_splitting)) {
if (select_triangle(facet, new_state, triangle_splitting, select_partially)) {
// add neighboring facets to list to be processed later
for (int neighbor_idx : m_neighbors[facet])
if (neighbor_idx >= 0 && m_cursor->is_facet_visible(neighbor_idx, m_face_normals))
@@ -552,7 +599,7 @@ void TriangleSelector::bucket_fill_select_triangles(const Vec3f& hit, int facet_
// This is done by an actual recursive call. Returns false if the triangle is
// outside the cursor.
// Called by select_patch() and by itself.
bool TriangleSelector::select_triangle(int facet_idx, EnforcerBlockerType type, bool triangle_splitting)
bool TriangleSelector::select_triangle(int facet_idx, EnforcerBlockerType type, bool triangle_splitting, bool select_partially)
{
assert(facet_idx < int(m_triangles.size()));
@@ -562,7 +609,7 @@ bool TriangleSelector::select_triangle(int facet_idx, EnforcerBlockerType type,
Vec3i32 neighbors = m_neighbors[facet_idx];
assert(this->verify_triangle_neighbors(m_triangles[facet_idx], neighbors));
if (! select_triangle_recursive(facet_idx, neighbors, type, triangle_splitting))
if (! select_triangle_recursive(facet_idx, neighbors, type, triangle_splitting, select_partially))
return false;
// In case that all children are leafs and have the same state now,
@@ -902,7 +949,7 @@ Vec3i32 TriangleSelector::child_neighbors_propagated(const Triangle &tr, const V
return out;
}
bool TriangleSelector::select_triangle_recursive(int facet_idx, const Vec3i32 &neighbors, EnforcerBlockerType type, bool triangle_splitting)
bool TriangleSelector::select_triangle_recursive(int facet_idx, const Vec3i32 &neighbors, EnforcerBlockerType type, bool triangle_splitting, bool select_partially)
{
assert(facet_idx < int(m_triangles.size()));
@@ -935,8 +982,10 @@ bool TriangleSelector::select_triangle_recursive(int facet_idx, const Vec3i32 &n
if (triangle_splitting)
split_triangle(facet_idx, neighbors);
else if (!m_triangles[facet_idx].is_split())
if ((!triangle_splitting || select_partially) && !m_triangles[facet_idx].is_split()) {
m_triangles[facet_idx].set_state(type);
return true;
}
tr = &m_triangles[facet_idx]; // might have been invalidated by split_triangle().
int num_of_children = tr->number_of_split_sides() + 1;
@@ -946,7 +995,7 @@ bool TriangleSelector::select_triangle_recursive(int facet_idx, const Vec3i32 &n
assert(tr->children[i] < int(m_triangles.size()));
// Recursion, deep first search over the children of this triangle.
// All children of this triangle were created by splitting a single source triangle of the original mesh.
select_triangle_recursive(tr->children[i], this->child_neighbors(*tr, neighbors, i), type, triangle_splitting);
select_triangle_recursive(tr->children[i], this->child_neighbors(*tr, neighbors, i), type, triangle_splitting, select_partially);
tr = &m_triangles[facet_idx]; // might have been invalidated
}
}
@@ -2208,4 +2257,271 @@ std::vector<EnforcerBlockerType> TriangleSelector::extract_used_facet_states(con
return out;
}
static bool segments_intersect_proj(const Vec3f& p1, const Vec3f& p2, const Vec3f& p3, const Vec3f& p4, const std::pair<int, int>& proj)
{
auto cross2d = [](float ax, float ay, float bx, float by) -> float { return ax * by - ay * bx; };
const auto [u_axis, v_axis] = proj;
const auto u1 = p1(u_axis), v1 = p1(v_axis);
const auto u2 = p2(u_axis), v2 = p2(v_axis);
const auto u3 = p3(u_axis), v3 = p3(v_axis);
const auto u4 = p4(u_axis), v4 = p4(v_axis);
const float ru = u2 - u1, rv = v2 - v1;
const float su = u4 - u3, sv = v4 - v3;
const float denom = cross2d(ru, rv, su, sv);
if (std::abs(denom) < 1e-10f)
return false;
const float dpu = u3 - u1, dpv = v3 - v1;
const float t1 = cross2d(dpu, dpv, su, sv) / denom;
const float t2 = cross2d(dpu, dpv, ru, rv) / denom;
return 0.f <= t1 && t1 <= 1.0f && 0.f <= t2 && t2 <= 1.f;
};
class TriangleCursor : public TriangleSelector::SinglePointCursor
{
public:
TriangleCursor() = delete;
explicit TriangleCursor(const Vec3f& center_,
const Vec3f& source_,
float radius_world,
const Transform3d& trafo_,
const TriangleSelector::ClippingPlane& clipping_plane_,
const std::array<const Vec3f, 3>& vertices)
: TriangleSelector::SinglePointCursor(center_, source_, radius_world, trafo_, clipping_plane_)
, barycentric_(vertices[0], vertices[1], vertices[2])
{}
~TriangleCursor() override = default;
static std::unique_ptr<Cursor> build_cursor(const TriangleSelector& source_selector, const TriangleSelector::Triangle& tri)
{
const Vec3f& pv0 = source_selector.m_vertices[tri.verts_idxs[0]].v;
const Vec3f& pv1 = source_selector.m_vertices[tri.verts_idxs[1]].v;
const Vec3f& pv2 = source_selector.m_vertices[tri.verts_idxs[2]].v;
// Calculate the centroid of the triangle
const Vec3f center = (pv0 + pv1 + pv2) / 3.f;
// Calculate the norm of the plane
const Vec3f& norm = source_selector.m_face_normals[tri.source_triangle];
// Calculate the min distance from the centroid to every edges
const float radius = std::max(std::min(std::min(point_to_line_dist(center, pv0, pv1), point_to_line_dist(center, pv0, pv2)),
point_to_line_dist(center, pv1, pv2)), 0.1f);
return std::make_unique<TriangleCursor>(center, center + norm, radius, Transform3d::Identity(), TriangleSelector::ClippingPlane(),
std::array<const Vec3f, 3>{pv0, pv1, pv2});
}
bool is_mesh_point_inside(const Vec3f& point) const override
{
return barycentric_.is_point_inside_triangle(point);
}
bool is_edge_inside_cursor(const TriangleSelector::Triangle& tr, const std::vector<TriangleSelector::Vertex>& vertices) const override
{
std::array<Vec3f, 3> pts_proj; // Projected point onto the plane
std::array<float, 3> pts_dist; // Distance to the plane, positive means above, negative means below the plane
for (int i = 0; i < 3; ++i) {
Vec3f p = vertices[tr.verts_idxs[i]].v;
if (!this->uniform_scaling)
p = this->trafo * p;
const Vec3f diff = p - center;
pts_dist[i] = diff.dot(dir);
pts_proj[i] = p - pts_dist[i] * dir;
}
for (int side = 0; side < 3; ++side) {
const int idx_a = side;
const int idx_b = side < 2 ? side + 1 : 0;
// If both ends at the same side and farther than tolerance, skip
const float dist_a = pts_dist[idx_a];
const float dist_b = pts_dist[idx_b];
if ((dist_a > tolerance && dist_b > tolerance) || (dist_a < -tolerance && dist_b < -tolerance)) {
continue;
}
// Find the projected line segment which has distance to the plane within the tolerance
Vec3f pt_a = pts_proj[idx_a];
Vec3f pt_b = pts_proj[idx_b];
if (std::abs(dist_a) > tolerance) {
pt_a = (tolerance - dist_b) / (dist_a - dist_b) * (pts_proj[idx_a] - pts_proj[idx_b]) + pts_proj[idx_b];
}
if (std::abs(dist_b) > tolerance) {
pt_b = (tolerance - dist_a) / (dist_b - dist_a) * (pts_proj[idx_b] - pts_proj[idx_a]) + pts_proj[idx_a];
}
// If any projected end is inside the triangle, then is in
if (barycentric_.is_point_inside_triangle(pt_a) ||
barycentric_.is_point_inside_triangle(pt_b)) {
return true;
}
// Otherwise see if the segment (pt_a, pt_b) intersects the triangle
{
const Vec3f uvw_a = barycentric_.calc(pt_a);
const Vec3f uvw_b = barycentric_.calc(pt_b);
const Vec3f uvw_0 {1.f,0.f,0.f};
const Vec3f uvw_1 {0.f,1.f,0.f};
const Vec3f uvw_2 {0.f,0.f,1.f};
constexpr std::pair<int, int> proj{0, 1};
if (segments_intersect_proj(uvw_a, uvw_b, uvw_0, uvw_1, proj)||
segments_intersect_proj(uvw_a, uvw_b, uvw_0, uvw_2, proj)||
segments_intersect_proj(uvw_a, uvw_b, uvw_1, uvw_2, proj)) {
return true;
}
}
}
return false;
}
bool is_facet_visible(int facet_idx, const std::vector<Vec3f>& face_normals) const override
{
const Vec3f& n = face_normals[facet_idx];
return check_normal(n, this->dir);
}
static bool check_normal(const Vec3f& facet_norm, const Vec3f& camera_dir)
{
const float a = -(facet_norm.dot(camera_dir));
return std::clamp(a, 0.f, 1.f) >= facet_angle_limit;
}
static constexpr float tolerance = 0.01f;
private:
const Barycentric barycentric_;
static const double facet_angle_limit;
static float point_to_line_dist(const Vec3f& p, const Vec3f& a, const Vec3f& b)
{
const Eigen::ParametrizedLine<float, 3> line = Eigen::ParametrizedLine<float, 3>::Through(a, b);
return line.distance(p);
}
};
const double TriangleCursor::facet_angle_limit = cos(Geometry::deg2rad(5.0));
// Remap painting data from source mesh to target mesh using spatial mapping.
TriangleSelector::TriangleSplittingData TriangleSelector::remap_painting(
const indexed_triangle_set& source_its,
const TriangleSplittingData& source_painting,
const indexed_triangle_set& target_its,
const Transform3d& target_transform,
const std::optional<std::reference_wrapper<const TriangleSplittingData>>& existing_painting)
{
TriangleSelector::TriangleSplittingData result;
if (source_painting.bitstream.empty())
return result;
// 1. Deserialize source painting
TriangleMesh source_mesh(source_its);
TriangleSelector source_selector(source_mesh);
source_selector.deserialize(source_painting, false);
// 2. Extract painted geometry
std::vector<std::reference_wrapper<const Triangle>> painted_triangles;
painted_triangles.reserve(source_selector.m_triangles.size());
for (const Triangle& tr : source_selector.m_triangles) {
if (tr.valid() && !tr.is_split() && tr.get_state() != EnforcerBlockerType::NONE) {
painted_triangles.push_back(std::ref(tr));
}
}
if (painted_triangles.empty())
return result;
// 3. Build AABB tree of target mesh so we could find nearest face quickly
TriangleMesh target_mesh(target_its);
target_mesh.transform(target_transform);
AABBTreeIndirect::Tree3f target_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(target_mesh.its.vertices, target_mesh.its.indices);
// Helper: check overlap between a paint triangle and a target triangle.
// Uses 3D barycentric point-in-triangle tests and dominant-axis 2D projection
// for edge-edge intersection to handle all triangle orientations correctly.
auto check_overlap = [&](const Vec3f& pa, const Vec3f& pb, const Vec3f& pc,
const Vec3f& ta, const Vec3f& tb, const Vec3f& tc) -> bool {
// Check if any target vertex is inside the paint triangle
if (is_point_inside_triangle(ta, pa, pb, pc)) return true;
if (is_point_inside_triangle(tb, pa, pb, pc)) return true;
if (is_point_inside_triangle(tc, pa, pb, pc)) return true;
// Check if any paint vertex is inside the target triangle
if (is_point_inside_triangle(pa, ta, tb, tc)) return true;
if (is_point_inside_triangle(pb, ta, tb, tc)) return true;
if (is_point_inside_triangle(pc, ta, tb, tc)) return true;
// Check edge-edge intersections using dominant-axis 2D projection.
// Project onto the plane (XY, XZ, or YZ) where the triangles have the
// most area, avoiding degenerate projections for vertical/angled surfaces.
Vec3f n1 = (pb - pa).cross(pc - pa);
Vec3f n2 = (tb - ta).cross(tc - ta);
Vec3f n_abs = (n1.cwiseAbs() + n2.cwiseAbs());
int axis = (n_abs.x() >= n_abs.y() && n_abs.x() >= n_abs.z()) ? 0
: (n_abs.y() >= n_abs.z()) ? 1 : 2;
const int u_axis = (axis + 1) % 3;
const int v_axis = (axis + 2) % 3;
const std::pair<int, int> proj{u_axis, v_axis};
if (segments_intersect_proj(pa, pb, ta, tb, proj)) return true;
if (segments_intersect_proj(pa, pb, tb, tc, proj)) return true;
if (segments_intersect_proj(pa, pb, tc, ta, proj)) return true;
if (segments_intersect_proj(pb, pc, ta, tb, proj)) return true;
if (segments_intersect_proj(pb, pc, tb, tc, proj)) return true;
if (segments_intersect_proj(pb, pc, tc, ta, proj)) return true;
if (segments_intersect_proj(pc, pa, ta, tb, proj)) return true;
if (segments_intersect_proj(pc, pa, tb, tc, proj)) return true;
if (segments_intersect_proj(pc, pa, tc, ta, proj)) return true;
return false;
};
// 4. For each painted face, we find the nearest target face, and apply the TriangleCursor to paint it
TriangleSelector target_selector(target_mesh);
if (existing_painting) {
// Restore existing painting first, if given
target_selector.deserialize(existing_painting->get(), false);
}
for (auto tri_ref : painted_triangles) {
const Triangle& tri = tri_ref.get();
const Vec3f& pv0 = source_selector.m_vertices[tri.verts_idxs[0]].v;
const Vec3f& pv1 = source_selector.m_vertices[tri.verts_idxs[1]].v;
const Vec3f& pv2 = source_selector.m_vertices[tri.verts_idxs[2]].v;
// Find ALL target faces whose bounding boxes overlap with the paint
// triangle's bounding box, not just the nearest one.
Eigen::AlignedBox3f pt_bbox;
pt_bbox.extend(pv0);
pt_bbox.extend(pv1);
pt_bbox.extend(pv2);
pt_bbox.min() -= Eigen::Vector3f::Constant(TriangleCursor::tolerance);
pt_bbox.max() += Eigen::Vector3f::Constant(TriangleCursor::tolerance);
AABBTreeIndirect::traverse(target_tree, AABBTreeIndirect::intersecting(pt_bbox), [&](const AABBTreeIndirect::Tree3f::Node& node) -> bool {
size_t face_idx = node.idx;
if (face_idx >= target_mesh.its.indices.size())
return true;
const Vec3f& norm_a = source_selector.m_face_normals[tri.source_triangle];
const Vec3f& norm_b = target_selector.m_face_normals[face_idx];
const Vec3i32& face = target_mesh.its.indices[face_idx];
const Vec3f& ta = target_mesh.its.vertices[face(0)];
const Vec3f& tb = target_mesh.its.vertices[face(1)];
const Vec3f& tc = target_mesh.its.vertices[face(2)];
if (TriangleCursor::check_normal(norm_b, -norm_a) && check_overlap(pv0, pv1, pv2, ta, tb, tc)) {
// Paint this face
target_selector.select_patch(face_idx, TriangleCursor::build_cursor(source_selector, tri), tri.get_state(),
Transform3d::Identity(), true, 0.f, true);
}
return true; // continue traversal
});
}
return target_selector.serialize();
}
} // namespace Slic3r

View File

@@ -308,7 +308,8 @@ public:
EnforcerBlockerType new_state, // enforcer or blocker?
const Transform3d &trafo_no_translate, // matrix to get from mesh to world without translation
bool triangle_splitting, // If triangles will be split base on the cursor or not
float highlight_by_angle_deg = 0.f); // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees.
float highlight_by_angle_deg = 0.f, // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees.
bool select_partially = false); // Select a triangle if it's partially in the cursor but too small to be subdivided
void seed_fill_select_triangles(const Vec3f &hit, // point where to start
int facet_start, // facet of the original mesh (unsplit) that the hit point belongs to
@@ -372,6 +373,25 @@ public:
// The operation may merge split triangles if they are being assigned the same color.
void seed_fill_apply_on_triangles(EnforcerBlockerType new_state);
// Saved painting data for remapping after mesh change.
struct SavedPainting {
TriangleMesh mesh; // Original mesh
TriangleSplittingData supported;
TriangleSplittingData seam;
TriangleSplittingData mmu;
TriangleSplittingData fuzzy;
};
// Remap painting data from source mesh to target mesh using spatial mapping.
// `target_transform` should transform the target mesh into source's coordinate space.
// If `existing_painting` is present, the result will be a combine of `existing_painting` and remapped `source_painting`.
static TriangleSplittingData remap_painting(
const indexed_triangle_set& source_its,
const TriangleSplittingData& source_painting,
const indexed_triangle_set& target_its,
const Transform3d& target_transform,
const std::optional<std::reference_wrapper<const TriangleSplittingData>>& existing_painting);
protected:
// Triangle and info about how it's split.
class Triangle {
@@ -477,8 +497,8 @@ protected:
// Private functions:
private:
bool select_triangle(int facet_idx, EnforcerBlockerType type, bool triangle_splitting);
bool select_triangle_recursive(int facet_idx, const Vec3i32 &neighbors, EnforcerBlockerType type, bool triangle_splitting);
bool select_triangle(int facet_idx, EnforcerBlockerType type, bool triangle_splitting, bool select_partially);
bool select_triangle_recursive(int facet_idx, const Vec3i32 &neighbors, EnforcerBlockerType type, bool triangle_splitting, bool select_partially);
void undivide_triangle(int facet_idx);
void split_triangle(int facet_idx, const Vec3i32 &neighbors);
void remove_useless_children(int facet_idx); // No hidden meaning. Triangles are meant.
@@ -521,11 +541,10 @@ private:
int m_free_triangles_head { -1 };
int m_free_vertices_head { -1 };
friend class TriangleCursor;
};
} // namespace Slic3r
#endif // libslic3r_TriangleSelector_hpp_

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

View File

@@ -13,6 +13,7 @@
#include "libslic3r/MeshBoolean.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/format.hpp"
#include "libslic3r/Thread.hpp"
#include "../GUI/I18N.hpp"
// Orca: This file provides utilities for repairing 3D model meshes using the CGAL library, handling mesh splitting, merging, and boolean operations.
@@ -66,7 +67,7 @@ public:
// Orca: Main function to repair model objects using CGAL, with progress dialog and cancellation support.
// Returns false if fixing was canceled. fix_result contains error message if failed.
bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::ProgressDialog &progress_dialog, const wxString &msg_header, std::string &fix_result)
bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::ProgressDialog &progress_dialog, const wxString &msg_header, std::string &fix_result, bool keep_painting)
{
// Orca: Synchronization primitives for progress updates between worker thread and GUI.
std::mutex mtx;
@@ -94,8 +95,10 @@ bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::Pro
};
// Orca: Worker thread that performs the actual model repair operations.
auto worker_thread = std::thread([&model_object, volume_idx, &ivolume, on_progress, &success, &canceled, &finished, &fix_result]() {
auto worker_thread = std::thread([&model_object, volume_idx, &ivolume, on_progress, &success, &canceled, &finished, &fix_result, keep_painting]() {
try {
set_current_thread_name("cgal_fix_model");
size_t start_volume = volume_idx == -1 ? 0 : size_t(volume_idx);
size_t end_volume = volume_idx == -1 ? std::numeric_limits<size_t>::max() : size_t(volume_idx);
@@ -112,7 +115,7 @@ bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::Pro
// Orca: Split splittable volumes into parts for individual processing.
size_t parts_count = 1;
if (volume->is_splittable()) {
parts_count = volume->split(1);
parts_count = volume->split(1, keep_painting);
if (parts_count > 1) {
const std::string msg = Slic3r::format(L("Split into %1% parts"), parts_count);
on_progress(msg.c_str(), 10);
@@ -150,6 +153,12 @@ bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::Pro
ModelVolume *part_volume = model_object.volumes[part_idx];
TriangleMesh mesh = part_volume->mesh();
if (its_num_open_edges(mesh.its) != 0) {
// Save painting for later remap
const std::optional<TriangleSelector::SavedPainting> saved_painting = keep_painting ?
part_volume->save_painting() :
std::optional<TriangleSelector::SavedPainting>{};
std::string error;
if (!MeshBoolean::cgal::repair(mesh, nullptr, &error))
throw Slic3r::RuntimeError(error.empty() ? L("Repair failed") : error.c_str());
@@ -158,6 +167,9 @@ bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::Pro
part_volume->calculate_convex_hull();
part_volume->invalidate_convex_hull_2d();
part_volume->set_new_unique_id();
// Remap paint back
part_volume->restore_painting(saved_painting);
}
}

View File

@@ -11,7 +11,7 @@ class ModelObject;
class Print;
// Return false if fixing was canceled. fix_result is empty on success.
extern bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::ProgressDialog &progress_dlg, const wxString &msg_header, std::string &fix_result);
extern bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::ProgressDialog &progress_dlg, const wxString &msg_header, std::string &fix_result, bool keep_painting);
} // namespace Slic3r