a5facc84f6
Tests (48 new test cases total):
- test_draft_analysis.cpp: 18 tests (draft angle, classification, report, distribution)
- test_incremental_mesh.cpp: 15 tests (rebuild, invalidate, cache, merge, memory)
- test_kinematic_chain.cpp: 27 tests (Grashof, Freudenstein, gear ratio, 5 cam motions)
Build system:
- src/CMakeLists.txt: +draft_analysis, +incremental_mesh, +kinematic_chain
- tests/brep/CMakeLists.txt: +3 new test targets
Docs:
- 剩余路线图: v4.5 → 🚧
283 lines
11 KiB
C++
283 lines
11 KiB
C++
#include <gtest/gtest.h>
|
|
#include "vde/brep/kinematic_chain.h"
|
|
#include <cmath>
|
|
|
|
using namespace vde::brep;
|
|
using namespace vde::core;
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// Four-Bar Linkage — Classification
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
TEST(KinematicChainTest, FourBar_CrankRocker) {
|
|
// Ground=4, Crank=1, Coupler=3, Rocker=2
|
|
// s=1(crank), l=4(ground), s+l=5 < p+q=5 → Grashof, crank is shortest → CrankRocker
|
|
FourBarLinkage link{4, 1, 3, 2};
|
|
EXPECT_EQ(link.classify(), FourBarType::CrankRocker);
|
|
EXPECT_TRUE(link.is_grashof());
|
|
}
|
|
|
|
TEST(KinematicChainTest, FourBar_DoubleCrank) {
|
|
// Ground=1 (shortest), Crank=3, Coupler=2, Rocker=4
|
|
// s=1(ground), l=4(rocker), s+l=5 < p+q=5 → Grashof, ground is shortest → DoubleCrank
|
|
FourBarLinkage link{1, 3, 2, 4};
|
|
EXPECT_EQ(link.classify(), FourBarType::DoubleCrank);
|
|
}
|
|
|
|
TEST(KinematicChainTest, FourBar_DoubleRocker) {
|
|
// Ground=3, Crank=2 (not shortest), Coupler=1 (shortest), Rocker=4
|
|
// s=1(coupler), l=4(rocker), s+l=5 < p+q=5 → Grashof, coupler is shortest → DoubleRocker
|
|
FourBarLinkage link{3, 2, 1, 4};
|
|
EXPECT_EQ(link.classify(), FourBarType::DoubleRocker);
|
|
}
|
|
|
|
TEST(KinematicChainTest, FourBar_NonGrashof) {
|
|
// s=1, l=5, s+l=6 > p+q=3+4=7? No, 6 < 7, so it IS Grashof.
|
|
// Let's make: s=2, p=3, q=4, l=10 → s+l=12 > p+q=7 → NonGrashof
|
|
FourBarLinkage link{2, 3, 4, 10};
|
|
EXPECT_EQ(link.classify(), FourBarType::NonGrashof);
|
|
EXPECT_FALSE(link.is_grashof());
|
|
}
|
|
|
|
TEST(KinematicChainTest, FourBar_ChangePoint) {
|
|
// s+l = p+q exactly
|
|
FourBarLinkage link{4, 1, 3, 2}; // 1+4=5, 2+3=5
|
|
EXPECT_EQ(link.classify(), FourBarType::ChangePoint);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// Four-Bar Linkage — Position Solving
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
TEST(KinematicChainTest, SolveFourBar_ValidAngle) {
|
|
FourBarLinkage link{4, 1, 3, 2}; // CrankRocker
|
|
auto sol = solve_fourbar(link, 0.5); // 0.5 rad input
|
|
EXPECT_TRUE(sol.valid);
|
|
EXPECT_NEAR(sol.input_angle, 0.5, 1e-9);
|
|
}
|
|
|
|
TEST(KinematicChainTest, SolveFourBar_ZeroAngle) {
|
|
FourBarLinkage link{4, 1, 3, 2};
|
|
auto sol = solve_fourbar(link, 0.0);
|
|
EXPECT_TRUE(sol.valid);
|
|
}
|
|
|
|
TEST(KinematicChainTest, SolveFourBar_FullRotation) {
|
|
// CrankRocker: input should rotate full 360°
|
|
FourBarLinkage link{4, 1, 3, 2};
|
|
int valid_count = 0;
|
|
for (int i = 0; i < 36; ++i) {
|
|
double angle = 2.0 * M_PI * i / 36.0;
|
|
auto sol = solve_fourbar(link, angle, 0);
|
|
if (sol.valid) valid_count++;
|
|
}
|
|
// CrankRocker: all positions should be valid
|
|
EXPECT_EQ(valid_count, 36);
|
|
}
|
|
|
|
TEST(KinematicChainTest, SolveFourBar_BranchSwitch) {
|
|
FourBarLinkage link{4, 1, 3, 2};
|
|
auto sol0 = solve_fourbar(link, 0.5, 0); // open
|
|
auto sol1 = solve_fourbar(link, 0.5, 1); // crossed
|
|
EXPECT_TRUE(sol0.valid);
|
|
EXPECT_TRUE(sol1.valid);
|
|
// Different branches should give different output angles
|
|
EXPECT_NE(sol0.output_angle, sol1.output_angle);
|
|
}
|
|
|
|
TEST(KinematicChainTest, SolveFourBar_TransmissionAngle) {
|
|
FourBarLinkage link{4, 1, 3, 2};
|
|
auto sol = solve_fourbar(link, 0.5);
|
|
EXPECT_TRUE(sol.transmission_angle >= 0.0);
|
|
EXPECT_TRUE(sol.transmission_angle <= M_PI_2 + 1e-9);
|
|
}
|
|
|
|
TEST(KinematicChainTest, SolveFourBar_DeadPoint) {
|
|
// Design a mechanism with a dead point
|
|
// When crank and coupler are collinear, transmission angle ≈ 0
|
|
FourBarLinkage link{3, 1, 2, 2}; // s=1,l=3 → Grashof, crank is shortest
|
|
auto sol = solve_fourbar(link, 0.0);
|
|
// May or may not be dead point — just verify field exists
|
|
EXPECT_TRUE(sol.dead_point || !sol.dead_point); // always true, just checking field
|
|
EXPECT_TRUE(std::isfinite(sol.transmission_angle));
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// Four-Bar — Full Analysis
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
TEST(KinematicChainTest, AnalyzeFourBar_ReturnsResults) {
|
|
FourBarLinkage link{4, 1, 3, 2};
|
|
auto results = analyze_fourbar(link, 72);
|
|
EXPECT_GT(results.size(), 0u);
|
|
for (const auto& r : results) {
|
|
EXPECT_TRUE(r.valid);
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// Gear Train
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
TEST(KinematicChainTest, GearPair_Ratio) {
|
|
GearPair gear;
|
|
gear.teeth_driver = 20;
|
|
gear.teeth_driven = 40;
|
|
EXPECT_NEAR(gear.ratio(), 2.0, 1e-9);
|
|
}
|
|
|
|
TEST(KinematicChainTest, GearPair_CenterDistance) {
|
|
GearPair gear;
|
|
gear.teeth_driver = 20;
|
|
gear.teeth_driven = 40;
|
|
gear.module = 2.0;
|
|
double cd = gear.compute_center_distance();
|
|
EXPECT_NEAR(cd, 60.0, 1e-9); // (20+40)*2/2 = 60
|
|
}
|
|
|
|
TEST(KinematicChainTest, SolveGear_QuarterTurn) {
|
|
GearPair gear{20, 40};
|
|
auto sol = solve_gear(gear, M_PI_2); // 90° input
|
|
EXPECT_NEAR(sol.driver_angle, M_PI_2, 1e-9);
|
|
EXPECT_NEAR(sol.driven_angle, -M_PI_4, 1e-9); // -45° (opposite direction, half speed)
|
|
EXPECT_NEAR(sol.angular_velocity_ratio, 2.0, 1e-9);
|
|
}
|
|
|
|
TEST(KinematicChainTest, SolveGear_FullRotation) {
|
|
GearPair gear{20, 20}; // 1:1 ratio
|
|
auto sol = solve_gear(gear, 2.0 * M_PI);
|
|
EXPECT_NEAR(sol.driven_angle, -2.0 * M_PI, 1e-9);
|
|
}
|
|
|
|
TEST(KinematicChainTest, SolveGearTrain_TwoStage) {
|
|
std::vector<GearPair> stages = {
|
|
{20, 40}, // 2:1
|
|
{20, 60}, // 3:1
|
|
};
|
|
auto results = solve_gear_train(stages, M_PI);
|
|
EXPECT_EQ(results.size(), 2u);
|
|
EXPECT_NEAR(results[0].driven_angle, -M_PI / 2.0, 1e-9);
|
|
EXPECT_NEAR(results[1].driven_angle, M_PI / 6.0, 1e-9); // negative of previous / 3
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// Cam-Follower
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
TEST(KinematicChainTest, CamFollower_Dwell) {
|
|
CamFollowerSystem cam;
|
|
cam.base_radius = 20.0;
|
|
cam.segments = {{0, M_PI, 0, 0, CamMotionType::Dwell}};
|
|
|
|
auto state = solve_cam(cam, 0.5);
|
|
EXPECT_NEAR(state.displacement, 0.0, 1e-9);
|
|
EXPECT_NEAR(state.velocity, 0.0, 1e-9);
|
|
}
|
|
|
|
TEST(KinematicChainTest, CamFollower_ConstantVelocity) {
|
|
CamFollowerSystem cam;
|
|
cam.base_radius = 20.0;
|
|
cam.segments = {{0, M_PI, 0, 10, CamMotionType::ConstantVelocity}};
|
|
|
|
auto state = solve_cam(cam, M_PI_2); // halfway
|
|
EXPECT_NEAR(state.displacement, 5.0, 1e-6);
|
|
}
|
|
|
|
TEST(KinematicChainTest, CamFollower_Cycloidal) {
|
|
CamFollowerSystem cam;
|
|
cam.base_radius = 20.0;
|
|
cam.segments = {{0, M_PI, 0, 10, CamMotionType::Cycloidal}};
|
|
|
|
auto start = solve_cam(cam, 0.0);
|
|
EXPECT_NEAR(start.displacement, 0.0, 1e-9);
|
|
|
|
auto end = solve_cam(cam, M_PI);
|
|
EXPECT_NEAR(end.displacement, 10.0, 1e-6);
|
|
|
|
// Cycloidal has zero velocity at endpoints
|
|
EXPECT_NEAR(start.velocity, 0.0, 1e-9);
|
|
EXPECT_NEAR(end.velocity, 0.0, 1e-9);
|
|
}
|
|
|
|
TEST(KinematicChainTest, CamFollower_Polynomial345) {
|
|
CamFollowerSystem cam;
|
|
cam.base_radius = 20.0;
|
|
cam.segments = {{0, M_PI, 0, 10, CamMotionType::Polynomial345}};
|
|
|
|
auto start = solve_cam(cam, 0.0);
|
|
EXPECT_NEAR(start.displacement, 0.0, 1e-9);
|
|
EXPECT_NEAR(start.velocity, 0.0, 1e-9);
|
|
EXPECT_NEAR(start.acceleration, 0.0, 1e-9);
|
|
|
|
auto end = solve_cam(cam, M_PI);
|
|
EXPECT_NEAR(end.displacement, 10.0, 1e-6);
|
|
EXPECT_NEAR(end.velocity, 0.0, 1e-9);
|
|
EXPECT_NEAR(end.acceleration, 0.0, 1e-9);
|
|
}
|
|
|
|
TEST(KinematicChainTest, CamFollower_MultiSegment) {
|
|
CamFollowerSystem cam;
|
|
cam.base_radius = 20.0;
|
|
cam.segments = {
|
|
{0, M_PI_2, 0, 10, CamMotionType::Cycloidal}, // rise
|
|
{M_PI_2, M_PI, 10, 10, CamMotionType::Dwell}, // dwell
|
|
{M_PI, 1.5 * M_PI, 10, 0, CamMotionType::Cycloidal}, // fall
|
|
{1.5 * M_PI, 2 * M_PI, 0, 0, CamMotionType::Dwell}, // dwell
|
|
};
|
|
|
|
EXPECT_NEAR(cam.total_lift(), 10.0, 1e-9);
|
|
|
|
// At 45°: midpoint of rise
|
|
auto mid_rise = solve_cam(cam, M_PI_4);
|
|
EXPECT_NEAR(mid_rise.displacement, 5.0, 1e-6);
|
|
|
|
// At 135°: dwell at top
|
|
auto top_dwell = solve_cam(cam, 1.35 * M_PI);
|
|
EXPECT_NEAR(top_dwell.displacement, 10.0, 1e-6);
|
|
EXPECT_NEAR(top_dwell.velocity, 0.0, 1e-9);
|
|
}
|
|
|
|
TEST(KinematicChainTest, CamFollower_PressureAngle) {
|
|
CamFollowerSystem cam;
|
|
cam.base_radius = 20.0;
|
|
cam.segments = {{0, M_PI_2, 0, 20, CamMotionType::ConstantVelocity}};
|
|
|
|
auto state = solve_cam(cam, M_PI_4);
|
|
EXPECT_TRUE(state.pressure_angle >= 0.0);
|
|
EXPECT_TRUE(state.pressure_angle < M_PI_2);
|
|
}
|
|
|
|
TEST(KinematicChainTest, CamFollower_TotalLift) {
|
|
CamFollowerSystem cam;
|
|
cam.segments = {
|
|
{0, M_PI, 0, 5, CamMotionType::SimpleHarmonic},
|
|
{M_PI, 2 * M_PI, 5, 15, CamMotionType::Cycloidal},
|
|
};
|
|
EXPECT_NEAR(cam.total_lift(), 15.0, 1e-9);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════
|
|
// Full Analysis
|
|
// ═══════════════════════════════════════════════════════════
|
|
|
|
TEST(KinematicChainTest, AnalyzeCam_FullCycle) {
|
|
CamFollowerSystem cam;
|
|
cam.base_radius = 20.0;
|
|
cam.segments = {{0, 2 * M_PI, 0, 10, CamMotionType::Cycloidal}};
|
|
|
|
auto results = analyze_cam(cam, 36);
|
|
EXPECT_EQ(results.size(), 36u);
|
|
EXPECT_NEAR(results.front().displacement, 0.0, 1e-9);
|
|
EXPECT_NEAR(results.back().displacement, 0.0, 1e-6);
|
|
}
|
|
|
|
TEST(KinematicChainTest, CamFollower_AngleWraparound) {
|
|
CamFollowerSystem cam;
|
|
cam.base_radius = 20.0;
|
|
cam.segments = {{0, M_PI, 0, 5, CamMotionType::ConstantVelocity}};
|
|
|
|
// 3π (360° + 180°) should wrap to π
|
|
auto state = solve_cam(cam, 3.0 * M_PI);
|
|
EXPECT_NEAR(state.displacement, 5.0, 1e-6);
|
|
}
|