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:
+75
-39
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user