From 07173a8de41a4974d0e67052a4b5b1b0068b6923 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 28 Apr 2026 22:36:30 +0800 Subject: [PATCH 01/35] Remove unused facets --- src/libslic3r/Model.cpp | 6 +----- src/libslic3r/Model.hpp | 3 --- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index e3b2ada837..ef5ef6dbe6 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -2714,11 +2714,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))); diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 5974ca4cd6..3fd66f5f69 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -881,9 +881,6 @@ public: mutable std::vector 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 text_configuration; From ded2e72dcc47bc7cb38f1a50ac1605e1179f77db Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 28 Apr 2026 22:47:41 +0800 Subject: [PATCH 02/35] Refine code --- src/libslic3r/CutUtils.cpp | 8 ++++---- src/libslic3r/TriangleMeshSlicer.cpp | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libslic3r/CutUtils.cpp b/src/libslic3r/CutUtils.cpp index 8611ec302d..eff267588c 100644 --- a/src/libslic3r/CutUtils.cpp +++ b/src/libslic3r/CutUtils.cpp @@ -566,17 +566,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); } diff --git a/src/libslic3r/TriangleMeshSlicer.cpp b/src/libslic3r/TriangleMeshSlicer.cpp index 150c456b8b..2c1c0da23f 100644 --- a/src/libslic3r/TriangleMeshSlicer.cpp +++ b/src/libslic3r/TriangleMeshSlicer.cpp @@ -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 upper_slice_vertices, lower_slice_vertices; std::vector facets_edge_ids = its_face_edge_ids(mesh); - std::map section_vertices_map; + std::map 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]; From 24458ba5a20048394175ba9adb74f5582cfa54cf Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Mon, 4 May 2026 20:32:39 +0800 Subject: [PATCH 03/35] Remapping the paintings after cut --- src/libslic3r/CutUtils.cpp | 68 ++++++- src/libslic3r/Model.hpp | 1 + src/libslic3r/TriangleSelector.cpp | 292 +++++++++++++++++++++++++++-- src/libslic3r/TriangleSelector.hpp | 12 +- 4 files changed, 349 insertions(+), 24 deletions(-) diff --git a/src/libslic3r/CutUtils.cpp b/src/libslic3r/CutUtils.cpp index eff267588c..7979dc4095 100644 --- a/src/libslic3r/CutUtils.cpp +++ b/src/libslic3r/CutUtils.cpp @@ -8,11 +8,39 @@ #include "ObjectID.hpp" #include +#include namespace Slic3r { using namespace Geometry; +// Saved painting data for remapping after mesh cutting. +struct SavedPainting { + indexed_triangle_set its; // Original mesh transformed to cut space + TriangleSelector::TriangleSplittingData supported; + TriangleSelector::TriangleSplittingData seam; + TriangleSelector::TriangleSplittingData mmu; + TriangleSelector::TriangleSplittingData fuzzy; +}; + +// Remap painting data from saved source to a cut result mesh, and set on a volume. +static void remap_and_set_painting(ModelVolume* vol, const indexed_triangle_set& cut_its, + const SavedPainting& saved) +{ + auto remap_one = [&](const TriangleSelector::TriangleSplittingData& src_data, + FacetsAnnotation& target_facets) { + if (src_data.bitstream.empty()) + return; + auto result = TriangleSelector::remap_painting(saved.its, src_data, cut_its); + if (!result.bitstream.empty()) + target_facets.set_data(result); + }; + remap_one(saved.supported, vol->supported_facets); + remap_one(saved.seam, vol->seam_facets); + remap_one(saved.mmu, vol->mmu_segmentation_facets); + remap_one(saved.fuzzy, vol->fuzzy_skin_facets); +} + static void apply_tolerance(ModelVolume* vol) { ModelVolume::CutInfo& cut_info = vol->cut_info; @@ -179,7 +207,8 @@ static void process_modifier_cut(ModelVolume* volume, const Transform3d& instanc } static void process_solid_part_cut(ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& cut_matrix, - ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower) + ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower, + const SavedPainting* saved_painting = nullptr) { // Perform cut TriangleMesh upper_mesh, lower_mesh; @@ -189,18 +218,28 @@ static void process_solid_part_cut(ModelVolume* volume, const Transform3d& insta if (attributes.has(ModelObjectCutAttribute::KeepAsParts)) { add_cut_volume(upper_mesh, upper, volume, cut_matrix, "_A"); + if (saved_painting && !upper_mesh.empty()) + remap_and_set_painting(upper->volumes.back(), upper_mesh.its, *saved_painting); if (!lower_mesh.empty()) { add_cut_volume(lower_mesh, upper, volume, cut_matrix, "_B"); + if (saved_painting) + remap_and_set_painting(upper->volumes.back(), lower_mesh.its, *saved_painting); upper->volumes.back()->cut_info.is_from_upper = false; } return; } - if (attributes.has(ModelObjectCutAttribute::KeepUpper)) + if (attributes.has(ModelObjectCutAttribute::KeepUpper)) { add_cut_volume(upper_mesh, upper, volume, cut_matrix); + if (saved_painting && !upper_mesh.empty()) + remap_and_set_painting(upper->volumes.back(), upper_mesh.its, *saved_painting); + } - if (attributes.has(ModelObjectCutAttribute::KeepLower) && !lower_mesh.empty()) + if (attributes.has(ModelObjectCutAttribute::KeepLower) && !lower_mesh.empty()) { add_cut_volume(lower_mesh, lower, volume, cut_matrix); + if (saved_painting) + remap_and_set_painting(lower->volumes.back(), lower_mesh.its, *saved_painting); + } } static void reset_instance_transformation(ModelObject* object, size_t src_instance_idx, @@ -321,6 +360,26 @@ const ModelObjectPtrs& Cut::perform_with_plane() const Transform3d inverse_cut_matrix = cut_transformation.get_rotation_matrix().inverse() * translation_transform(-1. * cut_transformation.get_offset()); for (ModelVolume* volume : mo->volumes) { + // Save painting data before reset_extra_facets() discards it. + // Only for model parts that will be cut (not modifiers/connectors). + std::optional saved_painting; + if (volume->is_model_part() && !volume->mesh().empty()) { + SavedPainting sp; + // Get mesh in cut space (same transform as process_volume_cut applies) + TriangleMesh mesh(volume->mesh()); + const auto volume_matrix = volume->get_matrix(); + mesh.transform(inverse_cut_matrix * instance_matrix * volume_matrix, true); + mesh.transform(m_cut_matrix); + sp.its = std::move(mesh.its); + sp.supported = volume->supported_facets.get_data(); + sp.seam = volume->seam_facets.get_data(); + sp.mmu = volume->mmu_segmentation_facets.get_data(); + sp.fuzzy = volume->fuzzy_skin_facets.get_data(); + if (!sp.supported.bitstream.empty() || !sp.seam.bitstream.empty() || + !sp.mmu.bitstream.empty() || !sp.fuzzy.bitstream.empty()) + saved_painting = std::move(sp); + } + volume->reset_extra_facets(); if (!volume->is_model_part()) { @@ -330,7 +389,8 @@ const ModelObjectPtrs& Cut::perform_with_plane() process_connector_cut(volume, instance_matrix, m_cut_matrix, m_attributes, upper, lower, dowels); } else if (!volume->mesh().empty()) - process_solid_part_cut(volume, instance_matrix, m_cut_matrix, m_attributes, upper, lower); + process_solid_part_cut(volume, instance_matrix, m_cut_matrix, m_attributes, upper, lower, + saved_painting ? &*saved_painting : nullptr); } // Post-process cut parts diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 3fd66f5f69..302bf47935 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -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(const TriangleSelector::TriangleSplittingData &data) { m_data = data; this->touch(); } bool set(const TriangleSelector& selector); indexed_triangle_set get_facets(const ModelVolume& mv, EnforcerBlockerType type) const; // BBS diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index a6d19f505c..6368955fce 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -1,9 +1,11 @@ #include "TriangleSelector.hpp" #include "Model.hpp" +#include "AABBTreeIndirect.hpp" #include #include #include +#include #include #ifndef NDEBUG @@ -168,24 +170,25 @@ void TriangleSelector::Triangle::set_division(int sides_to_split, int special_si this->special_side_idx = char(special_side_idx); } +// Real-time collision detection, Ericson, Chapter 3.4 +inline Vec3f barycentric(const Vec3f& pt, const Vec3f& p1, const Vec3f& p2, const Vec3f& p3) +{ + std::array 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; + + 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; +} + inline bool is_point_inside_triangle(const Vec3f &pt, const Vec3f &p1, const Vec3f &p2, const Vec3f &p3) { - // Real-time collision detection, Ericson, Chapter 3.4 - auto barycentric = [&pt, &p1, &p2, &p3]() -> Vec3f { - std::array 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; - - 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 barycentric_cords = barycentric(); + Vec3f barycentric_cords = barycentric(pt, p1, p2, p3); return std::all_of(begin(barycentric_cords), end(barycentric_cords), [](float cord) { return 0.f <= cord && cord <= 1.0; }); } @@ -935,7 +938,7 @@ 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 (!m_triangles[facet_idx].is_split()) m_triangles[facet_idx].set_state(type); tr = &m_triangles[facet_idx]; // might have been invalidated by split_triangle(). @@ -2208,4 +2211,259 @@ std::vector TriangleSelector::extract_used_facet_states(con return out; } +inline bool segments_intersect_proj( + const Vec3f& p1, const Vec3f& p2, const Vec3f& p3, const Vec3f& p4, const std::function(const Vec3f&)>& proj) +{ + auto cross2d = [](float ax, float ay, float bx, float by) -> float { return ax * by - ay * bx; }; + auto [u1, v1] = proj(p1); + auto [u2, v2] = proj(p2); + auto [u3, v3] = proj(p3); + auto [u4, v4] = proj(p4); + float ru = u2 - u1, rv = v2 - v1; + float su = u4 - u3, sv = v4 - v3; + float denom = cross2d(ru, rv, su, sv); + if (std::abs(denom) < 1e-10f) + return false; + float dpu = u3 - u1, dpv = v3 - v1; + float t1 = cross2d(dpu, dpv, su, sv) / denom; + 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& vertices) + : TriangleSelector::SinglePointCursor(center_, source_, radius_world, trafo_, clipping_plane_) + , vertices_(vertices) + {} + ~TriangleCursor() override = default; + + static std::unique_ptr 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(center, center + norm, radius, Transform3d::Identity(), TriangleSelector::ClippingPlane(), + std::array{pv0, pv1, pv2}); + } + + bool is_mesh_point_inside(const Vec3f& point) const override + { + return is_point_inside_triangle(point, vertices_[0], vertices_[1], vertices_[2]); + } + + bool is_edge_inside_cursor(const TriangleSelector::Triangle& tr, const std::vector& vertices) const override + { + std::array pts_proj; // Projected point onto the plane + std::array 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]); + } + if (std::abs(dist_b) > tolerance_) { + pt_b = (tolerance_ - dist_a) / (dist_b - dist_a) * (pts_proj[idx_b] - pts_proj[idx_a]); + } + + // If any projected end is inside the triangle, then is in + if (is_point_inside_triangle(pt_a, vertices_[0], vertices_[1], vertices_[2]) || + is_point_inside_triangle(pt_b, vertices_[0], vertices_[1], vertices_[2])) { + return true; + } + + // Otherwise see if the segment (pt_a, pt_b) intersects the triangle + { + const Vec3f uvw_a = barycentric(pt_a, vertices_[0], vertices_[1], vertices_[2]); + const Vec3f uvw_b = barycentric(pt_b, vertices_[0], vertices_[1], vertices_[2]); + auto proj = [&](const Vec3f& p) -> std::pair { return {p(0), p(1)}; }; + 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}; + + 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& face_normals) const override + { + return TriangleSelector::Cursor::is_facet_visible(*this, facet_idx, face_normals); + } + +private: + const std::array vertices_; + const float tolerance_ = EPSILON; + const float tolerance_sqr_ = tolerance_ * tolerance_; + + static float point_to_line_dist(const Vec3f& p, const Vec3f& a, const Vec3f& b) + { + const Eigen::ParametrizedLine line = Eigen::ParametrizedLine::Through(a, b); + return line.distance(p); + } +}; + +// 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) +{ + 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> 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 + AABBTreeIndirect::Tree3f target_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(target_its.vertices, target_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; + int u_axis = (axis + 1) % 3; + int v_axis = (axis + 2) % 3; + + auto proj = [&](const Vec3f& p) -> std::pair { + return { p(u_axis), p(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 + TriangleMesh target_mesh(target_its); + TriangleSelector target_selector(target_mesh); + const auto facet_angle_limit = cos(Geometry::deg2rad(5)); + auto check_normal = [&source_selector, &target_selector, facet_angle_limit](int src_idx, int dst_idx) -> bool { + const Vec3f& norm_a = source_selector.m_face_normals[src_idx]; + const Vec3f& norm_b = target_selector.m_face_normals[dst_idx]; + const float a = norm_a.dot(norm_b); + + return std::clamp(a, 0.f, 1.f) >= facet_angle_limit; + }; + 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); + + AABBTreeIndirect::traverse(target_tree, AABBTreeIndirect::intersecting(pt_bbox), [&](const AABBTreeIndirect::Tree3f::Node& node) -> bool { + size_t face_idx = node.idx; + if (face_idx >= target_its.indices.size()) + return true; + + const Vec3i32& face = target_its.indices[face_idx]; + const Vec3f& ta = target_its.vertices[face(0)]; + const Vec3f& tb = target_its.vertices[face(1)]; + const Vec3f& tc = target_its.vertices[face(2)]; + + if (check_normal(tri.source_triangle, face_idx) && 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); + } + return true; // continue traversal + }); + } + + return target_selector.serialize(); +} + } // namespace Slic3r diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index 50bbdd4ed0..99ad19548e 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -372,6 +372,13 @@ public: // The operation may merge split triangles if they are being assigned the same color. void seed_fill_apply_on_triangles(EnforcerBlockerType new_state); + // Remap painting data from source mesh to target mesh using spatial mapping. + // Both meshes must be in the same coordinate space. + static TriangleSplittingData remap_painting( + const indexed_triangle_set& source_its, + const TriangleSplittingData& source_painting, + const indexed_triangle_set& target_its); + protected: // Triangle and info about how it's split. class Triangle { @@ -521,11 +528,10 @@ private: int m_free_triangles_head { -1 }; int m_free_vertices_head { -1 }; + + friend class TriangleCursor; }; - - - } // namespace Slic3r #endif // libslic3r_TriangleSelector_hpp_ From e451df27f792bf6455121a72c55c384dcd34ba32 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 5 May 2026 11:05:48 +0800 Subject: [PATCH 04/35] Fix issue of pojection calculation --- src/libslic3r/TriangleSelector.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index 6368955fce..ba6926d227 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -2299,10 +2299,10 @@ public: 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]); + 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]); + 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 @@ -2315,7 +2315,7 @@ public: { const Vec3f uvw_a = barycentric(pt_a, vertices_[0], vertices_[1], vertices_[2]); const Vec3f uvw_b = barycentric(pt_b, vertices_[0], vertices_[1], vertices_[2]); - auto proj = [&](const Vec3f& p) -> std::pair { return {p(0), p(1)}; }; + auto proj = [](const Vec3f& p) -> std::pair { return {p(0), p(1)}; }; 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}; From 4b489339c623e8d66a1154686aba000147066a37 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 5 May 2026 11:59:20 +0800 Subject: [PATCH 05/35] Check normal before expanding the painting to neighboring faces --- src/libslic3r/TriangleSelector.cpp | 33 +++++++++++++++++------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index ba6926d227..a38220e2a5 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -171,7 +171,7 @@ void TriangleSelector::Triangle::set_division(int sides_to_split, int special_si } // Real-time collision detection, Ericson, Chapter 3.4 -inline Vec3f barycentric(const Vec3f& pt, const Vec3f& p1, const Vec3f& p2, const Vec3f& p3) +static Vec3f barycentric(const Vec3f& pt, const Vec3f& p1, const Vec3f& p2, const Vec3f& p3) { std::array v = {p2 - p1, p3 - p1, pt - p1}; float d00 = v[0].dot(v[0]); @@ -186,7 +186,7 @@ inline Vec3f barycentric(const Vec3f& pt, const Vec3f& p1, const Vec3f& p2, cons return barycentric_cords; } -inline bool is_point_inside_triangle(const Vec3f &pt, const Vec3f &p1, const Vec3f &p2, const Vec3f &p3) +static bool is_point_inside_triangle(const Vec3f &pt, const Vec3f &p1, const Vec3f &p2, const Vec3f &p3) { Vec3f barycentric_cords = barycentric(pt, p1, p2, p3); return std::all_of(begin(barycentric_cords), end(barycentric_cords), [](float cord) { return 0.f <= cord && cord <= 1.0; }); @@ -2211,8 +2211,7 @@ std::vector TriangleSelector::extract_used_facet_states(con return out; } -inline bool segments_intersect_proj( - const Vec3f& p1, const Vec3f& p2, const Vec3f& p3, const Vec3f& p4, const std::function(const Vec3f&)>& proj) +static bool segments_intersect_proj(const Vec3f& p1, const Vec3f& p2, const Vec3f& p3, const Vec3f& p4, const std::function(const Vec3f&)>& proj) { auto cross2d = [](float ax, float ay, float bx, float by) -> float { return ax * by - ay * bx; }; auto [u1, v1] = proj(p1); @@ -2332,13 +2331,22 @@ public: bool is_facet_visible(int facet_idx, const std::vector& face_normals) const override { - return TriangleSelector::Cursor::is_facet_visible(*this, facet_idx, face_normals); + 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; } private: const std::array vertices_; const float tolerance_ = EPSILON; const float tolerance_sqr_ = tolerance_ * tolerance_; + static const double facet_angle_limit; static float point_to_line_dist(const Vec3f& p, const Vec3f& a, const Vec3f& b) { @@ -2346,6 +2354,8 @@ private: return line.distance(p); } }; +const double TriangleCursor::facet_angle_limit = cos(Geometry::deg2rad(5)); + // Remap painting data from source mesh to target mesh using spatial mapping. TriangleSelector::TriangleSplittingData TriangleSelector::remap_painting( @@ -2423,14 +2433,6 @@ TriangleSelector::TriangleSplittingData TriangleSelector::remap_painting( // 4. For each painted face, we find the nearest target face, and apply the TriangleCursor to paint it TriangleMesh target_mesh(target_its); TriangleSelector target_selector(target_mesh); - const auto facet_angle_limit = cos(Geometry::deg2rad(5)); - auto check_normal = [&source_selector, &target_selector, facet_angle_limit](int src_idx, int dst_idx) -> bool { - const Vec3f& norm_a = source_selector.m_face_normals[src_idx]; - const Vec3f& norm_b = target_selector.m_face_normals[dst_idx]; - const float a = norm_a.dot(norm_b); - - return std::clamp(a, 0.f, 1.f) >= facet_angle_limit; - }; for (auto tri_ref : painted_triangles) { const Triangle& tri = tri_ref.get(); const Vec3f& pv0 = source_selector.m_vertices[tri.verts_idxs[0]].v; @@ -2449,12 +2451,15 @@ TriangleSelector::TriangleSplittingData TriangleSelector::remap_painting( if (face_idx >= target_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_its.indices[face_idx]; const Vec3f& ta = target_its.vertices[face(0)]; const Vec3f& tb = target_its.vertices[face(1)]; const Vec3f& tc = target_its.vertices[face(2)]; - if (check_normal(tri.source_triangle, face_idx) && check_overlap(pv0, pv1, pv2, ta, tb, tc)) { + 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); From 01e0013b1656c4e622c4d9d5090868b751ea5da2 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 5 May 2026 17:08:53 +0800 Subject: [PATCH 06/35] Don't remap paint unless it's the final cut --- src/libslic3r/CutUtils.cpp | 47 ++++++++++++++-------------- src/libslic3r/CutUtils.hpp | 2 +- src/libslic3r/Model.hpp | 1 + src/slic3r/GUI/Gizmos/GLGizmoCut.cpp | 3 +- 4 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/libslic3r/CutUtils.cpp b/src/libslic3r/CutUtils.cpp index 7979dc4095..b69c4cf4ee 100644 --- a/src/libslic3r/CutUtils.cpp +++ b/src/libslic3r/CutUtils.cpp @@ -14,9 +14,9 @@ namespace Slic3r { using namespace Geometry; -// Saved painting data for remapping after mesh cutting. +// Saved painting data for remapping after mesh change. struct SavedPainting { - indexed_triangle_set its; // Original mesh transformed to cut space + indexed_triangle_set its; // Original mesh TriangleSelector::TriangleSplittingData supported; TriangleSelector::TriangleSplittingData seam; TriangleSelector::TriangleSplittingData mmu; @@ -24,21 +24,24 @@ struct SavedPainting { }; // Remap painting data from saved source to a cut result mesh, and set on a volume. -static void remap_and_set_painting(ModelVolume* vol, const indexed_triangle_set& cut_its, - const SavedPainting& saved) +static void remap_and_set_painting(ModelVolume* vol, const SavedPainting* saved) { + 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.its, src_data, cut_its); + auto result = TriangleSelector::remap_painting(saved->its, src_data, vol->mesh().its); if (!result.bitstream.empty()) target_facets.set_data(result); }; - remap_one(saved.supported, vol->supported_facets); - remap_one(saved.seam, vol->seam_facets); - remap_one(saved.mmu, vol->mmu_segmentation_facets); - remap_one(saved.fuzzy, vol->fuzzy_skin_facets); + remap_one(saved->supported, vol->supported_facets); + remap_one(saved->seam, vol->seam_facets); + remap_one(saved->mmu, vol->mmu_segmentation_facets); + remap_one(saved->fuzzy, vol->fuzzy_skin_facets); } static void apply_tolerance(ModelVolume* vol) @@ -217,28 +220,26 @@ static void process_solid_part_cut(ModelVolume* volume, const Transform3d& insta // Add required cut parts to the objects if (attributes.has(ModelObjectCutAttribute::KeepAsParts)) { - add_cut_volume(upper_mesh, upper, volume, cut_matrix, "_A"); - if (saved_painting && !upper_mesh.empty()) - remap_and_set_painting(upper->volumes.back(), upper_mesh.its, *saved_painting); + if (!upper_mesh.empty()) { + add_cut_volume(upper_mesh, upper, volume, cut_matrix, "_A"); + remap_and_set_painting(upper->volumes.back(), saved_painting); + } if (!lower_mesh.empty()) { add_cut_volume(lower_mesh, upper, volume, cut_matrix, "_B"); - if (saved_painting) - remap_and_set_painting(upper->volumes.back(), lower_mesh.its, *saved_painting); upper->volumes.back()->cut_info.is_from_upper = false; + remap_and_set_painting(upper->volumes.back(), saved_painting); } return; } - if (attributes.has(ModelObjectCutAttribute::KeepUpper)) { + if (attributes.has(ModelObjectCutAttribute::KeepUpper) && !upper_mesh.empty()) { add_cut_volume(upper_mesh, upper, volume, cut_matrix); - if (saved_painting && !upper_mesh.empty()) - remap_and_set_painting(upper->volumes.back(), upper_mesh.its, *saved_painting); + remap_and_set_painting(upper->volumes.back(), saved_painting); } if (attributes.has(ModelObjectCutAttribute::KeepLower) && !lower_mesh.empty()) { add_cut_volume(lower_mesh, lower, volume, cut_matrix); - if (saved_painting) - remap_and_set_painting(lower->volumes.back(), lower_mesh.its, *saved_painting); + remap_and_set_painting(lower->volumes.back(), saved_painting); } } @@ -363,21 +364,19 @@ const ModelObjectPtrs& Cut::perform_with_plane() // Save painting data before reset_extra_facets() discards it. // Only for model parts that will be cut (not modifiers/connectors). std::optional saved_painting; - if (volume->is_model_part() && !volume->mesh().empty()) { - SavedPainting sp; + if (m_attributes.has(ModelObjectCutAttribute::KeepPaint) && volume->is_any_painted() && volume->is_model_part() && !volume->mesh().empty()) { // Get mesh in cut space (same transform as process_volume_cut applies) TriangleMesh mesh(volume->mesh()); const auto volume_matrix = volume->get_matrix(); mesh.transform(inverse_cut_matrix * instance_matrix * volume_matrix, true); mesh.transform(m_cut_matrix); + SavedPainting sp; sp.its = std::move(mesh.its); sp.supported = volume->supported_facets.get_data(); sp.seam = volume->seam_facets.get_data(); sp.mmu = volume->mmu_segmentation_facets.get_data(); sp.fuzzy = volume->fuzzy_skin_facets.get_data(); - if (!sp.supported.bitstream.empty() || !sp.seam.bitstream.empty() || - !sp.mmu.bitstream.empty() || !sp.fuzzy.bitstream.empty()) - saved_painting = std::move(sp); + saved_painting = std::move(sp); } volume->reset_extra_facets(); diff --git a/src/libslic3r/CutUtils.hpp b/src/libslic3r/CutUtils.hpp index 2c477a3e2b..f39007c8a9 100644 --- a/src/libslic3r/CutUtils.hpp +++ b/src/libslic3r/CutUtils.hpp @@ -11,7 +11,7 @@ namespace Slic3r { using ModelObjectPtrs = std::vector; -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; ENABLE_ENUM_BITMASK_OPERATORS(ModelObjectCutAttribute); diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 302bf47935..0de76dbef4 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -1005,6 +1005,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. diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index 2483f7bd1b..59c14ef205 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -3329,7 +3329,8 @@ 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) | + 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); From 368563e14ef26bae0bc9463b73482fae1cc57781 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 5 May 2026 17:55:38 +0800 Subject: [PATCH 07/35] Fix part offset --- src/libslic3r/CutUtils.cpp | 2 +- src/libslic3r/TriangleSelector.cpp | 18 ++++++++++-------- src/libslic3r/TriangleSelector.hpp | 5 +++-- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/libslic3r/CutUtils.cpp b/src/libslic3r/CutUtils.cpp index b69c4cf4ee..16f0a19816 100644 --- a/src/libslic3r/CutUtils.cpp +++ b/src/libslic3r/CutUtils.cpp @@ -34,7 +34,7 @@ static void remap_and_set_painting(ModelVolume* vol, const SavedPainting* saved) FacetsAnnotation& target_facets) { if (src_data.bitstream.empty()) return; - auto result = TriangleSelector::remap_painting(saved->its, src_data, vol->mesh().its); + auto result = TriangleSelector::remap_painting(saved->its, src_data, vol->mesh().its, translation_transform(vol->mesh().get_init_shift())); if (!result.bitstream.empty()) target_facets.set_data(result); }; diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index a38220e2a5..d4821df7cd 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -2361,7 +2361,8 @@ const double TriangleCursor::facet_angle_limit = cos(Geometry::deg2rad(5)); TriangleSelector::TriangleSplittingData TriangleSelector::remap_painting( const indexed_triangle_set& source_its, const TriangleSplittingData& source_painting, - const indexed_triangle_set& target_its) + const indexed_triangle_set& target_its, + const Transform3d& target_transform) { TriangleSelector::TriangleSplittingData result; if (source_painting.bitstream.empty()) @@ -2385,7 +2386,9 @@ TriangleSelector::TriangleSplittingData TriangleSelector::remap_painting( return result; // 3. Build AABB tree of target mesh so we could find nearest face quickly - AABBTreeIndirect::Tree3f target_tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(target_its.vertices, target_its.indices); + 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 @@ -2431,7 +2434,6 @@ TriangleSelector::TriangleSplittingData TriangleSelector::remap_painting( }; // 4. For each painted face, we find the nearest target face, and apply the TriangleCursor to paint it - TriangleMesh target_mesh(target_its); TriangleSelector target_selector(target_mesh); for (auto tri_ref : painted_triangles) { const Triangle& tri = tri_ref.get(); @@ -2448,16 +2450,16 @@ TriangleSelector::TriangleSplittingData TriangleSelector::remap_painting( AABBTreeIndirect::traverse(target_tree, AABBTreeIndirect::intersecting(pt_bbox), [&](const AABBTreeIndirect::Tree3f::Node& node) -> bool { size_t face_idx = node.idx; - if (face_idx >= target_its.indices.size()) + 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_its.indices[face_idx]; - const Vec3f& ta = target_its.vertices[face(0)]; - const Vec3f& tb = target_its.vertices[face(1)]; - const Vec3f& tc = target_its.vertices[face(2)]; + 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 diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index 99ad19548e..8f02c7d717 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -373,11 +373,12 @@ public: void seed_fill_apply_on_triangles(EnforcerBlockerType new_state); // Remap painting data from source mesh to target mesh using spatial mapping. - // Both meshes must be in the same coordinate space. + // `target_transform` should transform the target mesh into source's coordinate space. static TriangleSplittingData remap_painting( const indexed_triangle_set& source_its, const TriangleSplittingData& source_painting, - const indexed_triangle_set& target_its); + const indexed_triangle_set& target_its, + const Transform3d& target_transform); protected: // Triangle and info about how it's split. From 5978e96ff5a8b643afc85e899bf53a788f646726 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 5 May 2026 18:39:50 +0800 Subject: [PATCH 08/35] Add option to enable/disable paint keep --- src/slic3r/GUI/Gizmos/GLGizmoCut.cpp | 2 +- src/slic3r/GUI/Preferences.cpp | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index 59c14ef205..bfc757589c 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -3330,7 +3330,7 @@ void GLGizmoCut3D::perform_cut(const Selection& selection) 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) | - ModelObjectCutAttribute::KeepPaint; + only_if(wxGetApp().app_config->get_bool("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); diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index c5d377b67b..fc27d27e31 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -956,7 +956,7 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " sync_user_preset: " << (sync ? "true" : "false"); } - #ifdef __WXMSW__ +#ifdef __WXMSW__ if (param == "associate_3mf") { bool pbool = app_config->get("associate_3mf") == "true" ? true : false; if (pbool) { @@ -1776,6 +1776,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); From be141d32c18a16e7aaa5b15f89af06f62a97d721 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Tue, 5 May 2026 21:43:29 +0800 Subject: [PATCH 09/35] Speed up calculation by caching barycentric independent variables --- src/libslic3r/TriangleSelector.cpp | 124 +++++++++++++++++++---------- 1 file changed, 83 insertions(+), 41 deletions(-) diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index d4821df7cd..e4d1a4521c 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -170,25 +170,69 @@ void TriangleSelector::Triangle::set_division(int sides_to_split, int special_si this->special_side_idx = char(special_side_idx); } +// Pre-computed barycentric resolver // Real-time collision detection, Ericson, Chapter 3.4 -static Vec3f barycentric(const Vec3f& pt, const Vec3f& p1, const Vec3f& p2, const Vec3f& p3) +// 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 { - std::array 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; + + 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 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(pt, p1, p2, 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; }); } @@ -2211,21 +2255,22 @@ std::vector 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::function(const Vec3f&)>& proj) +static bool segments_intersect_proj(const Vec3f& p1, const Vec3f& p2, const Vec3f& p3, const Vec3f& p4, const std::pair& proj) { auto cross2d = [](float ax, float ay, float bx, float by) -> float { return ax * by - ay * bx; }; - auto [u1, v1] = proj(p1); - auto [u2, v2] = proj(p2); - auto [u3, v3] = proj(p3); - auto [u4, v4] = proj(p4); - float ru = u2 - u1, rv = v2 - v1; - float su = u4 - u3, sv = v4 - v3; - float denom = cross2d(ru, rv, su, sv); + 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; - float dpu = u3 - u1, dpv = v3 - v1; - float t1 = cross2d(dpu, dpv, su, sv) / denom; - float t2 = cross2d(dpu, dpv, ru, rv) / denom; + 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; }; @@ -2240,7 +2285,7 @@ public: const TriangleSelector::ClippingPlane& clipping_plane_, const std::array& vertices) : TriangleSelector::SinglePointCursor(center_, source_, radius_world, trafo_, clipping_plane_) - , vertices_(vertices) + , barycentric_(vertices[0], vertices[1], vertices[2]) {} ~TriangleCursor() override = default; @@ -2266,7 +2311,7 @@ public: bool is_mesh_point_inside(const Vec3f& point) const override { - return is_point_inside_triangle(point, vertices_[0], vertices_[1], vertices_[2]); + return barycentric_.is_point_inside_triangle(point); } bool is_edge_inside_cursor(const TriangleSelector::Triangle& tr, const std::vector& vertices) const override @@ -2305,19 +2350,19 @@ public: } // If any projected end is inside the triangle, then is in - if (is_point_inside_triangle(pt_a, vertices_[0], vertices_[1], vertices_[2]) || - is_point_inside_triangle(pt_b, vertices_[0], vertices_[1], vertices_[2])) { + 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(pt_a, vertices_[0], vertices_[1], vertices_[2]); - const Vec3f uvw_b = barycentric(pt_b, vertices_[0], vertices_[1], vertices_[2]); - auto proj = [](const Vec3f& p) -> std::pair { return {p(0), p(1)}; }; - 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}; + 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 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)|| @@ -2343,7 +2388,7 @@ public: } private: - const std::array vertices_; + const Barycentric barycentric_; const float tolerance_ = EPSILON; const float tolerance_sqr_ = tolerance_ * tolerance_; static const double facet_angle_limit; @@ -2413,12 +2458,9 @@ TriangleSelector::TriangleSplittingData TriangleSelector::remap_painting( 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; - int u_axis = (axis + 1) % 3; - int v_axis = (axis + 2) % 3; - - auto proj = [&](const Vec3f& p) -> std::pair { - return { p(u_axis), p(v_axis) }; - }; + const int u_axis = (axis + 1) % 3; + const int v_axis = (axis + 2) % 3; + const std::pair 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; From 83e9f17aa817c28c03ea68db610aca59feebca44 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Thu, 7 May 2026 22:24:45 +0800 Subject: [PATCH 10/35] Fix issue that `Geometry::deg2rad()` do calculation in the same type as the parameter, which means if the parameter is `int` then you lose all the precision --- src/libslic3r/Geometry.hpp | 6 +++++- src/libslic3r/TriangleSelector.cpp | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/libslic3r/Geometry.hpp b/src/libslic3r/Geometry.hpp index 0ae7524fa0..e610a9ed4f 100644 --- a/src/libslic3r/Geometry.hpp +++ b/src/libslic3r/Geometry.hpp @@ -298,7 +298,11 @@ bool directions_perpendicular(double angle1, double angle2, double max_diff = 0) template bool contains(const std::vector &vector, const Point &point); template T rad2deg(T angle) { return T(180.0) * angle / T(PI); } double rad2deg_dir(double angle); -template constexpr T deg2rad(const T angle) { return T(PI) * angle / T(180.0); } +template constexpr T deg2rad(const T angle) +{ + static_assert(std::is_floating_point::value, "Why do you want to calculate angle in integer?"); + return T(PI) * angle / T(180.0); +} template T angle_to_0_2PI(T angle) { static const T TWO_PI = T(2) * T(PI); diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index e4d1a4521c..19f638a160 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -2399,7 +2399,7 @@ private: return line.distance(p); } }; -const double TriangleCursor::facet_angle_limit = cos(Geometry::deg2rad(5)); +const double TriangleCursor::facet_angle_limit = cos(Geometry::deg2rad(5.0)); // Remap painting data from source mesh to target mesh using spatial mapping. From fad680fcc160034f0aaa03d52e1a2366529a731e Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Fri, 8 May 2026 21:23:38 +0800 Subject: [PATCH 11/35] Simplify mesh transform --- src/libslic3r/CutUtils.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libslic3r/CutUtils.cpp b/src/libslic3r/CutUtils.cpp index 16f0a19816..a2cd7583a5 100644 --- a/src/libslic3r/CutUtils.cpp +++ b/src/libslic3r/CutUtils.cpp @@ -368,8 +368,7 @@ const ModelObjectPtrs& Cut::perform_with_plane() // Get mesh in cut space (same transform as process_volume_cut applies) TriangleMesh mesh(volume->mesh()); const auto volume_matrix = volume->get_matrix(); - mesh.transform(inverse_cut_matrix * instance_matrix * volume_matrix, true); - mesh.transform(m_cut_matrix); + mesh.transform(instance_matrix * volume_matrix, true); SavedPainting sp; sp.its = std::move(mesh.its); sp.supported = volume->supported_facets.get_data(); From 2f160ad8bc397431d1146ecbec757130de2c90dd Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Fri, 8 May 2026 22:54:20 +0800 Subject: [PATCH 12/35] Rearrange code a little bit to make it more reusable --- src/libslic3r/CutUtils.cpp | 40 +++++++++--------------------- src/libslic3r/Model.hpp | 18 ++++++++++++++ src/libslic3r/TriangleSelector.hpp | 9 +++++++ 3 files changed, 39 insertions(+), 28 deletions(-) diff --git a/src/libslic3r/CutUtils.cpp b/src/libslic3r/CutUtils.cpp index a2cd7583a5..8586b952f3 100644 --- a/src/libslic3r/CutUtils.cpp +++ b/src/libslic3r/CutUtils.cpp @@ -14,17 +14,8 @@ namespace Slic3r { using namespace Geometry; -// Saved painting data for remapping after mesh change. -struct SavedPainting { - indexed_triangle_set its; // Original mesh - TriangleSelector::TriangleSplittingData supported; - TriangleSelector::TriangleSplittingData seam; - TriangleSelector::TriangleSplittingData mmu; - TriangleSelector::TriangleSplittingData fuzzy; -}; - // Remap painting data from saved source to a cut result mesh, and set on a volume. -static void remap_and_set_painting(ModelVolume* vol, const SavedPainting* saved) +static void remap_and_set_painting(ModelVolume* vol, const std::optional& saved) { if (!saved) { return; @@ -34,7 +25,7 @@ static void remap_and_set_painting(ModelVolume* vol, const SavedPainting* saved) FacetsAnnotation& target_facets) { if (src_data.bitstream.empty()) return; - auto result = TriangleSelector::remap_painting(saved->its, src_data, vol->mesh().its, translation_transform(vol->mesh().get_init_shift())); + auto result = TriangleSelector::remap_painting(saved->mesh.its, src_data, vol->mesh().its, translation_transform(vol->mesh().get_init_shift())); if (!result.bitstream.empty()) target_facets.set_data(result); }; @@ -211,7 +202,7 @@ static void process_modifier_cut(ModelVolume* volume, const Transform3d& instanc static void process_solid_part_cut(ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& cut_matrix, ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower, - const SavedPainting* saved_painting = nullptr) + const std::optional& saved_painting) { // Perform cut TriangleMesh upper_mesh, lower_mesh; @@ -362,20 +353,14 @@ const ModelObjectPtrs& Cut::perform_with_plane() for (ModelVolume* volume : mo->volumes) { // Save painting data before reset_extra_facets() discards it. - // Only for model parts that will be cut (not modifiers/connectors). - std::optional saved_painting; - if (m_attributes.has(ModelObjectCutAttribute::KeepPaint) && volume->is_any_painted() && volume->is_model_part() && !volume->mesh().empty()) { - // Get mesh in cut space (same transform as process_volume_cut applies) - TriangleMesh mesh(volume->mesh()); - const auto volume_matrix = volume->get_matrix(); - mesh.transform(instance_matrix * volume_matrix, true); - SavedPainting sp; - sp.its = std::move(mesh.its); - sp.supported = volume->supported_facets.get_data(); - sp.seam = volume->seam_facets.get_data(); - sp.mmu = volume->mmu_segmentation_facets.get_data(); - sp.fuzzy = volume->fuzzy_skin_facets.get_data(); - saved_painting = std::move(sp); + std::optional saved_painting; + if (m_attributes.has(ModelObjectCutAttribute::KeepPaint)) { + saved_painting = volume->save_painting(); + if (saved_painting) { + // Transform mesh to cut space (same transform as process_volume_cut applies) + const auto volume_matrix = volume->get_matrix(); + saved_painting->mesh.transform(instance_matrix * volume_matrix, true); + } } volume->reset_extra_facets(); @@ -387,8 +372,7 @@ const ModelObjectPtrs& Cut::perform_with_plane() process_connector_cut(volume, instance_matrix, m_cut_matrix, m_attributes, upper, lower, dowels); } else if (!volume->mesh().empty()) - process_solid_part_cut(volume, instance_matrix, m_cut_matrix, m_attributes, upper, lower, - saved_painting ? &*saved_painting : nullptr); + process_solid_part_cut(volume, instance_matrix, m_cut_matrix, m_attributes, upper, lower, saved_painting); } // Post-process cut parts diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 0de76dbef4..ccc0760097 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -878,6 +878,24 @@ 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 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 {}; + } + // BBS: quick access for volume extruders, 1 based mutable std::vector mmuseg_extruders; mutable Timestamp mmuseg_ts; diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index 8f02c7d717..412c9fb9b1 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -372,6 +372,15 @@ 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. static TriangleSplittingData remap_painting( From 4490dccade7deb179a9b763dc748209fb450be72 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Sat, 9 May 2026 10:12:36 +0800 Subject: [PATCH 13/35] Fix issue that support/seam/fuzzy skin painting not kept after split --- src/libslic3r/Model.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index ef5ef6dbe6..bcdf1e54d3 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -2078,10 +2078,16 @@ 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); } // BBS: clear volume's config, as we already set them into object From bfde91685e420d1f29801c39be29bbbabe2eb7cd Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Sat, 9 May 2026 16:16:06 +0800 Subject: [PATCH 14/35] Rearrange code a little bit to make it more reusable --- src/libslic3r/CutUtils.cpp | 29 ++++------------------------- src/libslic3r/Model.cpp | 35 +++++++++++++++++++++++++++++++++++ src/libslic3r/Model.hpp | 18 ++++-------------- 3 files changed, 43 insertions(+), 39 deletions(-) diff --git a/src/libslic3r/CutUtils.cpp b/src/libslic3r/CutUtils.cpp index 8586b952f3..7fc21bb3b4 100644 --- a/src/libslic3r/CutUtils.cpp +++ b/src/libslic3r/CutUtils.cpp @@ -14,27 +14,6 @@ namespace Slic3r { using namespace Geometry; -// Remap painting data from saved source to a cut result mesh, and set on a volume. -static void remap_and_set_painting(ModelVolume* vol, const std::optional& saved) -{ - 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, vol->mesh().its, translation_transform(vol->mesh().get_init_shift())); - if (!result.bitstream.empty()) - target_facets.set_data(result); - }; - remap_one(saved->supported, vol->supported_facets); - remap_one(saved->seam, vol->seam_facets); - remap_one(saved->mmu, vol->mmu_segmentation_facets); - remap_one(saved->fuzzy, vol->fuzzy_skin_facets); -} - static void apply_tolerance(ModelVolume* vol) { ModelVolume::CutInfo& cut_info = vol->cut_info; @@ -213,24 +192,24 @@ static void process_solid_part_cut(ModelVolume* volume, const Transform3d& insta if (attributes.has(ModelObjectCutAttribute::KeepAsParts)) { if (!upper_mesh.empty()) { add_cut_volume(upper_mesh, upper, volume, cut_matrix, "_A"); - remap_and_set_painting(upper->volumes.back(), saved_painting); + upper->volumes.back()->restore_painting(saved_painting); } if (!lower_mesh.empty()) { add_cut_volume(lower_mesh, upper, volume, cut_matrix, "_B"); upper->volumes.back()->cut_info.is_from_upper = false; - remap_and_set_painting(upper->volumes.back(), saved_painting); + upper->volumes.back()->restore_painting(saved_painting); } return; } if (attributes.has(ModelObjectCutAttribute::KeepUpper) && !upper_mesh.empty()) { add_cut_volume(upper_mesh, upper, volume, cut_matrix); - remap_and_set_painting(upper->volumes.back(), saved_painting); + upper->volumes.back()->restore_painting(saved_painting); } if (attributes.has(ModelObjectCutAttribute::KeepLower) && !lower_mesh.empty()) { add_cut_volume(lower_mesh, lower, volume, cut_matrix); - remap_and_set_painting(lower->volumes.back(), saved_painting); + lower->volumes.back()->restore_painting(saved_painting); } } diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index bcdf1e54d3..57a114479b 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -1980,6 +1980,41 @@ void ModelVolume::reset_extra_facets() this->fuzzy_skin_facets.reset(); } +std::optional 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& saved) +{ + 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())); + if (!result.bitstream.empty()) + target_facets.set_data(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())) { diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index ccc0760097..5bf345607d 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -881,20 +881,10 @@ public: // 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 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 {}; - } + std::optional save_painting() const; + + // Remap painting data from previous saved source to this mesh + void restore_painting(const std::optional& saved); // BBS: quick access for volume extruders, 1 based mutable std::vector mmuseg_extruders; From c2d33bea08d1d6e51451d51248382b0a98294257 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Sat, 9 May 2026 16:18:49 +0800 Subject: [PATCH 15/35] Remap paint after split to objects --- src/OrcaSlicer.cpp | 2 +- src/libslic3r/Model.cpp | 10 +++++++++- src/libslic3r/Model.hpp | 2 +- src/slic3r/GUI/Plater.cpp | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/OrcaSlicer.cpp b/src/OrcaSlicer.cpp index ba52f6787b..b258e096d4 100644 --- a/src/OrcaSlicer.cpp +++ b/src/OrcaSlicer.cpp @@ -4450,7 +4450,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)); } } diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 57a114479b..09751cd7df 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -2028,13 +2028,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 all_meshes; std::vector all_transfos; std::vector> volume_mesh_counts; all_meshes.reserve(this->volumes.size() * 5); bool is_multi_volume_object = (this->volumes.size() > 1); + std::optional saved_painting; for (int volume_idx = 0; volume_idx < this->volumes.size(); volume_idx++) { ModelVolume* volume = this->volumes[volume_idx]; @@ -2046,6 +2047,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 volume_meshes = volume->mesh().split(); int mesh_count = 0; @@ -2123,6 +2128,9 @@ void ModelObject::split(ModelObjectPtrs* new_objects) 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 diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 5bf345607d..6a672defb0 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -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 diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index ec74a796e4..f78cc3f9b7 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -7603,7 +7603,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.")); From fb0cf966cf5bd1ba77f6396204931d2a44b5718e Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Sat, 9 May 2026 17:06:19 +0800 Subject: [PATCH 16/35] Remap paint after split to parts --- src/libslic3r/Model.cpp | 7 ++++++- src/libslic3r/Model.hpp | 2 +- src/slic3r/GUI/GUI_ObjectList.cpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmoCut.cpp | 2 +- src/slic3r/Utils/FixModelByCgal.cpp | 2 +- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 09751cd7df..586043bf4b 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -2731,7 +2731,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 meshes = this->mesh().split(); if (meshes.size() <= 1) @@ -2741,6 +2741,9 @@ size_t ModelVolume::split(unsigned int max_extruders) if (text_configuration.has_value()) text_configuration.reset(); + std::optional saved_painting = remap_paint ? save_painting() : + std::optional{}; + 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; @@ -2776,6 +2779,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; } diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 6a672defb0..fbe8da7b6a 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -930,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); diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 726ea1aca3..81a4791a0d 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -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; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index bfc757589c..c2e4a19a73 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -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); // TODO: fix this m_parts.clear(); for (const ModelVolume* volume : volumes) { diff --git a/src/slic3r/Utils/FixModelByCgal.cpp b/src/slic3r/Utils/FixModelByCgal.cpp index c014ceaf04..e1f4320fec 100644 --- a/src/slic3r/Utils/FixModelByCgal.cpp +++ b/src/slic3r/Utils/FixModelByCgal.cpp @@ -112,7 +112,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, false); // TODO: fix this if (parts_count > 1) { const std::string msg = Slic3r::format(L("Split into %1% parts"), parts_count); on_progress(msg.c_str(), 10); From b3a513eab9a014e01def5c80e3958dc3fea0eaea Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Sat, 9 May 2026 17:36:17 +0800 Subject: [PATCH 17/35] Remove redundent mmu facets assign because that's already done in `new_object->add_volume(*volume)` --- src/slic3r/GUI/GUI_ObjectList.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 81a4791a0d..0a8d0f480a 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -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); From e6b3f6ccef662428a3c909c4f3769111333067f3 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Sat, 9 May 2026 21:47:40 +0800 Subject: [PATCH 18/35] Add option to keep existing painting while mapping new paintings to it --- src/libslic3r/Model.cpp | 10 +++++++--- src/libslic3r/Model.hpp | 4 ++-- src/libslic3r/TriangleSelector.cpp | 7 ++++++- src/libslic3r/TriangleSelector.hpp | 4 +++- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 586043bf4b..000ab997a9 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -1995,7 +1995,7 @@ std::optional ModelVolume::save_painting() cons return {}; } -void ModelVolume::restore_painting(const std::optional& saved) +void ModelVolume::restore_painting(const std::optional& saved, const bool keep_existing_paint) { if (!saved) { return; @@ -2005,9 +2005,13 @@ void ModelVolume::restore_painting(const std::optionalmesh.its, src_data, mesh().its, Geometry::translation_transform(mesh().get_init_shift())); + 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::ref(target_facets.get_data())} : + std::optional>{}); if (!result.bitstream.empty()) - target_facets.set_data(result); + target_facets.set_data(std::move(result)); }; remap_one(saved->supported, supported_facets); remap_one(saved->seam, seam_facets); diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index fbe8da7b6a..d8697adb41 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -731,7 +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(const TriangleSelector::TriangleSplittingData &data) { m_data = data; this->touch(); } + 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 @@ -884,7 +884,7 @@ public: std::optional save_painting() const; // Remap painting data from previous saved source to this mesh - void restore_painting(const std::optional& saved); + void restore_painting(const std::optional& saved, bool keep_existing_paint = false); // BBS: quick access for volume extruders, 1 based mutable std::vector mmuseg_extruders; diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index 19f638a160..0fb1423df4 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -2407,7 +2407,8 @@ 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 Transform3d& target_transform, + const std::optional>& existing_painting) { TriangleSelector::TriangleSplittingData result; if (source_painting.bitstream.empty()) @@ -2477,6 +2478,10 @@ TriangleSelector::TriangleSplittingData TriangleSelector::remap_painting( // 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; diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index 412c9fb9b1..09b606b5d2 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -383,11 +383,13 @@ public: // 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 Transform3d& target_transform, + const std::optional>& existing_painting); protected: // Triangle and info about how it's split. From 3aadd6e0808e7aed0011f45d4c04b3230f4ef1d7 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Sat, 9 May 2026 21:48:52 +0800 Subject: [PATCH 19/35] Remap paint after mesh boolean --- src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.cpp | 32 +++++++++++++++++--- src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.hpp | 4 ++- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.cpp b/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.cpp index bfff70d315..c7186e5d80 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.cpp @@ -188,6 +188,19 @@ CommonGizmosDataID GLGizmoMeshBoolean::on_get_requirements() const | int(CommonGizmosDataID::ObjectClipper)); } +std::optional VolumeInfo::save_painting() const +{ + if (wxGetApp().app_config->get_bool("keep_painting")) { + std::optional 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 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> 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 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> 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 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> 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>& 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; diff --git a/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.hpp b/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.hpp index c012a68dca..9c36be5cd9 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.hpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoMeshBoolean.hpp @@ -34,6 +34,8 @@ struct VolumeInfo { void serialize(Archive& ar) { ar(volume_idx, trafo); } + + std::optional 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>& saved_paintings); }; } // namespace GUI From f8a18e7656f2146b56cc3ae78d48690da7d88ad1 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Sun, 10 May 2026 00:41:18 +0800 Subject: [PATCH 20/35] Remap paint after mesh boolean (from context menu) --- src/slic3r/GUI/GUI_ObjectList.cpp | 39 +++++++++++++++++++++++++++++++ src/slic3r/GUI/Plater.cpp | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 0a8d0f480a..9da5edf6c3 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -3225,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> 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 @@ -3236,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(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index f78cc3f9b7..e95d428248 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -15108,7 +15108,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; From 219ef664bd0b7e53f280410410f97562c0d15226 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Sun, 10 May 2026 11:45:15 +0800 Subject: [PATCH 21/35] Fix issue that in certain cases the paint is not applied when faces are parallel to an axis, by adding a small tolerance to the bbox --- src/libslic3r/TriangleSelector.cpp | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index 0fb1423df4..93cb8747a6 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -2335,18 +2335,18 @@ public: // 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_)) { + 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_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 (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 @@ -2387,10 +2387,10 @@ public: return std::clamp(a, 0.f, 1.f) >= facet_angle_limit; } + static constexpr float tolerance = 0.01f; + private: const Barycentric barycentric_; - const float tolerance_ = EPSILON; - const float tolerance_sqr_ = tolerance_ * tolerance_; static const double facet_angle_limit; static float point_to_line_dist(const Vec3f& p, const Vec3f& a, const Vec3f& b) @@ -2494,6 +2494,8 @@ TriangleSelector::TriangleSplittingData TriangleSelector::remap_painting( 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; From 73096a11611789ab8a4c092fdcc55520747a64c5 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Sun, 10 May 2026 12:35:31 +0800 Subject: [PATCH 22/35] Fix artifact during normal paint --- src/libslic3r/TriangleSelector.cpp | 18 ++++++++++-------- src/libslic3r/TriangleSelector.hpp | 7 ++++--- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/libslic3r/TriangleSelector.cpp b/src/libslic3r/TriangleSelector.cpp index 93cb8747a6..3b032cc57e 100644 --- a/src/libslic3r/TriangleSelector.cpp +++ b/src/libslic3r/TriangleSelector.cpp @@ -280,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, 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, 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); @@ -343,7 +343,7 @@ void TriangleSelector::select_patch(int facet_start, std::unique_ptr &&c Matrix3f normal_matrix = static_cast(trafo_no_translate.matrix().block(0, 0, 3, 3).inverse().transpose().cast()); 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)) @@ -599,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())); @@ -609,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, @@ -949,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())); @@ -982,8 +982,10 @@ bool TriangleSelector::select_triangle_recursive(int facet_idx, const Vec3i32 &n if (triangle_splitting) split_triangle(facet_idx, neighbors); - 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; @@ -993,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 } } @@ -2513,7 +2515,7 @@ TriangleSelector::TriangleSplittingData TriangleSelector::remap_painting( 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); + Transform3d::Identity(), true, 0.f, true); } return true; // continue traversal }); diff --git a/src/libslic3r/TriangleSelector.hpp b/src/libslic3r/TriangleSelector.hpp index 09b606b5d2..11517f5c6c 100644 --- a/src/libslic3r/TriangleSelector.hpp +++ b/src/libslic3r/TriangleSelector.hpp @@ -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 @@ -496,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. From 51d2c91c9b10108f16d58bb97d3b8a4d2102c745 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Sun, 10 May 2026 16:26:19 +0800 Subject: [PATCH 23/35] Remap paint after reload from disk --- src/slic3r/GUI/Plater.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index e95d428248..c876ca457e 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -8880,6 +8880,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(); From 5c754e360e72c04d1091fb4188367b70f3bad4c4 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Sun, 10 May 2026 16:33:08 +0800 Subject: [PATCH 24/35] Spatial paint remapping after replace stl --- src/slic3r/GUI/Plater.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index c876ca457e..e184ad6272 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -8351,10 +8351,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) From 1299fcef4ae397a2f56e05ca81b76355cbec9628 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Sun, 10 May 2026 18:27:02 +0800 Subject: [PATCH 25/35] Remap paint after repair model --- src/slic3r/GUI/GUI_ObjectList.cpp | 7 +++++-- src/slic3r/GUI/Gizmos/GLGizmoCut.cpp | 7 ++++--- src/slic3r/Utils/FixModelByCgal.cpp | 18 +++++++++++++++--- src/slic3r/Utils/FixModelByCgal.hpp | 2 +- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 9da5edf6c3..93e5c94432 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -6086,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(); diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index c2e4a19a73..da523788b8 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -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) | @@ -3330,7 +3331,7 @@ void GLGizmoCut3D::perform_cut(const Selection& selection) 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(wxGetApp().app_config->get_bool("keep_painting"),ModelObjectCutAttribute::KeepPaint); + 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); @@ -3363,12 +3364,12 @@ void GLGizmoCut3D::perform_cut(const Selection& selection) // model_name failing reason std::vector> 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 &succes_models, std::vector> &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); diff --git a/src/slic3r/Utils/FixModelByCgal.cpp b/src/slic3r/Utils/FixModelByCgal.cpp index e1f4320fec..9e2e014d93 100644 --- a/src/slic3r/Utils/FixModelByCgal.cpp +++ b/src/slic3r/Utils/FixModelByCgal.cpp @@ -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::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, false); // TODO: fix this + 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 saved_painting = keep_painting ? + part_volume->save_painting() : + std::optional{}; + 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); } } diff --git a/src/slic3r/Utils/FixModelByCgal.hpp b/src/slic3r/Utils/FixModelByCgal.hpp index 3caa6d7fb9..61c8c91087 100644 --- a/src/slic3r/Utils/FixModelByCgal.hpp +++ b/src/slic3r/Utils/FixModelByCgal.hpp @@ -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 From a0d1ccaf28262b1e5e7e2f6e8235fa17482bde50 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Sun, 10 May 2026 20:01:50 +0800 Subject: [PATCH 26/35] Refactor planar cut code so it can be reused by other cut type --- src/libslic3r/CutUtils.cpp | 64 +++++++++++++++++++++++++------------- src/libslic3r/Model.hpp | 1 + 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/libslic3r/CutUtils.cpp b/src/libslic3r/CutUtils.cpp index 7fc21bb3b4..9fc6f13c2a 100644 --- a/src/libslic3r/CutUtils.cpp +++ b/src/libslic3r/CutUtils.cpp @@ -8,7 +8,6 @@ #include "ObjectID.hpp" #include -#include namespace Slic3r { @@ -62,9 +61,10 @@ static void add_cut_volume(TriangleMesh& mesh, ModelObject* object, const ModelV assert(vol->config.id() != src_volume->config.id()); vol->set_material(src_volume->material_id(), *src_volume->material()); vol->cut_info = src_volume->cut_info; + vol->cut_info.source_volume = src_volume->id(); } -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(); @@ -179,9 +179,8 @@ 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, - ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower, - const std::optional& saved_painting) +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 TriangleMesh upper_mesh, lower_mesh; @@ -190,27 +189,19 @@ static void process_solid_part_cut(ModelVolume* volume, const Transform3d& insta // Add required cut parts to the objects if (attributes.has(ModelObjectCutAttribute::KeepAsParts)) { - if (!upper_mesh.empty()) { - add_cut_volume(upper_mesh, upper, volume, cut_matrix, "_A"); - upper->volumes.back()->restore_painting(saved_painting); - } + add_cut_volume(upper_mesh, upper, volume, cut_matrix, "_A"); if (!lower_mesh.empty()) { add_cut_volume(lower_mesh, upper, volume, cut_matrix, "_B"); upper->volumes.back()->cut_info.is_from_upper = false; - upper->volumes.back()->restore_painting(saved_painting); } return; } - if (attributes.has(ModelObjectCutAttribute::KeepUpper) && !upper_mesh.empty()) { + if (attributes.has(ModelObjectCutAttribute::KeepUpper)) add_cut_volume(upper_mesh, upper, volume, cut_matrix); - upper->volumes.back()->restore_painting(saved_painting); - } - if (attributes.has(ModelObjectCutAttribute::KeepLower) && !lower_mesh.empty()) { + if (attributes.has(ModelObjectCutAttribute::KeepLower) && !lower_mesh.empty()) add_cut_volume(lower_mesh, lower, volume, cut_matrix); - lower->volumes.back()->restore_painting(saved_painting); - } } static void reset_instance_transformation(ModelObject* object, size_t src_instance_idx, @@ -259,8 +250,12 @@ Cut::Cut(const ModelObject* object, int instance, const Transform3d& cut_matrix, : m_instance(instance), m_cut_matrix(cut_matrix), m_attributes(attributes) { m_model = Model(); - if (object) - m_model.add_object(*object); + if (object) { + const auto obj = m_model.add_object(*object); + for (int i = 0; i < obj->volumes.size(); i++) { + obj->volumes[i]->cut_info.source_volume = object->volumes[i]->id(); + } + } } void Cut::post_process(ModelObject* object, ModelObjectPtrs& cut_object_ptrs, bool keep, bool place_on_cut, bool flip) @@ -291,7 +286,20 @@ void Cut::post_process(ModelObject* upper, ModelObject* lower, ModelObjectPtrs& void Cut::finalize(const ModelObjectPtrs& objects) { - //clear model from temporarry objects + for (const auto obj : objects) { + for (const auto v : obj->volumes) { + if (v->cut_info.source_volume.valid()) { + for (const auto src : m_model.objects.front()->volumes) { + if (src->id() == v->cut_info.source_volume) { + v->cut_info.source_volume = src->cut_info.source_volume; + break; + } + } + } + } + } + + //clear model from temporary objects m_model.clear_objects(); // add to model result objects @@ -350,8 +358,22 @@ const ModelObjectPtrs& Cut::perform_with_plane() else process_connector_cut(volume, instance_matrix, m_cut_matrix, m_attributes, upper, lower, dowels); } - else if (!volume->mesh().empty()) - process_solid_part_cut(volume, instance_matrix, m_cut_matrix, m_attributes, upper, lower, saved_painting); + else if (!volume->mesh().empty()) { + process_solid_part_cut(volume, instance_matrix, m_cut_matrix, m_attributes, upper, lower); + + // Apply paint to volumes cut from current volume + if (saved_painting) { + auto apply_paint = [&saved_painting, &volume](const ModelObject* obj) { + for (const auto v : obj->volumes) { + if (v->cut_info.source_volume == volume->id()) { + v->restore_painting(saved_painting); + } + } + }; + apply_paint(upper); + apply_paint(lower); + } + } } // Post-process cut parts diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index d8697adb41..eeec2783d3 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -825,6 +825,7 @@ public: CutConnectorType connector_type{ CutConnectorType::Plug }; float radius_tolerance{ 0.f };// [0.f : 1.f] float height_tolerance{ 0.f };// [0.f : 1.f] + ObjectID source_volume; // id of the volume from input model that this one is cut from CutInfo() = default; CutInfo(CutConnectorType type, float rad_tolerance, float h_tolerance, bool processed = false) : From f3fe37eab387533b5b1c3bf15cea5cd3df648799 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Sun, 10 May 2026 23:08:40 +0800 Subject: [PATCH 27/35] Remap paint after dovetail cut --- src/libslic3r/CutUtils.cpp | 71 +++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/CutUtils.cpp b/src/libslic3r/CutUtils.cpp index 9fc6f13c2a..fbc4d93c3e 100644 --- a/src/libslic3r/CutUtils.cpp +++ b/src/libslic3r/CutUtils.cpp @@ -451,15 +451,23 @@ static void merge_solid_parts_inside_object(ModelObjectPtrs& objects) for (ModelObject* mo : objects) { TriangleMesh mesh; // Merge all SolidPart but not Connectors + std::vector> saved_paintings; for (const ModelVolume* mv : mo->volumes) { if (mv->is_model_part() && !mv->is_cut_connector()) { TriangleMesh m = mv->mesh(); m.transform(mv->get_matrix()); mesh.merge(m); + saved_paintings.emplace_back(mv->save_painting()); + if (saved_paintings.back()) { + saved_paintings.back()->mesh = std::move(m); + } } } if (!mesh.empty()) { - ModelVolume* new_volume = mo->add_volume(mesh); + ModelVolume* new_volume = mo->add_volume(std::move(mesh)); + for (const auto& saved_painting : saved_paintings) { + new_volume->restore_painting(saved_painting, true); + } new_volume->name = mo->name; // Delete all merged SolidPart but not Connectors for (int i = int(mo->volumes.size()) - 2; i >= 0; --i) { @@ -583,6 +591,11 @@ const ModelObjectPtrs& Cut::perform_with_groove(const Groove& groove, const Tran tmp_model.add_object(*cut_mo); ModelObject* tmp_object = tmp_model.objects.front(); + // Store source volume id so we could track them back after the cut + for (int i = 0; i < cut_mo->volumes.size(); i++) { + tmp_object->volumes[i]->cut_info.source_volume = cut_mo->volumes[i]->id(); + } + auto add_volumes_from_cut = [](ModelObject* object, const ModelObjectCutAttribute attribute, const Model& tmp_model_for_cut) { const auto& volumes = tmp_model_for_cut.objects.front()->volumes; for (const ModelVolume* volume : volumes) @@ -603,6 +616,20 @@ const ModelObjectPtrs& Cut::perform_with_groove(const Groove& groove, const Tran tmp_model_for_cut.add_object(*cut.perform_with_plane().front()); assert(!tmp_model_for_cut.objects.empty()); + // Track back the volume id + for (const auto v : tmp_model_for_cut.objects.front()->volumes) { + auto& src_id = v->cut_info.source_volume; + if (src_id.valid()) { + // Find the src volume + for (const auto volume : object->volumes) { + if (src_id == volume->id()) { + src_id = volume->cut_info.source_volume; + break; + } + } + } + } + object->clear_volumes(); add_volumes_from_cut(object, add_volumes_attribute, tmp_model_for_cut); reset_instance_transformation(object, m_instance); @@ -663,6 +690,48 @@ const ModelObjectPtrs& Cut::perform_with_groove(const Groove& groove, const Tran // this part can be added to the upper object now add_volumes_from_cut(upper, ModelObjectCutAttribute::KeepLower, tmp_model_for_cut); + // Paint volumes + if (m_attributes.has(ModelObjectCutAttribute::KeepPaint)) { + const auto instance_matrix = cut_mo->instances[m_instance]->get_transformation().get_matrix_no_offset(); + std::map> saved_paintings; + auto get_painting = [&saved_paintings, &cut_mo, &instance_matrix](const ObjectID& volume_id) -> std::optional& { + const auto it = saved_paintings.find(volume_id); + if (it != saved_paintings.end()) { + return it->second; + } + + // Export paintings from volume + const auto volume = std::find_if(cut_mo->volumes.begin(), cut_mo->volumes.end(), + [&volume_id](const ModelVolume* v) { return volume_id == v->id(); }); + + std::optional saved_painting = (*volume)->save_painting(); + if (saved_painting) { + // Transform mesh to cut space (same transform as process_volume_cut applies) + const auto volume_matrix = (*volume)->get_matrix(); + saved_painting->mesh.transform(instance_matrix * volume_matrix, true); + } + + saved_paintings.emplace(volume_id, std::move(saved_painting)); + + return saved_paintings[volume_id]; + }; + + auto paint_volumes = [&get_painting](const ModelObject* object) { + for (const auto volume : object->volumes) { + if (volume->cut_info.source_volume.valid()) { + volume->restore_painting(get_painting(volume->cut_info.source_volume)); + } + } + }; + + if (m_attributes.has(ModelObjectCutAttribute::KeepUpper)) { + paint_volumes(upper); + } + if (m_attributes.has(ModelObjectCutAttribute::KeepLower)) { + paint_volumes(lower); + } + } + ModelObjectPtrs cut_object_ptrs; if (keep_as_parts) { From 89ab4d27cb2175cdce3e2218c7c2057da6914532 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Mon, 11 May 2026 14:19:03 +0800 Subject: [PATCH 28/35] Revert "Remap paint after dovetail cut" This reverts commit f3fe37eab387533b5b1c3bf15cea5cd3df648799. --- src/libslic3r/CutUtils.cpp | 71 +------------------------------------- 1 file changed, 1 insertion(+), 70 deletions(-) diff --git a/src/libslic3r/CutUtils.cpp b/src/libslic3r/CutUtils.cpp index fbc4d93c3e..9fc6f13c2a 100644 --- a/src/libslic3r/CutUtils.cpp +++ b/src/libslic3r/CutUtils.cpp @@ -451,23 +451,15 @@ static void merge_solid_parts_inside_object(ModelObjectPtrs& objects) for (ModelObject* mo : objects) { TriangleMesh mesh; // Merge all SolidPart but not Connectors - std::vector> saved_paintings; for (const ModelVolume* mv : mo->volumes) { if (mv->is_model_part() && !mv->is_cut_connector()) { TriangleMesh m = mv->mesh(); m.transform(mv->get_matrix()); mesh.merge(m); - saved_paintings.emplace_back(mv->save_painting()); - if (saved_paintings.back()) { - saved_paintings.back()->mesh = std::move(m); - } } } if (!mesh.empty()) { - ModelVolume* new_volume = mo->add_volume(std::move(mesh)); - for (const auto& saved_painting : saved_paintings) { - new_volume->restore_painting(saved_painting, true); - } + ModelVolume* new_volume = mo->add_volume(mesh); new_volume->name = mo->name; // Delete all merged SolidPart but not Connectors for (int i = int(mo->volumes.size()) - 2; i >= 0; --i) { @@ -591,11 +583,6 @@ const ModelObjectPtrs& Cut::perform_with_groove(const Groove& groove, const Tran tmp_model.add_object(*cut_mo); ModelObject* tmp_object = tmp_model.objects.front(); - // Store source volume id so we could track them back after the cut - for (int i = 0; i < cut_mo->volumes.size(); i++) { - tmp_object->volumes[i]->cut_info.source_volume = cut_mo->volumes[i]->id(); - } - auto add_volumes_from_cut = [](ModelObject* object, const ModelObjectCutAttribute attribute, const Model& tmp_model_for_cut) { const auto& volumes = tmp_model_for_cut.objects.front()->volumes; for (const ModelVolume* volume : volumes) @@ -616,20 +603,6 @@ const ModelObjectPtrs& Cut::perform_with_groove(const Groove& groove, const Tran tmp_model_for_cut.add_object(*cut.perform_with_plane().front()); assert(!tmp_model_for_cut.objects.empty()); - // Track back the volume id - for (const auto v : tmp_model_for_cut.objects.front()->volumes) { - auto& src_id = v->cut_info.source_volume; - if (src_id.valid()) { - // Find the src volume - for (const auto volume : object->volumes) { - if (src_id == volume->id()) { - src_id = volume->cut_info.source_volume; - break; - } - } - } - } - object->clear_volumes(); add_volumes_from_cut(object, add_volumes_attribute, tmp_model_for_cut); reset_instance_transformation(object, m_instance); @@ -690,48 +663,6 @@ const ModelObjectPtrs& Cut::perform_with_groove(const Groove& groove, const Tran // this part can be added to the upper object now add_volumes_from_cut(upper, ModelObjectCutAttribute::KeepLower, tmp_model_for_cut); - // Paint volumes - if (m_attributes.has(ModelObjectCutAttribute::KeepPaint)) { - const auto instance_matrix = cut_mo->instances[m_instance]->get_transformation().get_matrix_no_offset(); - std::map> saved_paintings; - auto get_painting = [&saved_paintings, &cut_mo, &instance_matrix](const ObjectID& volume_id) -> std::optional& { - const auto it = saved_paintings.find(volume_id); - if (it != saved_paintings.end()) { - return it->second; - } - - // Export paintings from volume - const auto volume = std::find_if(cut_mo->volumes.begin(), cut_mo->volumes.end(), - [&volume_id](const ModelVolume* v) { return volume_id == v->id(); }); - - std::optional saved_painting = (*volume)->save_painting(); - if (saved_painting) { - // Transform mesh to cut space (same transform as process_volume_cut applies) - const auto volume_matrix = (*volume)->get_matrix(); - saved_painting->mesh.transform(instance_matrix * volume_matrix, true); - } - - saved_paintings.emplace(volume_id, std::move(saved_painting)); - - return saved_paintings[volume_id]; - }; - - auto paint_volumes = [&get_painting](const ModelObject* object) { - for (const auto volume : object->volumes) { - if (volume->cut_info.source_volume.valid()) { - volume->restore_painting(get_painting(volume->cut_info.source_volume)); - } - } - }; - - if (m_attributes.has(ModelObjectCutAttribute::KeepUpper)) { - paint_volumes(upper); - } - if (m_attributes.has(ModelObjectCutAttribute::KeepLower)) { - paint_volumes(lower); - } - } - ModelObjectPtrs cut_object_ptrs; if (keep_as_parts) { From 4aeb6a31db561e13ea6cdb2d8eae19b07132e900 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Mon, 11 May 2026 14:19:08 +0800 Subject: [PATCH 29/35] Revert "Refactor planar cut code so it can be reused by other cut type" This reverts commit a0d1ccaf28262b1e5e7e2f6e8235fa17482bde50. --- src/libslic3r/CutUtils.cpp | 64 +++++++++++++------------------------- src/libslic3r/Model.hpp | 1 - 2 files changed, 21 insertions(+), 44 deletions(-) diff --git a/src/libslic3r/CutUtils.cpp b/src/libslic3r/CutUtils.cpp index 9fc6f13c2a..7fc21bb3b4 100644 --- a/src/libslic3r/CutUtils.cpp +++ b/src/libslic3r/CutUtils.cpp @@ -8,6 +8,7 @@ #include "ObjectID.hpp" #include +#include namespace Slic3r { @@ -61,10 +62,9 @@ static void add_cut_volume(TriangleMesh& mesh, ModelObject* object, const ModelV assert(vol->config.id() != src_volume->config.id()); vol->set_material(src_volume->material_id(), *src_volume->material()); vol->cut_info = src_volume->cut_info; - vol->cut_info.source_volume = src_volume->id(); } -static void process_volume_cut( const ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& cut_matrix, +static void process_volume_cut( 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(); @@ -179,8 +179,9 @@ static void process_modifier_cut(ModelVolume* volume, const Transform3d& instanc lower->add_volume(*volume); } -static void process_solid_part_cut(const ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& cut_matrix, - ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower) +static void process_solid_part_cut(ModelVolume* volume, const Transform3d& instance_matrix, const Transform3d& cut_matrix, + ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower, + const std::optional& saved_painting) { // Perform cut TriangleMesh upper_mesh, lower_mesh; @@ -189,19 +190,27 @@ static void process_solid_part_cut(const ModelVolume* volume, const Transform3d& // Add required cut parts to the objects if (attributes.has(ModelObjectCutAttribute::KeepAsParts)) { - add_cut_volume(upper_mesh, upper, volume, cut_matrix, "_A"); + if (!upper_mesh.empty()) { + add_cut_volume(upper_mesh, upper, volume, cut_matrix, "_A"); + upper->volumes.back()->restore_painting(saved_painting); + } if (!lower_mesh.empty()) { add_cut_volume(lower_mesh, upper, volume, cut_matrix, "_B"); upper->volumes.back()->cut_info.is_from_upper = false; + upper->volumes.back()->restore_painting(saved_painting); } return; } - if (attributes.has(ModelObjectCutAttribute::KeepUpper)) + if (attributes.has(ModelObjectCutAttribute::KeepUpper) && !upper_mesh.empty()) { add_cut_volume(upper_mesh, upper, volume, cut_matrix); + upper->volumes.back()->restore_painting(saved_painting); + } - if (attributes.has(ModelObjectCutAttribute::KeepLower) && !lower_mesh.empty()) + if (attributes.has(ModelObjectCutAttribute::KeepLower) && !lower_mesh.empty()) { add_cut_volume(lower_mesh, lower, volume, cut_matrix); + lower->volumes.back()->restore_painting(saved_painting); + } } static void reset_instance_transformation(ModelObject* object, size_t src_instance_idx, @@ -250,12 +259,8 @@ Cut::Cut(const ModelObject* object, int instance, const Transform3d& cut_matrix, : m_instance(instance), m_cut_matrix(cut_matrix), m_attributes(attributes) { m_model = Model(); - if (object) { - const auto obj = m_model.add_object(*object); - for (int i = 0; i < obj->volumes.size(); i++) { - obj->volumes[i]->cut_info.source_volume = object->volumes[i]->id(); - } - } + if (object) + m_model.add_object(*object); } void Cut::post_process(ModelObject* object, ModelObjectPtrs& cut_object_ptrs, bool keep, bool place_on_cut, bool flip) @@ -286,20 +291,7 @@ void Cut::post_process(ModelObject* upper, ModelObject* lower, ModelObjectPtrs& void Cut::finalize(const ModelObjectPtrs& objects) { - for (const auto obj : objects) { - for (const auto v : obj->volumes) { - if (v->cut_info.source_volume.valid()) { - for (const auto src : m_model.objects.front()->volumes) { - if (src->id() == v->cut_info.source_volume) { - v->cut_info.source_volume = src->cut_info.source_volume; - break; - } - } - } - } - } - - //clear model from temporary objects + //clear model from temporarry objects m_model.clear_objects(); // add to model result objects @@ -358,22 +350,8 @@ const ModelObjectPtrs& Cut::perform_with_plane() else process_connector_cut(volume, instance_matrix, m_cut_matrix, m_attributes, upper, lower, dowels); } - else if (!volume->mesh().empty()) { - process_solid_part_cut(volume, instance_matrix, m_cut_matrix, m_attributes, upper, lower); - - // Apply paint to volumes cut from current volume - if (saved_painting) { - auto apply_paint = [&saved_painting, &volume](const ModelObject* obj) { - for (const auto v : obj->volumes) { - if (v->cut_info.source_volume == volume->id()) { - v->restore_painting(saved_painting); - } - } - }; - apply_paint(upper); - apply_paint(lower); - } - } + else if (!volume->mesh().empty()) + process_solid_part_cut(volume, instance_matrix, m_cut_matrix, m_attributes, upper, lower, saved_painting); } // Post-process cut parts diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index eeec2783d3..d8697adb41 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -825,7 +825,6 @@ public: CutConnectorType connector_type{ CutConnectorType::Plug }; float radius_tolerance{ 0.f };// [0.f : 1.f] float height_tolerance{ 0.f };// [0.f : 1.f] - ObjectID source_volume; // id of the volume from input model that this one is cut from CutInfo() = default; CutInfo(CutConnectorType type, float rad_tolerance, float h_tolerance, bool processed = false) : From 66f8d954af8babd5de33e3ddd448e78dd1c8f7b3 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Mon, 11 May 2026 14:29:17 +0800 Subject: [PATCH 30/35] Simplify cut repaint code to make it more generic --- src/libslic3r/CutUtils.cpp | 60 ++++++++++++++++++++------------------ src/libslic3r/CutUtils.hpp | 2 +- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/src/libslic3r/CutUtils.cpp b/src/libslic3r/CutUtils.cpp index 7fc21bb3b4..35be5246de 100644 --- a/src/libslic3r/CutUtils.cpp +++ b/src/libslic3r/CutUtils.cpp @@ -8,7 +8,6 @@ #include "ObjectID.hpp" #include -#include namespace Slic3r { @@ -64,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(); @@ -179,9 +178,8 @@ 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, - ModelObjectCutAttributes attributes, ModelObject* upper, ModelObject* lower, - const std::optional& saved_painting) +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 TriangleMesh upper_mesh, lower_mesh; @@ -190,27 +188,19 @@ static void process_solid_part_cut(ModelVolume* volume, const Transform3d& insta // Add required cut parts to the objects if (attributes.has(ModelObjectCutAttribute::KeepAsParts)) { - if (!upper_mesh.empty()) { - add_cut_volume(upper_mesh, upper, volume, cut_matrix, "_A"); - upper->volumes.back()->restore_painting(saved_painting); - } + add_cut_volume(upper_mesh, upper, volume, cut_matrix, "_A"); if (!lower_mesh.empty()) { add_cut_volume(lower_mesh, upper, volume, cut_matrix, "_B"); upper->volumes.back()->cut_info.is_from_upper = false; - upper->volumes.back()->restore_painting(saved_painting); } return; } - if (attributes.has(ModelObjectCutAttribute::KeepUpper) && !upper_mesh.empty()) { + if (attributes.has(ModelObjectCutAttribute::KeepUpper)) add_cut_volume(upper_mesh, upper, volume, cut_matrix); - upper->volumes.back()->restore_painting(saved_painting); - } - if (attributes.has(ModelObjectCutAttribute::KeepLower) && !lower_mesh.empty()) { + if (attributes.has(ModelObjectCutAttribute::KeepLower) && !lower_mesh.empty()) add_cut_volume(lower_mesh, lower, volume, cut_matrix); - lower->volumes.back()->restore_painting(saved_painting); - } } static void reset_instance_transformation(ModelObject* object, size_t src_instance_idx, @@ -289,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>& 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 @@ -330,15 +333,14 @@ 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> saved_paintings; for (ModelVolume* volume : mo->volumes) { // Save painting data before reset_extra_facets() discards it. - std::optional saved_painting; if (m_attributes.has(ModelObjectCutAttribute::KeepPaint)) { - saved_painting = volume->save_painting(); - if (saved_painting) { + saved_paintings.emplace_back(volume->save_painting()); + if (saved_paintings.back()) { // Transform mesh to cut space (same transform as process_volume_cut applies) - const auto volume_matrix = volume->get_matrix(); - saved_painting->mesh.transform(instance_matrix * volume_matrix, true); + saved_paintings.back()->mesh.transform(instance_matrix * volume->get_matrix(), true); } } @@ -351,7 +353,7 @@ const ModelObjectPtrs& Cut::perform_with_plane() process_connector_cut(volume, instance_matrix, m_cut_matrix, m_attributes, upper, lower, dowels); } else if (!volume->mesh().empty()) - process_solid_part_cut(volume, instance_matrix, m_cut_matrix, m_attributes, upper, lower, saved_painting); + process_solid_part_cut(volume, instance_matrix, m_cut_matrix, m_attributes, upper, lower); } // Post-process cut parts @@ -397,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; } @@ -496,7 +498,7 @@ const ModelObjectPtrs& Cut::perform_by_contour(std::vector 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, {}); } else if (volumes.size() > cut_parts_cnt) { // Means that object is cut with connectors @@ -527,7 +529,7 @@ const ModelObjectPtrs& Cut::perform_by_contour(std::vector 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, {}); // Add Dowel-connectors as separate objects to model if (cut_connectors_obj.size() >= 3) @@ -679,7 +681,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, {}); return m_model.objects; } diff --git a/src/libslic3r/CutUtils.hpp b/src/libslic3r/CutUtils.hpp index f39007c8a9..d7e486beaf 100644 --- a/src/libslic3r/CutUtils.hpp +++ b/src/libslic3r/CutUtils.hpp @@ -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>& saved_paintings); public: From b9c9f42f013dc680e68d80f4bc3a7f78fc408a1e Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Mon, 11 May 2026 14:53:53 +0800 Subject: [PATCH 31/35] Remap paint after dovetail cut --- src/libslic3r/CutUtils.cpp | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/libslic3r/CutUtils.cpp b/src/libslic3r/CutUtils.cpp index 35be5246de..d192a854bb 100644 --- a/src/libslic3r/CutUtils.cpp +++ b/src/libslic3r/CutUtils.cpp @@ -555,6 +555,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> 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(); @@ -681,7 +695,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; } From ea5119ec3f89af6334d8ec3723fb87456d5a277d Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Mon, 11 May 2026 15:33:09 +0800 Subject: [PATCH 32/35] Remap paint after contour cut (cut with part assigned to other side using right mouse click) --- src/libslic3r/CutUtils.cpp | 19 ++++++++++++++++--- src/libslic3r/CutUtils.hpp | 2 +- src/slic3r/GUI/Gizmos/GLGizmoCut.cpp | 4 ++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/libslic3r/CutUtils.cpp b/src/libslic3r/CutUtils.cpp index d192a854bb..5899313035 100644 --- a/src/libslic3r/CutUtils.cpp +++ b/src/libslic3r/CutUtils.cpp @@ -454,7 +454,7 @@ static void merge_solid_parts_inside_object(ModelObjectPtrs& objects) } -const ModelObjectPtrs& Cut::perform_by_contour(std::vector parts, int dowels_count) +const ModelObjectPtrs& Cut::perform_by_contour(const ModelObject* src_object, std::vector parts, int dowels_count) { ModelObject* cut_mo = m_model.objects.front(); @@ -469,6 +469,19 @@ const ModelObjectPtrs& Cut::perform_by_contour(std::vector parts, int dowe lower->name = lower->name + "_B"; } + // Save painting data so we later can remap it. + std::vector> 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; @@ -498,7 +511,7 @@ const ModelObjectPtrs& Cut::perform_by_contour(std::vector 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 @@ -529,7 +542,7 @@ const ModelObjectPtrs& Cut::perform_by_contour(std::vector 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) diff --git a/src/libslic3r/CutUtils.hpp b/src/libslic3r/CutUtils.hpp index d7e486beaf..99cdb9cd65 100644 --- a/src/libslic3r/CutUtils.hpp +++ b/src/libslic3r/CutUtils.hpp @@ -56,7 +56,7 @@ public: }; const ModelObjectPtrs& perform_with_plane(); - const ModelObjectPtrs& perform_by_contour(std::vector parts, int dowels_count); + const ModelObjectPtrs& perform_by_contour(const ModelObject* src_object, std::vector parts, int dowels_count); const ModelObjectPtrs& perform_with_groove(const Groove& groove, const Transform3d& rotation_m, bool keep_as_parts = false); }; // namespace Cut diff --git a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp index da523788b8..f042656ab7 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoCut.cpp @@ -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, false); // TODO: fix this + 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) { @@ -3337,7 +3337,7 @@ void GLGizmoCut3D::perform_cut(const Selection& selection) 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(); From 3cbdb092d6e2de0d9f02c44261ddbcff0ddfa757 Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Mon, 11 May 2026 17:00:31 +0800 Subject: [PATCH 33/35] Always clear existing facets first if not kept --- src/libslic3r/Model.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 000ab997a9..0067684f15 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -1997,6 +1997,10 @@ std::optional ModelVolume::save_painting() cons void ModelVolume::restore_painting(const std::optional& saved, const bool keep_existing_paint) { + if (!keep_existing_paint) { + reset_extra_facets(); + } + if (!saved) { return; } From b4b29821069393d537a5c374f272ed714afda36a Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Mon, 11 May 2026 17:00:54 +0800 Subject: [PATCH 34/35] Remap paint after simplify --- src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp index 1f8af3bf09..95936556d4 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoSimplify.cpp @@ -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 saved_painting = keep_painting ? mv->save_painting() : + std::optional{}; 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(); From 5b1766b1d59e93e532c70dad901239191c8a664b Mon Sep 17 00:00:00 2001 From: Noisyfox Date: Mon, 11 May 2026 17:16:36 +0800 Subject: [PATCH 35/35] Remap paint after smooth --- src/slic3r/GUI/GUI_ObjectList.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/slic3r/GUI/GUI_ObjectList.cpp b/src/slic3r/GUI/GUI_ObjectList.cpp index 93e5c94432..3cee3d2058 100644 --- a/src/slic3r/GUI/GUI_ObjectList.cpp +++ b/src/slic3r/GUI/GUI_ObjectList.cpp @@ -6210,6 +6210,7 @@ void GUI::ObjectList::smooth_mesh() WarningDialog dlg(static_cast(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); @@ -6221,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 saved_painting = keep_painting ? + mv->save_painting() : + std::optional{}; 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(); @@ -6247,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 saved_painting = keep_painting ? + mv->save_painting() : + std::optional{}; 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();