feat(v3.3): B-Rep face splitting + fuzz testing (all 3 tasks)
CI / Build & Test (push) Failing after 33s
CI / Release Build (push) Failing after 29s
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 12:48:42 +00:00
parent acc26e3a4b
commit 107cf58034
10 changed files with 775 additions and 0 deletions
+1
View File
@@ -149,6 +149,7 @@ add_library(vde_brep STATIC
brep/iges_export.cpp
brep/brep_boolean.cpp
brep/brep_validate.cpp
brep/brep_face_split.cpp
)
target_include_directories(vde_brep
PUBLIC ${CMAKE_SOURCE_DIR}/include
+172
View File
@@ -0,0 +1,172 @@
#include "vde/brep/brep_face_split.h"
#include "vde/brep/brep.h"
#include "vde/core/point.h"
#include <vector>
#include <cmath>
namespace vde::brep {
using core::Point3D;
using core::Vector3D;
namespace {
/// Signed distance from point to plane (positive = same side as normal).
inline double signed_distance(const Point3D& p, const Point3D& plane_pt,
const Vector3D& normal) {
return (p - plane_pt).dot(normal);
}
/// Get the ordered vertices of a face by traversing its outer loop edges.
/// Returns pairs of (vertex_id, point) in traversal order.
/// The last entry closes the loop (duplicates the first vertex).
std::vector<std::pair<int, Point3D>> ordered_face_vertices(
const BrepModel& body, int face_id) {
std::vector<std::pair<int, Point3D>> result;
auto es = body.face_edges(face_id);
if (es.empty()) return result;
// Each edge's v_start gives the next vertex in loop order
for (int ei : es) {
const auto& e = body.edge(ei);
result.emplace_back(e.v_start, body.vertex_by_id(e.v_start).point);
}
// Close the loop: add the end vertex of the last edge
const auto& last_e = body.edge(es.back());
result.emplace_back(last_e.v_end, body.vertex_by_id(last_e.v_end).point);
return result;
}
/// Build a BrepModel fragment from a polygon defined by ordered vertices.
/// Uses the same surface as the source face.
BrepModel build_polygon_fragment(const BrepModel& body, int face_id,
const std::vector<Point3D>& verts) {
BrepModel frag;
if (verts.size() < 3) return frag;
// Add vertices
int nv = static_cast<int>(verts.size());
std::vector<int> vids(nv);
for (int i = 0; i < nv; ++i)
vids[i] = frag.add_vertex(verts[i]);
// Add edges connecting consecutive vertices
std::vector<int> eids(nv);
for (int i = 0; i < nv; ++i)
eids[i] = frag.add_edge(vids[i], vids[(i + 1) % nv]);
int loop_id = frag.add_loop(eids, true);
// Copy the source face's surface
const auto& src_face = body.face(face_id);
int sid = frag.add_surface(body.surface(src_face.surface_id));
int fid = frag.add_face(sid, {loop_id});
int shid = frag.add_shell({fid}, false);
frag.add_body({shid}, "face_split_fragment");
return frag;
}
/// Tolerance for zero-distance comparisons.
constexpr double kZeroTol = 1e-9;
/// Classification of a vertex relative to the cutting plane.
enum class Side { Positive, Negative, On };
Side classify(double dist) {
if (dist > kZeroTol) return Side::Positive;
if (dist < -kZeroTol) return Side::Negative;
return Side::On;
}
} // anonymous namespace
std::vector<BrepModel> split_face_by_plane(
const BrepModel& body, int face_id,
const Point3D& plane_point,
const Vector3D& plane_normal) {
Vector3D n = plane_normal.normalized();
// ── 1. Get ordered face vertices ──
auto vtx = ordered_face_vertices(body, face_id);
if (vtx.size() < 3) {
// Degenerate face — return empty
return {};
}
int nv = static_cast<int>(vtx.size());
// ── 2. Classify each vertex ──
std::vector<double> dists(nv);
int pos_count = 0, neg_count = 0;
for (int i = 0; i < nv; ++i) {
dists[i] = signed_distance(vtx[i].second, plane_point, n);
if (dists[i] > kZeroTol) pos_count++;
else if (dists[i] < -kZeroTol) neg_count++;
}
// ── 3. All same side → single fragment ──
if (pos_count == 0 || neg_count == 0) {
std::vector<Point3D> all_pts;
for (int i = 0; i < nv - 1; ++i) // exclude closing duplicate
all_pts.push_back(vtx[i].second);
auto frag = build_polygon_fragment(body, face_id, all_pts);
if (frag.num_faces() > 0) return {std::move(frag)};
return {};
}
// ── 4. Face crosses plane — build two fragments ──
std::vector<Point3D> pos_verts, neg_verts;
for (int i = 0; i < nv - 1; ++i) { // nv-1 because last entry duplicates first
int j = i + 1;
double di = dists[i];
double dj = dists[j];
// Current vertex
if (di >= -kZeroTol) pos_verts.push_back(vtx[i].second);
if (di <= kZeroTol) neg_verts.push_back(vtx[i].second);
// Edge-plane intersection
if ((di > kZeroTol && dj < -kZeroTol) ||
(di < -kZeroTol && dj > kZeroTol)) {
double t = di / (di - dj);
Point3D isect = vtx[i].second + t * (vtx[j].second - vtx[i].second);
pos_verts.push_back(isect);
neg_verts.push_back(isect);
}
}
// Remove degenerate collinear duplicates if any
auto cleanup = [](std::vector<Point3D>& pts) {
if (pts.size() < 3) return;
std::vector<Point3D> dedup;
for (size_t i = 0; i < pts.size(); ++i) {
if (dedup.empty() ||
(pts[i] - dedup.back()).squaredNorm() > kZeroTol * kZeroTol) {
dedup.push_back(pts[i]);
}
}
// Also check first vs last
if (dedup.size() > 1 &&
(dedup.front() - dedup.back()).squaredNorm() < kZeroTol * kZeroTol) {
dedup.pop_back();
}
if (dedup.size() >= 3) pts = std::move(dedup);
};
cleanup(pos_verts);
cleanup(neg_verts);
std::vector<BrepModel> result;
auto pos_frag = build_polygon_fragment(body, face_id, pos_verts);
if (pos_frag.num_faces() > 0) result.push_back(std::move(pos_frag));
auto neg_frag = build_polygon_fragment(body, face_id, neg_verts);
if (neg_frag.num_faces() > 0) result.push_back(std::move(neg_frag));
return result;
}
} // namespace vde::brep