feat(v3.6): SSI-based boolean operations — face-face intersection splitting
CI / Build & Test (push) Failing after 1m40s
CI / Release Build (push) Failing after 31s
Build & Test / build-and-test (push) Has been cancelled
Build & Test / python-bindings (push) Has been cancelled

This commit is contained in:
茂之钳
2026-07-24 14:36:32 +00:00
parent 00ea90b522
commit 38ebb963d0
2 changed files with 281 additions and 32 deletions
+242 -29
View File
@@ -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);
+36
View File
@@ -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)
// ═══════════════════════════════════════════════════════════