Files
ViewDesignEngine/tests/brep/test_assembly_constraints.cpp
T
茂之钳 12db7d3e5a
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
feat(v3.5): perf caching + measure + flange/gear + feature tree + assembly constraints
2026-07-24 14:02:24 +00:00

359 lines
13 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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);
}
}