From 212c6a02c58d207baa447851ecfa6e620be82097 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8C=82=E4=B9=8B=E9=92=B3?= Date: Fri, 24 Jul 2026 13:20:17 +0000 Subject: [PATCH] feat(v3.4): integrate face splitting into B-Rep boolean operations Replace centroid-only classification (classify_point_mesh on full face fragments) with split-and-classify approach using split_face_by_plane() from v3.3: - Add include for brep_face_split.h - Add face_normal() helper (extracts surface normal at parametric mid-point) - Add split_and_classify_faces(): splits each face of body A by the planes of every face in body B, then classifies each resulting fragment individually - Refactor brep_union, brep_intersection, brep_difference to use the new split-and-classify pipeline - Add test: Union_SelfUnion_WithSplitting This produces more precise boolean results by ensuring face fragments that straddle the boundary are properly split before classification. --- src/brep/brep_boolean.cpp | 114 ++++++++++++++++++++----------- tests/brep/test_brep_boolean.cpp | 11 +++ 2 files changed, 86 insertions(+), 39 deletions(-) diff --git a/src/brep/brep_boolean.cpp b/src/brep/brep_boolean.cpp index caa6198..128db97 100644 --- a/src/brep/brep_boolean.cpp +++ b/src/brep/brep_boolean.cpp @@ -1,4 +1,5 @@ #include "vde/brep/brep_boolean.h" +#include "vde/brep/brep_face_split.h" #include "vde/core/plane.h" #include "vde/core/line.h" #include "vde/core/aabb.h" @@ -198,6 +199,15 @@ Plane3D face_plane(const BrepModel& body, int face_id) { return Plane3D(p, n); } +// ── Face normal from surface ─────────────────────────── +Vector3D face_normal(const BrepModel& body, int face_id) { + const auto& face = body.face(face_id); + const auto& surf = body.surface(face.surface_id); + Vector3D n = surf.normal(0.5, 0.5); + if (n.norm() < 1e-12) n = Vector3D::UnitZ(); + return n; +} + // ── Compute face centroid ─────────────────────────────── Point3D face_centroid(const BrepModel& body, int face_id) { auto edges = body.face_edges(face_id); @@ -356,6 +366,41 @@ BrepModel extract_face_fragment(const BrepModel& body, int face_id) { return frag; } +// ── Split and classify faces by face-plane cutting ───── +/// Split each face of body_a by the planes of body_b's faces, +/// then classify each resulting fragment against body_b. +/// Returns vector of (fragment, classification) pairs. +std::vector> +split_and_classify_faces(const BrepModel& a, const BrepModel& b) { + std::vector> result; + + for (size_t fi = 0; fi < a.num_faces(); ++fi) { + std::vector current; + current.push_back(extract_face_fragment(a, static_cast(fi))); + + // Split by each face plane of B + for (size_t bfi = 0; bfi < b.num_faces(); ++bfi) { + Point3D plane_pt = face_centroid(b, static_cast(bfi)); + Vector3D plane_n = face_normal(b, static_cast(bfi)); + + std::vector next; + for (auto& frag : current) { + auto splits = split_face_by_plane(frag, 0, plane_pt, plane_n); + for (auto& s : splits) next.push_back(std::move(s)); + } + current = std::move(next); + if (current.empty()) break; // all fragments eliminated + } + + // Classify each remaining fragment + for (auto& frag : current) { + auto cls = classify_face_fragment(frag, b); + result.emplace_back(std::move(frag), cls); + } + } + return result; +} + /// Create a deep copy of a BrepModel, preserving all topology exactly. BrepModel copy_brep(const BrepModel& src) { BrepModel result; @@ -409,22 +454,22 @@ BrepModel brep_union(const BrepModel& a, const BrepModel& b) { return sew_faces({a, b}); } - // Full classification-based approach - // For union: keep faces that are OUT of the other body, - // and faces ON the boundary (only from A to avoid duplicates) + // 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). std::vector keep; - // A faces: keep those OUT of B, and those ON B - for (size_t fi = 0; fi < a.num_faces(); ++fi) { - auto frag = extract_face_fragment(a, static_cast(fi)); - auto cls = classify_face_fragment(frag, b); + // 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)); } - // B faces: keep those OUT of A (ON faces already handled via A) - for (size_t fi = 0; fi < b.num_faces(); ++fi) { - auto frag = extract_face_fragment(b, static_cast(fi)); - auto cls = classify_face_fragment(frag, a); + // 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)); } @@ -439,21 +484,20 @@ BrepModel brep_intersection(const BrepModel& a, const BrepModel& b) { // Quick AABB check for disjoint bodies if (!a.bounds().intersects(b.bounds())) return BrepModel(); - // For intersection: keep faces IN the other body, - // and faces ON the boundary (only from A to avoid duplicates) + // Split-and-classify 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 IN B or ON B - for (size_t fi = 0; fi < a.num_faces(); ++fi) { - auto frag = extract_face_fragment(a, static_cast(fi)); - auto cls = classify_face_fragment(frag, b); + // 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)); } - // B faces IN A (ON faces already handled via A) - for (size_t fi = 0; fi < b.num_faces(); ++fi) { - auto frag = extract_face_fragment(b, static_cast(fi)); - auto cls = classify_face_fragment(frag, a); + // 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)); } @@ -469,30 +513,22 @@ 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: // For difference A \ B: - // - Keep A faces that are OUT of B (discard IN and ON) - // - Keep B faces that are IN A (reversed, forms the cut surface) + // - Keep A fragments that are OUT of B (discard IN and ON) + // - Keep B fragments that are IN A (forms the cut surface) std::vector keep; - // A faces OUT of B - for (size_t fi = 0; fi < a.num_faces(); ++fi) { - auto frag = extract_face_fragment(a, static_cast(fi)); - auto cls = classify_face_fragment(frag, b); + // 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)); } - // B faces IN A (reversed — these form the inner cut surface) - for (size_t fi = 0; fi < b.num_faces(); ++fi) { - auto frag = extract_face_fragment(b, static_cast(fi)); - auto cls = classify_face_fragment(frag, a); - if (cls == IN) { - // Mark all faces in this fragment as reversed - for (size_t ffi = 0; ffi < frag.num_faces(); ++ffi) { - // Face orientation reversal is handled implicitly: - // the face is included as-is from B, forming the inner boundary. - } - 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)); } if (keep.empty()) return BrepModel(); diff --git a/tests/brep/test_brep_boolean.cpp b/tests/brep/test_brep_boolean.cpp index 631402c..c47ad54 100644 --- a/tests/brep/test_brep_boolean.cpp +++ b/tests/brep/test_brep_boolean.cpp @@ -153,6 +153,17 @@ TEST(BrepBooleanTest, Union_Commutative) { EXPECT_TRUE(r2.is_valid()); } +// ═══════════════════════════════════════════════════════════ +// Boolean operations with face-plane splitting (v3.4) +// ═══════════════════════════════════════════════════════════ + +TEST(BrepBooleanTest, Union_SelfUnion_WithSplitting) { + auto box = make_box(2, 2, 2); + auto result = brep_union(box, box); + EXPECT_TRUE(result.is_valid()); + // Self-union should produce a valid result +} + TEST(BrepBooleanTest, Intersection_Commutative) { auto box1 = make_box(2, 2, 2); auto box2 = make_box(2, 2, 2);