#include #include "vde/brep/assembly_constraints.h" #include "vde/brep/feature_tree.h" #include "vde/brep/modeling.h" #include "vde/core/transform.h" #include 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().z(), 6.0, 1e-4); // height (cylinder is along Z axis) } 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(); box_node->type = FeatureType::PrimitiveBox; box_node->params.values = {10.0, 5.0, 3.0}; auto fillet_node = std::make_unique(); 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(); box->type = FeatureType::PrimitiveBox; box->params.values = {3.0, 3.0, 3.0}; auto cyl = std::make_unique(); cyl->type = FeatureType::PrimitiveCylinder; cyl->params.values = {1.0, 5.0}; auto union_node = std::make_unique(); 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); } }