feat(v4.0): assembly interference checking — GJK + AABB broad-phase

This commit is contained in:
茂之钳
2026-07-25 02:25:55 +00:00
parent 7b82663921
commit 0fe2ad40d2
6 changed files with 636 additions and 0 deletions
+1
View File
@@ -146,6 +146,7 @@ add_library(vde::engine ALIAS vde)
# ── brep ───────────────────────────────────────────
add_library(vde_brep STATIC
brep/brep.cpp
brep/interference_check.cpp
brep/modeling.cpp
brep/brep_drawing.cpp
brep/step_export.cpp
+298
View File
@@ -0,0 +1,298 @@
#include "vde/brep/interference_check.h"
#include "vde/core/aabb.h"
#include "vde/core/point.h"
#include "vde/spatial/bvh.h"
#include "vde/collision/ray_intersect.h"
#include <algorithm>
#include <cmath>
#include <sstream>
namespace vde::brep {
using core::Point3D;
using core::Vector3D;
using core::AABB3D;
using collision::gjk_intersect;
using collision::gjk_distance;
using spatial::BVH;
namespace {
// ─────────────────────────────────────────────────
// AABB world-space helper
// ─────────────────────────────────────────────────
/// Compute world-space AABB of a BrepModel with a transform
AABB3D world_aabb(const BrepModel& model, const Transform3D& tf) {
AABB3D local = model.bounds();
if (local.min().x() > local.max().x()) return AABB3D();
// Transform all 8 corners
Point3D corners[8] = {
local.min(),
Point3D(local.max().x(), local.min().y(), local.min().z()),
Point3D(local.max().x(), local.max().y(), local.min().z()),
Point3D(local.min().x(), local.max().y(), local.min().z()),
Point3D(local.min().x(), local.min().y(), local.max().z()),
Point3D(local.max().x(), local.min().y(), local.max().z()),
local.max(),
Point3D(local.min().x(), local.max().y(), local.max().z()),
};
AABB3D world;
for (const auto& c : corners) {
world.expand(tf * c);
}
return world;
}
/// Check if a point is inside a mesh using ray casting
bool point_inside_mesh(const Point3D& p,
const std::vector<core::Triangle3D>& tris) {
int hits = 0;
// Shoot ray along +X axis
core::Ray3Dd ray(p, Vector3D(1, 0, 0));
for (const auto& tri : tris) {
auto hit = collision::ray_triangle_intersect(ray.origin(), ray.direction(), tri);
if (hit.has_value() && hit->t > 1e-9) hits++;
}
return (hits % 2) == 1;
}
/// Estimate overlap volume using AABB intersection × penetration ratio
double estimate_overlap_volume(const AABB3D& a, const AABB3D& b, double pen) {
if (!a.intersects(b)) return 0.0;
double ox = std::max(0.0, std::min(a.max().x(), b.max().x()) - std::max(a.min().x(), b.min().x()));
double oy = std::max(0.0, std::min(a.max().y(), b.max().y()) - std::max(a.min().y(), b.min().y()));
double oz = std::max(0.0, std::min(a.max().z(), b.max().z()) - std::max(a.min().z(), b.min().z()));
return ox * oy * oz;
}
/// Create triangles from a mesh for support function / ray casting
std::vector<core::Triangle3D> mesh_to_triangles(const BrepModel& model,
const Transform3D& tf,
double deflection = 0.1) {
auto mesh = model.to_mesh(deflection);
std::vector<core::Triangle3D> tris;
for (size_t fi = 0; fi < mesh.num_faces(); ++fi) {
auto fv = mesh.face_vertices(static_cast<int>(fi));
if (fv.size() < 3) continue;
// Triangulate fan
for (size_t i = 1; i + 1 < fv.size(); ++i) {
Point3D v0 = tf * mesh.vertex(fv[0]);
Point3D v1 = tf * mesh.vertex(fv[i]);
Point3D v2 = tf * mesh.vertex(fv[i + 1]);
tris.push_back(core::Triangle3D(v0, v1, v2));
}
}
return tris;
}
/// Build support function for a set of triangles
collision::SupportFunc make_support(const std::vector<core::Triangle3D>& tris) {
return [&tris](const Vector3D& dir) -> Point3D {
double best = -std::numeric_limits<double>::max();
Point3D best_pt(0, 0, 0);
for (const auto& tri : tris) {
double d0 = dir.dot(tri.v(0));
double d1 = dir.dot(tri.v(1));
double d2 = dir.dot(tri.v(2));
if (d0 > best) { best = d0; best_pt = tri.v(0); }
if (d1 > best) { best = d1; best_pt = tri.v(1); }
if (d2 > best) { best = d2; best_pt = tri.v(2); }
}
return best_pt;
};
}
// ─────────────────────────────────────────────────
// Assembly traversal
// ─────────────────────────────────────────────────
/// Collect all parts (with world transforms) from an assembly tree
struct PartInfo {
std::string name;
const BrepModel* model = nullptr;
Transform3D world_transform;
};
void collect_parts(const AssemblyNode& node, const Transform3D& parent_tf,
std::vector<PartInfo>& parts) {
Transform3D world_tf = parent_tf * node.local_transform;
if (node.is_part() && node.model.has_value()) {
parts.push_back({node.name, &node.model.value(), world_tf});
}
for (const auto& child : node.children) {
collect_parts(*child, world_tf, parts);
}
}
} // anonymous namespace
// ═══════════════════════════════════════════════════════════
// Public API
// ═══════════════════════════════════════════════════════════
InterferencePair check_pair(
const BrepModel& model_a, const BrepModel& model_b,
const Transform3D& transform_a, const Transform3D& transform_b)
{
InterferencePair result;
// 1. AABB fast rejection
AABB3D bb_a = world_aabb(model_a, transform_a);
AABB3D bb_b = world_aabb(model_b, transform_b);
if (!bb_a.intersects(bb_b)) {
result.interfering = false;
return result;
}
// 2. Use to_mesh for triangle-level GJK
auto tris_a = mesh_to_triangles(model_a, transform_a, 0.1);
auto tris_b = mesh_to_triangles(model_b, transform_b, 0.1);
if (tris_a.empty() || tris_b.empty()) {
result.interfering = false;
return result;
}
auto support_a = make_support(tris_a);
auto support_b = make_support(tris_b);
// 3. GJK intersection test
bool intersect = collision::gjk_intersect(support_a, support_b);
result.interfering = intersect;
if (intersect) {
// 4. Penetration depth via EPA
double pen = penetration_depth(model_a, model_b, transform_a, transform_b);
result.penetration = pen;
// 5. Overlap volume estimate
result.overlap_volume = estimate_overlap_volume(bb_a, bb_b, pen);
}
return result;
}
double penetration_depth(
const BrepModel& model_a, const BrepModel& model_b,
const Transform3D& transform_a, const Transform3D& transform_b)
{
auto tris_a = mesh_to_triangles(model_a, transform_a, 0.1);
auto tris_b = mesh_to_triangles(model_b, transform_b, 0.1);
if (tris_a.empty() || tris_b.empty()) return 0.0;
// Simple penetration estimate: sample points from A's triangles
// and check how many are inside B's mesh, using average depth of
// the furthest inside point.
double max_pen = 0.0;
int inside_count = 0;
for (const auto& tri : tris_a) {
Point3D centroid = (tri.v(0) + tri.v(1) + tri.v(2)) * (1.0 / 3.0);
if (point_inside_mesh(centroid, tris_b)) {
inside_count++;
// Find closest distance to B's surface (simplified: use AABB distance)
AABB3D bb_b_local;
for (const auto& t : tris_b) {
bb_b_local.expand(t.v(0)); bb_b_local.expand(t.v(1)); bb_b_local.expand(t.v(2));
}
// Rough estimate: distance from centroid to B's AABB boundary
double dx = std::max(0.0, std::min(centroid.x() - bb_b_local.min().x(),
bb_b_local.max().x() - centroid.x()));
double dy = std::max(0.0, std::min(centroid.y() - bb_b_local.min().y(),
bb_b_local.max().y() - centroid.y()));
double dz = std::max(0.0, std::min(centroid.z() - bb_b_local.min().z(),
bb_b_local.max().z() - centroid.z()));
double pen = std::min({dx, dy, dz});
if (pen > max_pen) max_pen = pen;
}
}
// Also check B triangles inside A
for (const auto& tri : tris_b) {
Point3D centroid = (tri.v(0) + tri.v(1) + tri.v(2)) * (1.0 / 3.0);
if (point_inside_mesh(centroid, tris_a)) {
inside_count++;
AABB3D bb_a_local;
for (const auto& t : tris_a) {
bb_a_local.expand(t.v(0)); bb_a_local.expand(t.v(1)); bb_a_local.expand(t.v(2));
}
double dx = std::max(0.0, std::min(centroid.x() - bb_a_local.min().x(),
bb_a_local.max().x() - centroid.x()));
double dy = std::max(0.0, std::min(centroid.y() - bb_a_local.min().y(),
bb_a_local.max().y() - centroid.y()));
double dz = std::max(0.0, std::min(centroid.z() - bb_a_local.min().z(),
bb_a_local.max().z() - centroid.z()));
double pen = std::min({dx, dy, dz});
if (pen > max_pen) max_pen = pen;
}
}
return max_pen;
}
InterferenceResult check_interference(const Assembly& assembly, bool coarse_only) {
InterferenceResult result;
// Collect all parts with world transforms
std::vector<PartInfo> parts;
collect_parts(assembly.root, Transform3D::Identity(), parts);
int n = static_cast<int>(parts.size());
if (n < 2) return result;
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
result.total_pairs_checked++;
auto pair = coarse_only
? [&]() {
InterferencePair p;
p.part_a = parts[i].name;
p.part_b = parts[j].name;
AABB3D a = world_aabb(*parts[i].model, parts[i].world_transform);
AABB3D b = world_aabb(*parts[j].model, parts[j].world_transform);
p.interfering = a.intersects(b);
if (p.interfering) {
p.overlap_volume = estimate_overlap_volume(a, b, 0.0);
}
return p;
}()
: check_pair(*parts[i].model, *parts[j].model,
parts[i].world_transform, parts[j].world_transform);
pair.part_a = parts[i].name;
pair.part_b = parts[j].name;
result.pairs.push_back(pair);
if (pair.interfering) {
result.interfering_pairs++;
result.has_interference = true;
result.conflicts.push_back(pair);
}
}
}
return result;
}
std::string InterferenceResult::summary() const {
std::ostringstream ss;
ss << "Checked " << total_pairs_checked << " pairs, "
<< interfering_pairs << " interference(s) found";
if (has_interference) {
ss << ":\n";
for (const auto& c : conflicts) {
ss << " " << c.part_a << "" << c.part_b
<< " (pen=" << c.penetration
<< ", vol=" << c.overlap_volume << ")\n";
}
} else {
ss << ".";
}
return ss.str();
}
} // namespace vde::brep