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.
This commit is contained in:
茂之钳
2026-07-24 13:20:17 +00:00
parent 2b8bc3c212
commit 212c6a02c5
2 changed files with 86 additions and 39 deletions
+75 -39
View File
@@ -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<std::pair<BrepModel, ClassResult>>
split_and_classify_faces(const BrepModel& a, const BrepModel& b) {
std::vector<std::pair<BrepModel, ClassResult>> result;
for (size_t fi = 0; fi < a.num_faces(); ++fi) {
std::vector<BrepModel> current;
current.push_back(extract_face_fragment(a, static_cast<int>(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<int>(bfi));
Vector3D plane_n = face_normal(b, static_cast<int>(bfi));
std::vector<BrepModel> 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<BrepModel> 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<int>(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<int>(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<BrepModel> 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<int>(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<int>(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<BrepModel> keep;
// A faces OUT of B
for (size_t fi = 0; fi < a.num_faces(); ++fi) {
auto frag = extract_face_fragment(a, static_cast<int>(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<int>(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();
+11
View File
@@ -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);