feat(v3.3): B-Rep face splitting + fuzz testing (all 3 tasks)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user