diff --git a/include/vde/brep/measure.h b/include/vde/brep/measure.h index a6ca5d6..a43e59d 100644 --- a/include/vde/brep/measure.h +++ b/include/vde/brep/measure.h @@ -8,6 +8,8 @@ * - 表面积(三角剖分求和) * - 质心 * - 两实体间最小距离 + * - 质量属性(惯性张量、回转半径) + * - 装配体质量属性 + BOM 物料清单 * * @ingroup brep */ @@ -15,91 +17,47 @@ #include "vde/brep/assembly.h" #include "vde/core/point.h" #include +#include +#include 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 generate_bom(const AssemblyNode& node); + } // namespace vde::brep diff --git a/src/brep/measure.cpp b/src/brep/measure.cpp index 6603b07..1ac9b7a 100644 --- a/src/brep/measure.cpp +++ b/src/brep/measure.cpp @@ -4,7 +4,10 @@ #include "vde/core/aabb.h" #include "vde/mesh/halfedge_mesh.h" #include +#include #include +#include +#include #include namespace vde::brep { @@ -333,4 +336,36 @@ double clearance(const BrepModel& a, const BrepModel& b) { return distance(a, b); } +// ── generate_bom ──────────────────────────────────────────── + +std::vector generate_bom(const AssemblyNode& node) { + std::map bom_map; + + std::function 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 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 diff --git a/tests/brep/test_measure.cpp b/tests/brep/test_measure.cpp index a3be7ea..637234f 100644 --- a/tests/brep/test_measure.cpp +++ b/tests/brep/test_measure.cpp @@ -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()); +}