feat(v3.5): perf caching + measure + flange/gear + feature tree + assembly constraints
CI / Build & Test (push) Failing after 1m31s
CI / Release Build (push) Failing after 31s
Build & Test / build-and-test (push) Has been cancelled
Build & Test / python-bindings (push) Has been cancelled

This commit is contained in:
茂之钳
2026-07-24 14:02:24 +00:00
parent f89f5f6bcd
commit 12db7d3e5a
17 changed files with 1487 additions and 2 deletions
+358
View File
@@ -0,0 +1,358 @@
#include <gtest/gtest.h>
#include "vde/brep/assembly_constraints.h"
#include "vde/brep/feature_tree.h"
#include "vde/brep/modeling.h"
#include "vde/core/transform.h"
#include <cmath>
using namespace vde::brep;
using namespace vde::core;
// ════════════════════════════════════════════════════════════════
// Assembly Constraint Tests
// ════════════════════════════════════════════════════════════════
TEST(AssemblyConstraintTest, CoincidentConstraint) {
Assembly assy("test_assy");
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
auto* box_b = assy.root.add_part("box_b", make_box(2, 2, 2),
translate(0, 0, 10));
EXPECT_TRUE(apply_constraint(assy, box_a, box_b, ConstraintType::Coincident));
// After coincident constraint, box_b should be sitting directly on box_a
// Verify by checking that the bounding boxes share a face plane
AABB3D bb_a = box_a->model->bounds();
// Compute box_b's world-space bounding box
AABB3D bb_b_local = box_b->model->bounds();
AABB3D bb_b_world;
Point3D corners[8] = {
bb_b_local.min(),
Point3D(bb_b_local.max().x(), bb_b_local.min().y(), bb_b_local.min().z()),
Point3D(bb_b_local.max().x(), bb_b_local.max().y(), bb_b_local.min().z()),
Point3D(bb_b_local.min().x(), bb_b_local.max().y(), bb_b_local.min().z()),
Point3D(bb_b_local.min().x(), bb_b_local.min().y(), bb_b_local.max().z()),
Point3D(bb_b_local.max().x(), bb_b_local.min().y(), bb_b_local.max().z()),
bb_b_local.max(),
Point3D(bb_b_local.min().x(), bb_b_local.max().y(), bb_b_local.max().z()),
};
for (const auto& c : corners) {
bb_b_world.expand(box_b->local_transform * c);
}
// box_a top = 1.0, box_b bottom should be very close to 1.0
EXPECT_NEAR(bb_b_world.min().z(), bb_a.max().z(), 1e-4);
}
TEST(AssemblyConstraintTest, ConcentricConstraint) {
Assembly assy("test_assy");
auto* cyl_a = assy.root.add_part("cyl_a", make_cylinder(1.0, 4.0));
auto* cyl_b = assy.root.add_part("cyl_b", make_cylinder(0.5, 2.0),
translate(5, 0, 0));
EXPECT_TRUE(apply_constraint(assy, cyl_a, cyl_b, ConstraintType::Concentric));
// Both cylinders should now be concentric (same center in X,Y)
AABB3D bb_a = cyl_a->model->bounds();
Point3D center_a = bb_a.center();
AABB3D bb_b_local = cyl_b->model->bounds();
Point3D center_b_local = bb_b_local.center();
Point3D center_b_world = cyl_b->local_transform * center_b_local;
EXPECT_NEAR(center_b_world.x(), center_a.x(), 1e-4);
EXPECT_NEAR(center_b_world.y(), center_a.y(), 1e-4);
}
TEST(AssemblyConstraintTest, DistanceConstraint) {
Assembly assy("test_assy");
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
auto* box_b = assy.root.add_part("box_b", make_box(2, 2, 2),
translate(0, 0, 10));
EXPECT_TRUE(apply_constraint(assy, box_a, box_b, ConstraintType::Distance, 5.0));
// After distance constraint of 5.0, box_b bottom should be 5.0 above box_a top
AABB3D bb_a = box_a->model->bounds();
AABB3D bb_b_local = box_b->model->bounds();
Point3D bb_b_min_local = bb_b_local.min();
Point3D bb_b_min_world = box_b->local_transform * bb_b_min_local;
double gap = bb_b_min_world.z() - bb_a.max().z();
EXPECT_NEAR(gap, 5.0, 1e-4);
}
TEST(AssemblyConstraintTest, AngleConstraint) {
Assembly assy("test_assy");
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
auto* box_b = assy.root.add_part("box_b", make_box(1, 1, 4));
// Apply 90-degree angle constraint (rotate node_b)
EXPECT_TRUE(apply_constraint(assy, box_a, box_b, ConstraintType::Angle, M_PI / 2.0));
// Verify the transform was applied (non-identity rotation)
EXPECT_FALSE(box_b->local_transform.isApprox(Transform3D::Identity(), 1e-6));
}
TEST(AssemblyConstraintTest, PerpendicularConstraint) {
Assembly assy("test_assy");
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
auto* box_b = assy.root.add_part("box_b", make_box(1, 1, 4));
EXPECT_TRUE(apply_constraint(assy, box_a, box_b, ConstraintType::Perpendicular));
// Verify the transform is not identity (rotation was applied)
EXPECT_FALSE(box_b->local_transform.isApprox(Transform3D::Identity(), 1e-6));
}
TEST(AssemblyConstraintTest, ParallelConstraint) {
Assembly assy("test_assy");
auto* box_a = assy.root.add_part("box_a", make_box(2, 2, 2));
auto* box_b = assy.root.add_part("box_b", make_box(2, 2, 2),
translate(0, 5, 0));
EXPECT_TRUE(apply_constraint(assy, box_a, box_b, ConstraintType::Parallel));
// Parallel constraint preserves the offset (no transform change for axis-aligned)
AABB3D bb_b_local = box_b->model->bounds();
Point3D center_b_local = bb_b_local.center();
Point3D center_b_world = box_b->local_transform * center_b_local;
EXPECT_NEAR(center_b_world.y(), 5.0, 1e-4);
}
// ════════════════════════════════════════════════════════════════
// Feature Tree Tests
// ════════════════════════════════════════════════════════════════
TEST(FeatureTreeTest, PrimitiveBox) {
FeatureNode node;
node.type = FeatureType::PrimitiveBox;
node.params.values = {2.0, 3.0, 4.0};
BrepModel result = node.evaluate();
EXPECT_GT(result.num_vertices(), 0u);
EXPECT_GT(result.num_faces(), 0u);
AABB3D bb = result.bounds();
EXPECT_NEAR(bb.extent().x(), 2.0, 1e-4);
EXPECT_NEAR(bb.extent().y(), 3.0, 1e-4);
EXPECT_NEAR(bb.extent().z(), 4.0, 1e-4);
}
TEST(FeatureTreeTest, PrimitiveCylinder) {
FeatureNode node;
node.type = FeatureType::PrimitiveCylinder;
node.params.values = {1.5, 6.0};
BrepModel result = node.evaluate();
EXPECT_GT(result.num_vertices(), 0u);
EXPECT_GT(result.num_faces(), 0u);
AABB3D bb = result.bounds();
EXPECT_NEAR(bb.extent().x(), 3.0, 0.1); // diameter
EXPECT_NEAR(bb.extent().y(), 6.0, 1e-4); // height
}
TEST(FeatureTreeTest, PrimitiveSphere) {
FeatureNode node;
node.type = FeatureType::PrimitiveSphere;
node.params.values = {2.5};
BrepModel result = node.evaluate();
EXPECT_GT(result.num_vertices(), 0u);
AABB3D bb = result.bounds();
EXPECT_NEAR(bb.extent().x(), 5.0, 0.1); // diameter
EXPECT_NEAR(bb.extent().y(), 5.0, 0.1);
EXPECT_NEAR(bb.extent().z(), 5.0, 0.1);
}
TEST(FeatureTreeTest, NestedFeatureTree) {
// Box → Fillet
auto box_node = std::make_unique<FeatureNode>();
box_node->type = FeatureType::PrimitiveBox;
box_node->params.values = {10.0, 5.0, 3.0};
auto fillet_node = std::make_unique<FeatureNode>();
fillet_node->type = FeatureType::Fillet;
fillet_node->params.values = {1.0};
fillet_node->params.int_values = {0};
fillet_node->children.push_back(std::move(box_node));
BrepModel result = fillet_node->evaluate();
EXPECT_TRUE(result.is_valid());
EXPECT_GT(result.num_faces(), 0u);
}
TEST(FeatureTreeTest, BooleanUnionFeature) {
// Box Cylinder
auto box = std::make_unique<FeatureNode>();
box->type = FeatureType::PrimitiveBox;
box->params.values = {3.0, 3.0, 3.0};
auto cyl = std::make_unique<FeatureNode>();
cyl->type = FeatureType::PrimitiveCylinder;
cyl->params.values = {1.0, 5.0};
auto union_node = std::make_unique<FeatureNode>();
union_node->type = FeatureType::BooleanUnion;
union_node->children.push_back(std::move(box));
union_node->children.push_back(std::move(cyl));
BrepModel result = union_node->evaluate();
EXPECT_GT(result.num_vertices(), 0u);
EXPECT_GT(result.num_faces(), 0u);
}
// ════════════════════════════════════════════════════════════════
// FeatureHistory Undo/Redo Tests
// ════════════════════════════════════════════════════════════════
TEST(FeatureHistoryTest, ApplyOperations) {
FeatureHistory history;
// Apply 3 operations
BrepModel box1 = make_box(2, 2, 2);
history.apply(box1, FeatureType::PrimitiveBox, FeatureParams{});
EXPECT_EQ(history.size(), 1u);
BrepModel box2 = make_box(3, 3, 3);
history.apply(box2, FeatureType::PrimitiveBox, FeatureParams{});
EXPECT_EQ(history.size(), 2u);
BrepModel cyl = make_cylinder(1, 5);
history.apply(cyl, FeatureType::PrimitiveCylinder, FeatureParams{});
EXPECT_EQ(history.size(), 3u);
}
TEST(FeatureHistoryTest, UndoRedo) {
FeatureHistory history;
// Create 3 distinct models as initial states
BrepModel box_small = make_box(2, 2, 2);
BrepModel box_large = make_box(5, 5, 5);
BrepModel cylinder = make_cylinder(2, 10);
history.apply(box_small, FeatureType::PrimitiveBox, FeatureParams{});
EXPECT_EQ(history.size(), 1u);
history.apply(box_large, FeatureType::PrimitiveBox, FeatureParams{});
EXPECT_EQ(history.size(), 2u);
history.apply(cylinder, FeatureType::PrimitiveCylinder, FeatureParams{});
EXPECT_EQ(history.size(), 3u);
// Undo twice
EXPECT_TRUE(history.undo());
EXPECT_EQ(history.size(), 3u); // size doesn't change with undo
EXPECT_TRUE(history.undo());
EXPECT_EQ(history.size(), 3u);
// Should not be able to undo past start
EXPECT_TRUE(history.undo());
EXPECT_FALSE(history.undo()); // no more to undo
// Redo all
EXPECT_TRUE(history.redo());
EXPECT_TRUE(history.redo());
EXPECT_TRUE(history.redo());
EXPECT_FALSE(history.redo()); // no more to redo
}
TEST(FeatureHistoryTest, UndoRedoVerifyStates) {
FeatureHistory history;
BrepModel initial = make_box(2, 2, 2);
history.apply(initial, FeatureType::PrimitiveBox, FeatureParams{});
BrepModel second = make_box(5, 5, 5);
history.apply(second, FeatureType::PrimitiveBox, FeatureParams{});
BrepModel third = make_cylinder(2, 10);
history.apply(third, FeatureType::PrimitiveCylinder, FeatureParams{});
// Current should be the third operation's state
AABB3D bb_current = history.current().bounds();
EXPECT_NEAR(bb_current.extent().x(), 4.0, 0.5); // cylinder diameter
// Undo → should get second state
EXPECT_TRUE(history.undo());
AABB3D bb_undo = history.current().bounds();
EXPECT_NEAR(bb_undo.extent().x(), 5.0, 1e-4);
// Redo → should get third state back
EXPECT_TRUE(history.redo());
AABB3D bb_redo = history.current().bounds();
EXPECT_NEAR(bb_redo.extent().x(), 4.0, 0.5);
}
TEST(FeatureHistoryTest, ApplyAfterUndoTruncatesRedo) {
FeatureHistory history;
history.apply(make_box(2, 2, 2), FeatureType::PrimitiveBox, FeatureParams{});
history.apply(make_box(5, 5, 5), FeatureType::PrimitiveBox, FeatureParams{});
history.apply(make_cylinder(2, 10), FeatureType::PrimitiveCylinder, FeatureParams{});
// Undo one
EXPECT_TRUE(history.undo());
EXPECT_EQ(history.size(), 3u);
// Apply a new operation → should truncate redo stack
history.apply(make_box(10, 10, 10), FeatureType::PrimitiveBox, FeatureParams{});
EXPECT_EQ(history.size(), 3u); // still 3 → old "third" was replaced
// Redo should be exhausted (old third was deleted)
EXPECT_FALSE(history.redo());
}
TEST(FeatureHistoryTest, ThreeOperationsUndoRedoVerify) {
FeatureHistory history;
// Apply op1: small box
history.apply(make_box(2, 2, 2), FeatureType::PrimitiveBox, FeatureParams{});
// Apply op2: large box
history.apply(make_box(5, 5, 5), FeatureType::PrimitiveBox, FeatureParams{});
// Apply op3: cylinder
history.apply(make_cylinder(2, 10), FeatureType::PrimitiveCylinder, FeatureParams{});
EXPECT_EQ(history.size(), 3u);
// Verify current is cylinder
{
AABB3D bb = history.current().bounds();
EXPECT_NEAR(bb.extent().x(), 4.0, 0.5); // cylinder diameter ≈ 4
}
// Undo back to large box
EXPECT_TRUE(history.undo());
{
AABB3D bb = history.current().bounds();
EXPECT_NEAR(bb.extent().x(), 5.0, 1e-4);
}
// Undo back to small box
EXPECT_TRUE(history.undo());
{
AABB3D bb = history.current().bounds();
EXPECT_NEAR(bb.extent().x(), 2.0, 1e-4);
}
// Undo back to empty
EXPECT_TRUE(history.undo());
EXPECT_FALSE(history.undo());
// Redo: empty → small box → large box → cylinder
EXPECT_TRUE(history.redo());
EXPECT_TRUE(history.redo());
EXPECT_TRUE(history.redo());
EXPECT_FALSE(history.redo());
// Verify we're back at cylinder
{
AABB3D bb = history.current().bounds();
EXPECT_NEAR(bb.extent().x(), 4.0, 0.5);
}
}