feat(v4.3): BOM (bill of materials) — flat merged, name-sorted
- 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:
+28
-70
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user