feat(v3.6): SSI-based boolean operations — face-face intersection splitting
This commit is contained in:
+242
-29
@@ -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 <algorithm>
|
||||
#include <set>
|
||||
#include <map>
|
||||
@@ -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<int>(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<BrepModel> 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<int>(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<double>(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<Eigen::Matrix3d> 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,24 +561,67 @@ 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<BrepModel> 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<BrepModel> fragments;
|
||||
fragments.push_back(extract_face_fragment(a, static_cast<int>(fia)));
|
||||
|
||||
// Split by SSI against each face of B
|
||||
for (size_t fib = 0; fib < b.num_faces(); ++fib) {
|
||||
std::vector<BrepModel> new_fragments;
|
||||
for (auto& frag : fragments) {
|
||||
auto splits = split_face_by_face_ssi(
|
||||
frag, b, static_cast<int>(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;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// 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: SSI-split against A, classify, keep OUT only (ON from A already)
|
||||
for (size_t fib = 0; fib < b.num_faces(); ++fib) {
|
||||
std::vector<BrepModel> fragments;
|
||||
fragments.push_back(extract_face_fragment(b, static_cast<int>(fib)));
|
||||
|
||||
for (size_t fia = 0; fia < a.num_faces(); ++fia) {
|
||||
std::vector<BrepModel> new_fragments;
|
||||
for (auto& frag : fragments) {
|
||||
auto splits = split_face_by_face_ssi(
|
||||
frag, a, static_cast<int>(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();
|
||||
return sew_faces(keep);
|
||||
@@ -501,22 +634,62 @@ 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<BrepModel> 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<BrepModel> fragments;
|
||||
fragments.push_back(extract_face_fragment(a, static_cast<int>(fia)));
|
||||
|
||||
for (size_t fib = 0; fib < b.num_faces(); ++fib) {
|
||||
std::vector<BrepModel> new_fragments;
|
||||
for (auto& frag : fragments) {
|
||||
auto splits = split_face_by_face_ssi(
|
||||
frag, b, static_cast<int>(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;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
for (auto& frag : fragments) {
|
||||
auto cls = classify_face_fragment(frag, b);
|
||||
if (cls == IN || cls == ON) 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<BrepModel> fragments;
|
||||
fragments.push_back(extract_face_fragment(b, static_cast<int>(fib)));
|
||||
|
||||
for (size_t fia = 0; fia < a.num_faces(); ++fia) {
|
||||
std::vector<BrepModel> new_fragments;
|
||||
for (auto& frag : fragments) {
|
||||
auto splits = split_face_by_face_ssi(
|
||||
frag, a, static_cast<int>(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();
|
||||
return sew_faces(keep);
|
||||
@@ -530,23 +703,63 @@ 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<BrepModel> 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<BrepModel> fragments;
|
||||
fragments.push_back(extract_face_fragment(a, static_cast<int>(fia)));
|
||||
|
||||
for (size_t fib = 0; fib < b.num_faces(); ++fib) {
|
||||
std::vector<BrepModel> new_fragments;
|
||||
for (auto& frag : fragments) {
|
||||
auto splits = split_face_by_face_ssi(
|
||||
frag, b, static_cast<int>(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;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
for (auto& frag : fragments) {
|
||||
auto cls = classify_face_fragment(frag, b);
|
||||
if (cls == OUT) 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<BrepModel> fragments;
|
||||
fragments.push_back(extract_face_fragment(b, static_cast<int>(fib)));
|
||||
|
||||
for (size_t fia = 0; fia < a.num_faces(); ++fia) {
|
||||
std::vector<BrepModel> new_fragments;
|
||||
for (auto& frag : fragments) {
|
||||
auto splits = split_face_by_face_ssi(
|
||||
frag, a, static_cast<int>(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();
|
||||
return sew_faces(keep);
|
||||
|
||||
@@ -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)
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
Reference in New Issue
Block a user