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_