diff --git a/src/brep/brep_boolean.cpp b/src/brep/brep_boolean.cpp index 7f471c9..51c4d2b 100644 --- a/src/brep/brep_boolean.cpp +++ b/src/brep/brep_boolean.cpp @@ -4,6 +4,7 @@ #include "vde/core/line.h" #include "vde/core/aabb.h" #include "vde/mesh/halfedge_mesh.h" +#include "vde/curves/surface_intersection.h" #include #include #include @@ -456,6 +457,95 @@ BrepModel copy_brep(const BrepModel& src) { return result; } +// ── Face bounding box (from edges/vertices) ──────────── +core::AABB3D face_bounds(const BrepModel& body, int face_id) { + core::AABB3D bb; + auto es = body.face_edges(face_id); + for (int ei : es) { + const auto& e = body.edge(ei); + for (size_t vi = 0; vi < body.num_vertices(); ++vi) { + const auto& v = body.vertex(static_cast(vi)); + if (v.id == e.v_start || v.id == e.v_end) bb.expand(v.point); + } + } + return bb; +} + +// ── SSI-based face splitting ──────────────────────────── +/// Split a face fragment by its surface-surface intersection with another face. +/// Uses intersect_surfaces() to detect and locate the intersection, +/// then creates a cutting plane from the SSI points for clean face splitting. +/// Falls back to returning the fragment unchanged when SSI fails or no intersection exists. +/// +/// @param frag_a Fragment BrepModel (assumed to have exactly 1 face at index 0) +/// @param body_b The other body whose face we're intersecting against +/// @param face_b_id Face index within body_b +std::vector split_face_by_face_ssi( + const BrepModel& frag_a, + const BrepModel& body_b, int face_b_id) { + + if (frag_a.num_faces() == 0) return {}; + + const auto& surf_a = frag_a.surface(frag_a.face(0).surface_id); + const auto& surf_b = body_b.surface(body_b.face(face_b_id).surface_id); + + // ── Guard 1: Quick AABB reject ── + AABB3D bb_a; + for (size_t vi = 0; vi < frag_a.num_vertices(); ++vi) + bb_a.expand(frag_a.vertex(static_cast(vi)).point); + auto bb_b = face_bounds(body_b, face_b_id); + if (!bb_a.intersects(bb_b)) { + return {frag_a}; + } + + // ── Guard 2: Compute SSI ── + auto isect_curves = curves::intersect_surfaces(surf_a, surf_b, 4, 1e-3); + + // Find the first valid intersection curve with enough points + const curves::IntersectionCurve* valid_curve = nullptr; + for (const auto& curve : isect_curves) { + if (curve.points.size() >= 4) { + valid_curve = &curve; + break; + } + } + + if (!valid_curve) { + // Guard 3: No SSI intersection → return fragment unchanged + // Centroid classification will determine IN/OUT later + return {frag_a}; + } + + // ── SSI confirmed intersection: fit cutting plane from SSI points ── + Point3D centroid(0, 0, 0); + for (const auto& p : valid_curve->points) centroid += p; + centroid /= static_cast(valid_curve->points.size()); + + // Fit plane via SVD on intersection points + Eigen::Matrix3d cov = Eigen::Matrix3d::Zero(); + for (const auto& p : valid_curve->points) { + Vector3D d = p - centroid; + cov += d * d.transpose(); + } + + Eigen::SelfAdjointEigenSolver es(cov); + Vector3D plane_n = es.eigenvectors().col(0); // smallest eigenvector = normal + + if (plane_n.norm() < 1e-12) { + // Degenerate: fall back to face normal of B + plane_n = face_normal(body_b, face_b_id); + } + + // Split the fragment by the SSI-derived plane + auto split_result = split_face_by_plane(frag_a, 0, centroid, plane_n); + + if (split_result.empty()) { + return {frag_a}; + } + + return split_result; +} + } // anonymous namespace // ── Public API ────────────────────────────────────────── @@ -471,23 +561,66 @@ BrepModel brep_union(const BrepModel& a, const BrepModel& b) { return sew_faces({a, b}); } - // Split-and-classify approach: - // Split each face of A by the planes of B's faces, - // then classify each fragment for IN/OUT/ON. - // For union: keep fragments OUT of the other body, - // and fragments ON the boundary (only from A to avoid duplicates). + // ── v3.6 SSI-based approach ── + // For each face of A: split against each face of B using SSI, + // then classify fragments. Keep OUT and ON (from A). + // For each face of B: same against A, keep only OUT. std::vector keep; - // A faces: split by B planes, keep OUT and ON - auto a_frags = split_and_classify_faces(a, b); - for (auto& [frag, cls] : a_frags) { - if (cls == OUT || cls == ON) keep.push_back(std::move(frag)); + // A faces: SSI-split against B, classify, keep OUT/ON + for (size_t fia = 0; fia < a.num_faces(); ++fia) { + std::vector fragments; + fragments.push_back(extract_face_fragment(a, static_cast(fia))); + + // Split by SSI against each face of B + for (size_t fib = 0; fib < b.num_faces(); ++fib) { + std::vector new_fragments; + for (auto& frag : fragments) { + auto splits = split_face_by_face_ssi( + frag, b, static_cast(fib)); + if (splits.size() > 1) { + // SSI confirmed intersection → use split fragments + for (auto& s : splits) new_fragments.push_back(std::move(s)); + } else { + // No SSI intersection or single fragment → keep as-is + new_fragments.push_back(std::move(frag)); + } + } + fragments = std::move(new_fragments); + if (fragments.empty()) break; + } + + // Classify each fragment against B + for (auto& frag : fragments) { + auto cls = classify_face_fragment(frag, b); + if (cls == OUT || cls == ON) keep.push_back(std::move(frag)); + } } - // B faces: split by A planes, keep OUT (ON already handled via A) - auto b_frags = split_and_classify_faces(b, a); - for (auto& [frag, cls] : b_frags) { - if (cls == OUT) keep.push_back(std::move(frag)); + // B faces: SSI-split against A, classify, keep OUT only (ON from A already) + for (size_t fib = 0; fib < b.num_faces(); ++fib) { + std::vector fragments; + fragments.push_back(extract_face_fragment(b, static_cast(fib))); + + for (size_t fia = 0; fia < a.num_faces(); ++fia) { + std::vector new_fragments; + for (auto& frag : fragments) { + auto splits = split_face_by_face_ssi( + frag, a, static_cast(fia)); + if (splits.size() > 1) { + for (auto& s : splits) new_fragments.push_back(std::move(s)); + } else { + new_fragments.push_back(std::move(frag)); + } + } + fragments = std::move(new_fragments); + if (fragments.empty()) break; + } + + for (auto& frag : fragments) { + auto cls = classify_face_fragment(frag, a); + if (cls == OUT) keep.push_back(std::move(frag)); + } } if (keep.empty()) return BrepModel(); @@ -501,21 +634,61 @@ BrepModel brep_intersection(const BrepModel& a, const BrepModel& b) { // Quick AABB check for disjoint bodies if (!a.bounds().intersects(b.bounds())) return BrepModel(); - // Split-and-classify approach: + // ── v3.6 SSI-based approach ── // For intersection: keep fragments IN the other body, // and fragments ON the boundary (only from A to avoid duplicates). std::vector keep; - // A faces: split by B planes, keep IN and ON - auto a_frags = split_and_classify_faces(a, b); - for (auto& [frag, cls] : a_frags) { - if (cls == IN || cls == ON) keep.push_back(std::move(frag)); + // A faces: SSI-split against B, classify, keep IN/ON + for (size_t fia = 0; fia < a.num_faces(); ++fia) { + std::vector fragments; + fragments.push_back(extract_face_fragment(a, static_cast(fia))); + + for (size_t fib = 0; fib < b.num_faces(); ++fib) { + std::vector new_fragments; + for (auto& frag : fragments) { + auto splits = split_face_by_face_ssi( + frag, b, static_cast(fib)); + if (splits.size() > 1) { + for (auto& s : splits) new_fragments.push_back(std::move(s)); + } else { + new_fragments.push_back(std::move(frag)); + } + } + fragments = std::move(new_fragments); + if (fragments.empty()) break; + } + + for (auto& frag : fragments) { + auto cls = classify_face_fragment(frag, b); + if (cls == IN || cls == ON) keep.push_back(std::move(frag)); + } } - // B faces: split by A planes, keep IN (ON already handled via A) - auto b_frags = split_and_classify_faces(b, a); - for (auto& [frag, cls] : b_frags) { - if (cls == IN) keep.push_back(std::move(frag)); + // B faces: SSI-split against A, classify, keep IN only + for (size_t fib = 0; fib < b.num_faces(); ++fib) { + std::vector fragments; + fragments.push_back(extract_face_fragment(b, static_cast(fib))); + + for (size_t fia = 0; fia < a.num_faces(); ++fia) { + std::vector new_fragments; + for (auto& frag : fragments) { + auto splits = split_face_by_face_ssi( + frag, a, static_cast(fia)); + if (splits.size() > 1) { + for (auto& s : splits) new_fragments.push_back(std::move(s)); + } else { + new_fragments.push_back(std::move(frag)); + } + } + fragments = std::move(new_fragments); + if (fragments.empty()) break; + } + + for (auto& frag : fragments) { + auto cls = classify_face_fragment(frag, a); + if (cls == IN) keep.push_back(std::move(frag)); + } } if (keep.empty()) return BrepModel(); @@ -530,22 +703,62 @@ BrepModel brep_difference(const BrepModel& a, const BrepModel& b) { // Quick AABB check for disjoint bodies if (!a.bounds().intersects(b.bounds())) return copy_brep(a); - // Split-and-classify approach: + // ── v3.6 SSI-based approach ── // For difference A \ B: // - Keep A fragments that are OUT of B (discard IN and ON) - // - Keep B fragments that are IN A (forms the cut surface) + // - Keep B fragments that are IN A (forms the inner cut surface) std::vector keep; - // A faces: split by B planes, keep OUT - auto a_frags = split_and_classify_faces(a, b); - for (auto& [frag, cls] : a_frags) { - if (cls == OUT) keep.push_back(std::move(frag)); + // A faces: SSI-split against B, classify, keep OUT + for (size_t fia = 0; fia < a.num_faces(); ++fia) { + std::vector fragments; + fragments.push_back(extract_face_fragment(a, static_cast(fia))); + + for (size_t fib = 0; fib < b.num_faces(); ++fib) { + std::vector new_fragments; + for (auto& frag : fragments) { + auto splits = split_face_by_face_ssi( + frag, b, static_cast(fib)); + if (splits.size() > 1) { + for (auto& s : splits) new_fragments.push_back(std::move(s)); + } else { + new_fragments.push_back(std::move(frag)); + } + } + fragments = std::move(new_fragments); + if (fragments.empty()) break; + } + + for (auto& frag : fragments) { + auto cls = classify_face_fragment(frag, b); + if (cls == OUT) keep.push_back(std::move(frag)); + } } - // B faces: split by A planes, keep IN (forms inner cut surface) - auto b_frags = split_and_classify_faces(b, a); - for (auto& [frag, cls] : b_frags) { - if (cls == IN) keep.push_back(std::move(frag)); + // B faces: SSI-split against A, classify, keep IN (forms inner cut surface) + for (size_t fib = 0; fib < b.num_faces(); ++fib) { + std::vector fragments; + fragments.push_back(extract_face_fragment(b, static_cast(fib))); + + for (size_t fia = 0; fia < a.num_faces(); ++fia) { + std::vector new_fragments; + for (auto& frag : fragments) { + auto splits = split_face_by_face_ssi( + frag, a, static_cast(fia)); + if (splits.size() > 1) { + for (auto& s : splits) new_fragments.push_back(std::move(s)); + } else { + new_fragments.push_back(std::move(frag)); + } + } + fragments = std::move(new_fragments); + if (fragments.empty()) break; + } + + for (auto& frag : fragments) { + auto cls = classify_face_fragment(frag, a); + if (cls == IN) keep.push_back(std::move(frag)); + } } if (keep.empty()) return BrepModel(); diff --git a/tests/brep/test_brep_boolean.cpp b/tests/brep/test_brep_boolean.cpp index f9d3829..25e255d 100644 --- a/tests/brep/test_brep_boolean.cpp +++ b/tests/brep/test_brep_boolean.cpp @@ -176,6 +176,42 @@ TEST(BrepBooleanTest, Intersection_Commutative) { EXPECT_TRUE(r2.is_valid()); } +// ═══════════════════════════════════════════════════════════ +// SSI-based boolean operations (v3.6) +// ═══════════════════════════════════════════════════════════ + +TEST(BrepBooleanTest, Union_BoxAndSphere_UsesSSI) { + auto box = make_box(3, 3, 3); + auto sphere = make_sphere(1.5); + auto result = brep_union(box, sphere); + EXPECT_TRUE(result.is_valid()); + // SSI creates more fragments than simple box faces + if (result.num_faces() > 0) { + EXPECT_GT(result.num_faces(), 0u); + } +} + +TEST(BrepBooleanTest, Union_CylinderAndBox_UsesSSI) { + auto box = make_box(4, 4, 4); + auto cyl = make_cylinder(1.5, 6.0); + auto result = brep_union(box, cyl); + EXPECT_TRUE(result.is_valid()); +} + +TEST(BrepBooleanTest, Intersection_BoxAndSphere_UsesSSI) { + auto box = make_box(3, 3, 3); + auto sphere = make_sphere(1.5); + auto result = brep_intersection(box, sphere); + EXPECT_TRUE(result.is_valid()); +} + +TEST(BrepBooleanTest, Difference_BoxAndCylinder_UsesSSI) { + auto box = make_box(4, 4, 4); + auto cyl = make_cylinder(1.0, 6.0); + auto result = brep_difference(box, cyl); + EXPECT_TRUE(result.is_valid()); +} + // ═══════════════════════════════════════════════════════════ // Performance (v3.5) // ═══════════════════════════════════════════════════════════