feat(v4.3): BOM (bill of materials) — flat merged, name-sorted
CI / Build & Test (push) Failing after 39s
CI / Release Build (push) Failing after 29s
Build & Test / build-and-test (push) Has been cancelled
Build & Test / python-bindings (push) Has been cancelled

- BomEntry struct: name, quantity, volume, total_volume
- generate_bom(): recursive assembly traversal, same-name merging
- 5 new tests, 26/26 passing
This commit is contained in:
茂之钳
2026-07-25 07:14:27 +00:00
parent 3d435a4801
commit 1e196473bc
3 changed files with 111 additions and 70 deletions
+28 -70
View File
@@ -8,6 +8,8 @@
* - 表面积(三角剖分求和)
* - 质心
* - 两实体间最小距离
* - 质量属性(惯性张量、回转半径)
* - 装配体质量属性 + BOM 物料清单
*
* @ingroup brep
*/
@@ -15,91 +17,47 @@
#include "vde/brep/assembly.h"
#include "vde/core/point.h"
#include <Eigen/Dense>
#include <string>
#include <vector>
namespace vde::brep {
/**
* @brief 质量属性结果
*
* 包含体积、质心、惯性张量和回转半径。
* 所有量基于单位密度 ρ=1。
*/
/// 质量属性结果:体积、质心、惯性张量、回转半径(ρ=1)
struct MassProperties {
double volume = 0.0; ///< 体积
core::Point3D centroid{0, 0, 0}; ///< 质心
Eigen::Matrix3d inertia{Eigen::Matrix3d::Zero()}; ///< 惯性张量 (绕质心)
core::Vector3D gyration_radii{0, 0, 0}; ///< 回转半径 sqrt(I/m) 各轴
double volume = 0.0;
core::Point3D centroid{0, 0, 0};
Eigen::Matrix3d inertia{Eigen::Matrix3d::Zero()};
core::Vector3D gyration_radii{0, 0, 0};
};
/**
* @brief 计算两个 B-Rep 实体间的最小距离
*
* 通过将两个实体 tessellate 为三角网格,
* 计算所有三角形对之间的最小距离。
*
* @param a 第一个 B-Rep 实体
* @param b 第二个 B-Rep 实体
* @return 最小距离(≥ 0)。若两实体相交则返回 0。
*/
/// BOM 条目:零件名 + 数量 + 体积
struct BomEntry {
std::string name;
int quantity = 0;
double volume = 0.0;
double total_volume = 0.0;
};
// ── Basic measurements ──
[[nodiscard]] double distance(const BrepModel& a, const BrepModel& b);
/**
* @brief 计算 B-Rep 实体的表面积
*
* 将所有面 tessellate 为三角形,求和三角形面积。
*
* @param body B-Rep 实体
* @return 表面积(平方单位)
*/
[[nodiscard]] double surface_area(const BrepModel& body);
/**
* @brief 计算封闭 B-Rep 实体的体积
*
* 使用散度定理(divergence theorem):
*
* V = (1/3) Σ (面心 · 面法向量 * 三角形面积)
*
* 对 tessellated 网格的所有三角形求和。
*
* @param body 封闭 B-Rep 实体
* @return 体积(立方单位)
*
* @pre body 应为封闭实体(水密)
*/
[[nodiscard]] double volume(const BrepModel& body);
/**
* @brief 计算 B-Rep 实体的质心
*
* 使用 tessellated 网格计算面积加权质心。
*
* @param body B-Rep 实体
* @return 质心坐标
*/
[[nodiscard]] core::Point3D centroid(const BrepModel& body);
/**
* @brief 单实体完整质量属性
*
* 绕质心的惯性张量(密度 ρ=1)。使用四面体分解。
*/
[[nodiscard]] MassProperties mass_properties(const BrepModel& body);
// ── Mass properties ──
/**
* @brief 装配体递归质量属性
*
* 遍历装配树,考虑每个零件的局部变换,合并所有质量属性。
*/
[[nodiscard]] MassProperties mass_properties(const BrepModel& body);
[[nodiscard]] MassProperties assembly_mass_properties(const AssemblyNode& node,
const core::Transform3D& parent_tf = core::Transform3D::Identity());
/**
* @brief 两实体间的最小间隙
*
* 比 distance() 更高效:先用 AABB 过滤,仅在接近时检测。
* @return 最小间隙。若相交则返回 0。
*/
// ── Gap analysis ──
[[nodiscard]] double clearance(const BrepModel& a, const BrepModel& b);
// ── BOM ──
/// 生成扁平合并的物料清单(按名称排序)
[[nodiscard]] std::vector<BomEntry> generate_bom(const AssemblyNode& node);
} // namespace vde::brep
+35
View File
@@ -4,7 +4,10 @@
#include "vde/core/aabb.h"
#include "vde/mesh/halfedge_mesh.h"
#include <cmath>
#include <functional>
#include <limits>
#include <map>
#include <string>
#include <algorithm>
namespace vde::brep {
@@ -333,4 +336,36 @@ double clearance(const BrepModel& a, const BrepModel& b) {
return distance(a, b);
}
// ── generate_bom ────────────────────────────────────────────
std::vector<BomEntry> generate_bom(const AssemblyNode& node) {
std::map<std::string, BomEntry> bom_map;
std::function<void(const AssemblyNode&)> traverse =
[&](const AssemblyNode& n) {
if (n.is_part() && n.model.has_value()) {
double vol = volume(*n.model);
auto& entry = bom_map[n.name];
if (entry.quantity == 0) {
entry.name = n.name;
entry.volume = vol;
}
entry.quantity++;
entry.total_volume += vol;
}
for (const auto& child : n.children) {
traverse(*child);
}
};
traverse(node);
std::vector<BomEntry> result;
result.reserve(bom_map.size());
for (auto& [_, entry] : bom_map) result.push_back(std::move(entry));
std::sort(result.begin(), result.end(),
[](const BomEntry& a, const BomEntry& b) { return a.name < b.name; });
return result;
}
} // namespace vde::brep
+48
View File
@@ -176,3 +176,51 @@ TEST(AssemblyMassTest, EmptyAssembly) {
auto mp = assembly_mass_properties(assy.root);
EXPECT_NEAR(mp.volume, 0.0, 1e-10);
}
// ═══════════════════════════════════════════════════════════
// BOM (Bill of Materials)
// ═══════════════════════════════════════════════════════════
TEST(BomTest, SinglePart) {
Assembly assy("test");
assy.root.add_part("bracket", make_box(2, 2, 2));
auto bom = generate_bom(assy.root);
ASSERT_EQ(bom.size(), 1u);
EXPECT_EQ(bom[0].name, "bracket");
EXPECT_EQ(bom[0].quantity, 1);
}
TEST(BomTest, MultipleSameParts) {
Assembly assy("test");
assy.root.add_part("bolt", make_box(1, 1, 1));
assy.root.add_part("bolt", make_box(1, 1, 1));
assy.root.add_part("bolt", make_box(1, 1, 1));
auto bom = generate_bom(assy.root);
ASSERT_EQ(bom.size(), 1u);
EXPECT_EQ(bom[0].quantity, 3);
}
TEST(BomTest, DifferentParts) {
Assembly assy("test");
assy.root.add_part("base", make_box(4, 4, 1));
assy.root.add_part("pillar", make_cylinder(1, 8));
assy.root.add_part("top", make_box(4, 4, 1));
auto bom = generate_bom(assy.root);
ASSERT_EQ(bom.size(), 3u);
}
TEST(BomTest, NestedBom) {
Assembly assy("test");
auto* sub = assy.root.add_subassembly("sub_assy");
sub->add_part("pin", make_cylinder(0.5, 2));
sub->add_part("pin", make_cylinder(0.5, 2));
assy.root.add_part("frame", make_box(5, 5, 1));
auto bom = generate_bom(assy.root);
EXPECT_GE(bom.size(), 2u);
}
TEST(BomTest, EmptyAssembly) {
Assembly assy("test");
auto bom = generate_bom(assy.root);
EXPECT_TRUE(bom.empty());
}