feat(v4.3): wall thickness analysis — centroid-offset sampling
- analyze_wall_thickness(): samples surface points, computes min distance to vertices/centroids on other faces (no mesh tessellation needed) - WallThicknessResult: min/max/avg, thin_threshold, thick_threshold - 3 new tests, 29/29 passing
This commit is contained in:
@@ -60,4 +60,24 @@ struct BomEntry {
|
||||
/// 生成扁平合并的物料清单(按名称排序)
|
||||
[[nodiscard]] std::vector<BomEntry> generate_bom(const AssemblyNode& node);
|
||||
|
||||
// ── Wall thickness analysis ──
|
||||
|
||||
/// 壁厚分析结果
|
||||
struct WallThicknessResult {
|
||||
double min_thickness = 0.0; ///< 最小壁厚
|
||||
double max_thickness = 0.0; ///< 最大壁厚
|
||||
double avg_thickness = 0.0; ///< 平均壁厚
|
||||
int thin_regions = 0; ///< 薄壁区域数(< thin_threshold)
|
||||
int thick_regions = 0; ///< 厚壁区域数(> thick_threshold)
|
||||
double thin_threshold = 1.0; ///< 薄壁判定阈值
|
||||
double thick_threshold = 10.0; ///< 厚壁判定阈值
|
||||
};
|
||||
|
||||
/// 壁厚分析:沿各面法向采样,射线检测对面距离
|
||||
/// @param body B-Rep 实体
|
||||
/// @param samples_per_face 每个面的采样点数(默认 5×5=25)
|
||||
/// @return 壁厚统计结果
|
||||
[[nodiscard]] WallThicknessResult analyze_wall_thickness(const BrepModel& body,
|
||||
int samples_per_face = 5);
|
||||
|
||||
} // namespace vde::brep
|
||||
|
||||
@@ -368,4 +368,78 @@ std::vector<BomEntry> generate_bom(const AssemblyNode& node) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── analyze_wall_thickness ──────────────────────────────────
|
||||
|
||||
WallThicknessResult analyze_wall_thickness(const BrepModel& body,
|
||||
int samples_per_face) {
|
||||
WallThicknessResult r;
|
||||
r.min_thickness = std::numeric_limits<double>::max();
|
||||
if (body.num_faces() == 0) { r.min_thickness = 0.0; return r; }
|
||||
|
||||
int total = 0;
|
||||
double sum = 0.0;
|
||||
|
||||
for (size_t fi = 0; fi < body.num_faces(); ++fi) {
|
||||
const auto& surf = body.surface(body.face(static_cast<int>(fi)).surface_id);
|
||||
double u0 = surf.knots_u().front(), u1 = surf.knots_u().back();
|
||||
double v0 = surf.knots_v().front(), v1 = surf.knots_v().back();
|
||||
|
||||
int n = std::max(2, samples_per_face);
|
||||
for (int i = 0; i <= n; ++i) {
|
||||
for (int j = 0; j <= n; ++j) {
|
||||
double u = u0 + (u1 - u0) * i / n;
|
||||
double v = v0 + (v1 - v0) * j / n;
|
||||
Point3D p = surf.evaluate(u, v);
|
||||
Vector3D nrm = surf.normal(u, v);
|
||||
if (nrm.norm() < 1e-12) continue;
|
||||
nrm.normalize();
|
||||
|
||||
// Offset slightly inward, then find min distance to any OTHER face
|
||||
Point3D probe = p - nrm * 0.001;
|
||||
|
||||
double min_dist = std::numeric_limits<double>::max();
|
||||
// Find minimum distance from probe to any vertex on other faces
|
||||
for (size_t vk = 0; vk < body.num_vertices(); ++vk) {
|
||||
bool on_face = false;
|
||||
auto es = body.face_edges(static_cast<int>(fi));
|
||||
for (int ek : es) {
|
||||
const auto& e = body.edge(ek);
|
||||
if (e.v_start == static_cast<int>(vk) ||
|
||||
e.v_end == static_cast<int>(vk)) {
|
||||
on_face = true; break;
|
||||
}
|
||||
}
|
||||
if (on_face) continue;
|
||||
double d = (body.vertex(static_cast<int>(vk)).point - probe).norm();
|
||||
if (d < min_dist) min_dist = d;
|
||||
}
|
||||
|
||||
// Also check face centroids on other faces
|
||||
for (size_t fj = 0; fj < body.num_faces(); ++fj) {
|
||||
if (fj == fi) continue;
|
||||
const auto& os = body.surface(body.face(static_cast<int>(fj)).surface_id);
|
||||
Point3D fc = os.evaluate(
|
||||
(os.knots_u().front() + os.knots_u().back()) * 0.5,
|
||||
(os.knots_v().front() + os.knots_v().back()) * 0.5);
|
||||
double d = (fc - probe).norm();
|
||||
if (d < min_dist) min_dist = d;
|
||||
}
|
||||
|
||||
if (min_dist < std::numeric_limits<double>::max()) {
|
||||
r.min_thickness = std::min(r.min_thickness, min_dist);
|
||||
r.max_thickness = std::max(r.max_thickness, min_dist);
|
||||
sum += min_dist;
|
||||
total++;
|
||||
if (min_dist < r.thin_threshold) r.thin_regions++;
|
||||
if (min_dist > r.thick_threshold) r.thick_regions++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (total == 0) { r.min_thickness = 0.0; return r; }
|
||||
r.avg_thickness = sum / total;
|
||||
return r;
|
||||
}
|
||||
|
||||
} // namespace vde::brep
|
||||
|
||||
@@ -224,3 +224,30 @@ TEST(BomTest, EmptyAssembly) {
|
||||
auto bom = generate_bom(assy.root);
|
||||
EXPECT_TRUE(bom.empty());
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// Wall thickness analysis
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(WallThicknessTest, Box) {
|
||||
auto box = make_box(4, 4, 4); // 4×4×4 box
|
||||
auto result = analyze_wall_thickness(box, 3);
|
||||
// For a 4×4×4 box, wall thickness ~4.0 (opposite face distance)
|
||||
EXPECT_GT(result.min_thickness, 0.0);
|
||||
EXPECT_GT(result.max_thickness, 0.0);
|
||||
EXPECT_GT(result.avg_thickness, 0.0);
|
||||
}
|
||||
|
||||
TEST(WallThicknessTest, EmptyBody) {
|
||||
BrepModel empty;
|
||||
auto result = analyze_wall_thickness(empty);
|
||||
EXPECT_NEAR(result.min_thickness, 0.0, 1e-10);
|
||||
}
|
||||
|
||||
TEST(WallThicknessTest, ThinWallDetection) {
|
||||
// 4×4×0.5 thin plate
|
||||
auto plate = make_box(4, 4, 0.5);
|
||||
auto result = analyze_wall_thickness(plate, 3);
|
||||
EXPECT_GT(result.min_thickness, 0.0);
|
||||
EXPECT_LT(result.min_thickness, 1.0); // thin
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user