/** * @file polyline_test_fixed.cpp * @brief Polyline 及其 Extrude 完整测试套件(新架构适配版) * * 测试范围: * - Polyline Geometry 基础功能(构造、SDF计算、最近点、曲率) * - Polyline Extrude Subface 函数(SDF、梯度、参数化、约束曲线) * - Geometry Data 内部函数(PMC、isEnd、法线计算) * - 圆弧段支持(Profile 和 Axis) * - Profile/Axis 替换功能 * - 复杂几何(非凸、多段、自相交检测) * - 边界情况(退化、极小、零长度、开放曲线) * - 变换操作(缩放、旋转、平移) * * @note 新架构适配: * - new_primitive 添加 &dc 参数 * - 简化的资源销毁流程 * - ProfileData 生命周期管理 * * @author Test Suite (Architecture Adapted) * @date 2024 */ #define _USE_MATH_DEFINES #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ============================================================================ // 测试框架宏定义 // ============================================================================ #define TEST_SECTION(name) \ std::cout << "\n========================================\n" \ << "TEST: " << name << "\n" \ << "========================================\n" #define TEST_ASSERT(condition, message) \ do { \ if (!(condition)) { \ std::cerr << "❌ FAILED: " << message << "\n"; \ std::cerr << " at " << __FILE__ << ":" << __LINE__ << "\n"; \ return false; \ } else { \ std::cout << "✓ PASSED: " << message << "\n"; \ } \ } while (0) #define TEST_APPROX_EQUAL(a, b, eps, message) \ TEST_ASSERT(std::abs((a) - (b)) < (eps), \ message << " (expected: " << (b) << ", got: " << (a) << ", diff: " << std::abs((a) - (b)) << ")") #define TEST_VECTOR_APPROX_EQUAL(v1, v2, eps, message) \ TEST_ASSERT((v1 - v2).norm() < (eps), message << " (distance: " << (v1 - v2).norm() << ")") // ============================================================================ // 测试数据工厂(无需修改) // ============================================================================ class TestDataFactory { public: // ========== Profile 工厂 ========== struct ProfileData { std::vector points; std::vector bulges; profile_descriptor_t descriptor; void finalize() { descriptor.point_number = static_cast(points.size()); descriptor.points = points.data(); descriptor.bulge_number = static_cast(bulges.size()); descriptor.bulges = bulges.data(); descriptor.is_close = true; descriptor.reference_normal = {0, 0, 1}; } }; static ProfileData createSquareProfile(double size = 1.0) { ProfileData data; double half = size / 2.0; // CW winding (顺时针): bottom-left → top-left → top-right → bottom-right data.points = { {-half, -half, 0.0}, {-half, half, 0.0}, {half, half, 0.0}, {half, -half, 0.0} }; data.bulges = {0.0, 0.0, 0.0, 0.0}; data.finalize(); return data; } static ProfileData createCircleProfile(double radius = 0.5) { ProfileData data; data.points = { {radius, 0.0, 0.0}, {-radius, 0.0, 0.0} }; data.bulges = {1.0, 1.0}; // 正值 → CW 弧 → CW 圆 data.finalize(); return data; } static ProfileData createRectangleProfile(double width, double height) { ProfileData data; double half_w = width / 2.0; double half_h = height / 2.0; // CW winding data.points = { {-half_w, -half_h, 0.0}, {-half_w, half_h, 0.0}, {half_w, half_h, 0.0}, {half_w, -half_h, 0.0} }; data.bulges = {0.0, 0.0, 0.0, 0.0}; data.finalize(); return data; } static ProfileData createTriangleProfile(double size = 1.0) { ProfileData data; double half = size / 2.0; double h = size * std::sqrt(3.0) / 2.0; // CW winding: reverse vertex order data.points = { {0, 2 * h / 3, 0.0}, {half, -h / 3, 0.0}, {-half, -h / 3, 0.0} }; data.bulges = {0.0, 0.0, 0.0}; data.finalize(); return data; } static ProfileData createLShapeProfile(double size = 1.0) { ProfileData data; double s = size; double half = size / 2.0; // CW winding: reverse vertex order data.points = { {0, s, 0.0}, {half, s, 0.0}, {half, half, 0.0}, {s, half, 0.0}, {s, 0, 0.0}, {0, 0, 0.0} }; data.bulges = {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; data.finalize(); return data; } static ProfileData createPentagonProfile(double radius = 0.5) { ProfileData data; // CW winding: iterate in reverse angle direction (from top, going clockwise) for (int i = 0; i < 5; ++i) { double angle = -(2.0 * M_PI * i / 5.0 - M_PI / 2.0); // 反向迭代 → CW data.points.push_back({radius * std::cos(angle), radius * std::sin(angle), 0.0}); } data.bulges = {0.0, 0.0, 0.0, 0.0, 0.0}; data.finalize(); return data; } static ProfileData createStarProfile(double outer_radius = 0.5, double inner_radius = 0.25) { ProfileData data; // CW winding: reverse angle direction for (int i = 0; i < 10; ++i) { double angle = -(2.0 * M_PI * i / 10.0 - M_PI / 2.0); // CW double r = (i % 2 == 0) ? outer_radius : inner_radius; data.points.push_back({r * std::cos(angle), r * std::sin(angle), 0.0}); } data.bulges.resize(10, 0.0); data.finalize(); return data; } static ProfileData createOpenProfile(double length = 1.0) { ProfileData data; data.points = { {0, 0, 0.0}, {length, 0, 0.0}, {length, length, 0.0} }; data.bulges = {0.0, 0.0}; data.descriptor.point_number = 3; data.descriptor.points = data.points.data(); data.descriptor.bulge_number = 2; data.descriptor.bulges = data.bulges.data(); data.descriptor.is_close = false; // 不闭合 data.descriptor.reference_normal = {0, 0, 1}; return data; } // ========== Axis 工厂 ========== struct AxisData { std::vector points; std::vector bulges; axis_descriptor_t descriptor; void finalizeAsPolyline(const vector3d& ref_normal = {1, 0, 0}) { descriptor.type = AXIS_TYPE_POLYLINE; descriptor.data.polyline.point_number = static_cast(points.size()); descriptor.data.polyline.points = points.data(); descriptor.data.polyline.bulge_number = static_cast(bulges.size()); descriptor.data.polyline.bulges = bulges.data(); descriptor.data.polyline.is_close = false; descriptor.data.polyline.reference_normal = ref_normal; } }; static AxisData createStraightAxis(double length = 2.0) { AxisData data; data.points = { {0, 0, 0 }, {0, 0, length} }; data.bulges = {0.0}; data.finalizeAsPolyline(); return data; } static AxisData createSlantedAxis(const Eigen::Vector3d& start, const Eigen::Vector3d& end) { AxisData data; data.points = { {start.x(), start.y(), start.z()}, {end.x(), end.y(), end.z() } }; data.bulges = {0.0}; data.finalizeAsPolyline(); return data; } static AxisData createPolylineAxis(const std::vector& waypoints) { AxisData data; for (const auto& pt : waypoints) { data.points.push_back({pt.x(), pt.y(), pt.z()}); } data.bulges.resize(waypoints.size() - 1, 0.0); data.finalizeAsPolyline(); return data; } static AxisData createZigZagAxis(double segment_length = 1.0, int num_segments = 3) { AxisData data; for (int i = 0; i <= num_segments; ++i) { double x = (i % 2 == 0) ? 0.0 : 0.5; double z = i * segment_length; data.points.push_back({x, 0, z}); } data.bulges.resize(num_segments, 0.0); data.finalizeAsPolyline(); return data; } static AxisData createArcAxis(double radius = 1.0) { AxisData data; data.points = { {0, 0, 0 }, {radius, 0, radius} }; data.bulges = {0.3}; data.finalizeAsPolyline({0, 1, 0}); return data; } static AxisData createSpiralAxis(double radius = 1.0, double height = 2.0, int turns = 2) { AxisData data; int segments = turns * 8; // 每圈 8 段 for (int i = 0; i <= segments; ++i) { double t = static_cast(i) / segments; double angle = 2.0 * M_PI * turns * t; data.points.push_back({radius * std::cos(angle), radius * std::sin(angle), height * t}); } data.bulges.resize(segments, 0.0); data.finalizeAsPolyline(); return data; } static AxisData createUShapeAxis(double width = 2.0, double height = 1.0) { AxisData data; data.points = { {0, 0, 0 }, {0, 0, height}, {width, 0, height}, {width, 0, 0 } }; data.bulges = {0.0, 0.0, 0.0}; data.finalizeAsPolyline({0, 1, 0}); return data; } }; // ============================================================================ // 测试辅助工具(新架构适配) // ============================================================================ class TestHelper { public: /** * @brief 安全销毁 primitive(新架构简化版) */ static void safeFree(primitive* ptr) { if (!ptr) return; ptr->destroy(); mi_free(ptr); } static bool verifyAABB(const aabb_t& aabb, const Eigen::Vector3d& expected_min, const Eigen::Vector3d& expected_max, double tolerance = 1e-5) { bool min_ok = (aabb.min() - expected_min).norm() < tolerance; bool max_ok = (aabb.max() - expected_max).norm() < tolerance; if (!min_ok || !max_ok) { std::cout << " AABB mismatch:\n"; std::cout << " Expected min: " << expected_min.transpose() << "\n"; std::cout << " Actual min: " << aabb.min().transpose() << "\n"; std::cout << " Expected max: " << expected_max.transpose() << "\n"; std::cout << " Actual max: " << aabb.max().transpose() << "\n"; } return min_ok && max_ok; } static bool verifySDF(double sdf, const std::string& expected_region, double surface_tol = 1e-4) { if (expected_region == "inside") return sdf < surface_tol; if (expected_region == "outside") return sdf > -surface_tol; if (expected_region == "surface") return std::abs(sdf) < surface_tol; return false; } static void printStatistics(int passed, int total) { std::cout << "\n╔══════════════════════════════════════════╗\n"; std::cout << "║ TEST RESULTS SUMMARY ║\n"; std::cout << "╠══════════════════════════════════════════╣\n"; std::cout << "║ Total Tests: " << std::setw(4) << total << " ║\n"; std::cout << "║ Passed: " << std::setw(4) << passed << " ║\n"; std::cout << "║ Failed: " << std::setw(4) << (total - passed) << " ║\n"; std::cout << "║ Success Rate: " << std::fixed << std::setprecision(1) << std::setw(5) << (100.0 * passed / total) << "% ║\n"; std::cout << "╚══════════════════════════════════════════╝\n"; } }; // ============================================================================ // GROUP 1: Polyline Geometry 基础测试 // ============================================================================ bool test_polyline_basic_shapes() { TEST_SECTION("Polyline Geometry - Basic Shapes Construction"); Eigen::Vector3d proj_x(1, 0, 0); Eigen::Vector3d proj_y(0, 1, 0); Eigen::Vector3d origin(0, 0, 0); internal::aabb_t_dim<2> aabb; // 测试1: 正方形 { auto profile = TestDataFactory::createSquareProfile(1.0); internal::polyline_geometry_data geom; geom.build_as_profile(*reinterpret_cast(&profile.descriptor), proj_x, proj_y, origin, aabb); TEST_ASSERT(geom.vertices.size() == 5, "Square: 5 vertices (closed)"); TEST_ASSERT(geom.start_indices.size() == 4, "Square: 4 segments"); TEST_APPROX_EQUAL(aabb.min().x(), -0.5, 1e-6, "Square AABB min X"); TEST_APPROX_EQUAL(aabb.max().x(), 0.5, 1e-6, "Square AABB max X"); } // 测试2: 圆形(圆弧) { auto profile = TestDataFactory::createCircleProfile(0.5); internal::polyline_geometry_data geom; geom.build_as_profile(*reinterpret_cast(&profile.descriptor), proj_x, proj_y, origin, aabb); TEST_ASSERT(geom.vertices.size() == 7, "Circle: 7 vertices"); TEST_ASSERT(geom.start_indices.size() == 2, "Circle: 2 arc segments"); TEST_APPROX_EQUAL(aabb.min().x(), -0.5, 1e-6, "Circle AABB min X"); TEST_APPROX_EQUAL(aabb.max().x(), 0.5, 1e-6, "Circle AABB max X"); } // 测试3: 三角形 { auto profile = TestDataFactory::createTriangleProfile(1.0); internal::polyline_geometry_data geom; geom.build_as_profile(*reinterpret_cast(&profile.descriptor), proj_x, proj_y, origin, aabb); TEST_ASSERT(geom.vertices.size() == 4, "Triangle: 4 vertices (closed)"); TEST_ASSERT(geom.start_indices.size() == 3, "Triangle: 3 segments"); } return true; } bool test_polyline_sdf_calculation() { TEST_SECTION("Polyline Geometry - SDF Calculation"); auto profile = TestDataFactory::createSquareProfile(1.0); Eigen::Vector3d proj_x(1, 0, 0); Eigen::Vector3d proj_y(0, 1, 0); Eigen::Vector3d origin(0, 0, 0); internal::aabb_t_dim<2> aabb; internal::polyline_geometry_data geom; geom.build_as_profile(*reinterpret_cast(&profile.descriptor), proj_x, proj_y, origin, aabb); // 测试内部点 Eigen::Vector3d inside_point(0, 0, 0); auto closest_inside = geom.calculate_closest_param(inside_point); bool inside_pmc = geom.pmc(inside_point.head<2>(), closest_inside.first); double sdf_inside = inside_pmc ? std::abs(closest_inside.second) : -std::abs(closest_inside.second); TEST_ASSERT(sdf_inside < 0, "Inside point has negative SDF"); // 测试外部点 Eigen::Vector3d outside_point(1, 0, 0); auto closest_outside = geom.calculate_closest_param(outside_point); bool outside_pmc = geom.pmc(outside_point.head<2>(), closest_outside.first); double sdf_outside = outside_pmc ? std::abs(closest_outside.second) : -std::abs(closest_outside.second); TEST_ASSERT(sdf_outside > 0, "Outside point has positive SDF"); // 测试边界点 Eigen::Vector3d boundary_point(0.5, 0, 0); auto closest_boundary = geom.calculate_closest_param(boundary_point); bool boundary_pmc = geom.pmc(boundary_point.head<2>(), closest_boundary.first); double sdf_boundary = boundary_pmc ? std::abs(closest_boundary.second) : -std::abs(closest_boundary.second); TEST_APPROX_EQUAL(sdf_boundary, 0.0, 1e-6, "Boundary point SDF close to zero"); return true; } bool test_polyline_closest_point() { TEST_SECTION("Polyline Geometry - Closest Point Calculation"); auto profile = TestDataFactory::createSquareProfile(1.0); Eigen::Vector3d proj_x(1, 0, 0); Eigen::Vector3d proj_y(0, 1, 0); Eigen::Vector3d origin(0, 0, 0); internal::aabb_t_dim<2> aabb; internal::polyline_geometry_data geom; geom.build_as_profile(*reinterpret_cast(&profile.descriptor), proj_x, proj_y, origin, aabb); // 测试1: 点在边上 Eigen::Vector3d point_on_edge(0.5, 0.25, 0); auto result_edge = geom.calculate_closest_param(point_on_edge); TEST_APPROX_EQUAL(result_edge.first.point.x(), 0.5, 1e-6, "Closest point X on edge"); TEST_APPROX_EQUAL(result_edge.first.point.y(), 0.25, 1e-6, "Closest point Y on edge"); // 测试2: 点在角附近 Eigen::Vector3d point_near_corner(0.6, 0.6, 0); auto result_corner = geom.calculate_closest_param(point_near_corner); TEST_ASSERT(result_corner.second < 0.2, "Distance to corner is small"); return true; } bool test_polyline_polygon_profiles() { TEST_SECTION("Polyline Geometry - Polygon Profiles"); Eigen::Vector3d proj_x(1, 0, 0); Eigen::Vector3d proj_y(0, 1, 0); Eigen::Vector3d origin(0, 0, 0); internal::aabb_t_dim<2> aabb; // 测试五边形 { auto profile = TestDataFactory::createPentagonProfile(0.5); internal::polyline_geometry_data geom; geom.build_as_profile(*reinterpret_cast(&profile.descriptor), proj_x, proj_y, origin, aabb); TEST_ASSERT(geom.vertices.size() == 6, "Pentagon: 6 vertices (5 + close)"); TEST_ASSERT(geom.start_indices.size() == 5, "Pentagon: 5 segments"); } // 测试星形 { auto profile = TestDataFactory::createStarProfile(0.5, 0.25); internal::polyline_geometry_data geom; geom.build_as_profile(*reinterpret_cast(&profile.descriptor), proj_x, proj_y, origin, aabb); TEST_ASSERT(geom.vertices.size() == 11, "Star: 11 vertices (10 + close)"); TEST_ASSERT(geom.start_indices.size() == 10, "Star: 10 segments"); } return true; } // ============================================================================ // GROUP 2: Polyline Extrude Subface 函数测试(新架构适配) // ============================================================================ bool test_polyline_eval_sdf_detailed() { TEST_SECTION("Polyline SDF Evaluation - Detailed"); primitive_data_center_t dc; auto profile = TestDataFactory::createSquareProfile(1.0); auto axis = TestDataFactory::createStraightAxis(2.0); // ✅ 新架构:添加 &dc auto* prim = static_cast(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_POLYLINE, &dc)); extrude_polyline_descriptor_t desc; desc.profile_number = 1; desc.profiles = const_cast(reinterpret_cast(&profile.descriptor)); desc.axis = axis.descriptor.data.polyline; prim->initialize_with_components(&dc, desc); auto subfaces = prim->get_subfaces(); auto side_face = subfaces[0].get_ptr(); // 测试内部点 Eigen::Vector3d inside_point(0, 0, 1); double sdf_inside = internal::get_eval_sdf_ptr(surface_type::extrude_polyline_side)(make_pointer_wrapper(side_face), inside_point); TEST_ASSERT(sdf_inside < 0, "Inside point has negative SDF: " << sdf_inside); // 测试外部点 Eigen::Vector3d outside_point(1, 0, 1); double sdf_outside = internal::get_eval_sdf_ptr(surface_type::extrude_polyline_side)(make_pointer_wrapper(side_face), outside_point); TEST_ASSERT(sdf_outside > 0, "Outside point has positive SDF: " << sdf_outside); TEST_APPROX_EQUAL(std::abs(sdf_outside), 0.5, 1e-6, "Outside point SDF magnitude"); // 测试边界点 Eigen::Vector3d boundary_point(0.5, 0, 1); double sdf_boundary = internal::get_eval_sdf_ptr(surface_type::extrude_polyline_side)(make_pointer_wrapper(side_face), boundary_point); TEST_APPROX_EQUAL(sdf_boundary, 0.0, 1e-6, "Boundary point SDF close to zero"); // 测试远点 Eigen::Vector3d far_point(5, 0, 1); double sdf_far = internal::get_eval_sdf_ptr(surface_type::extrude_polyline_side)(make_pointer_wrapper(side_face), far_point); TEST_ASSERT(sdf_far > 4.0, "Far point has large positive SDF: " << sdf_far); TestHelper::safeFree(prim); return true; } bool test_polyline_eval_sdf_grad() { TEST_SECTION("Polyline SDF Gradient Evaluation"); primitive_data_center_t dc; auto profile = TestDataFactory::createSquareProfile(1.0); auto axis = TestDataFactory::createStraightAxis(2.0); auto* prim = static_cast(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_POLYLINE, &dc)); extrude_polyline_descriptor_t desc; desc.profile_number = 1; desc.profiles = const_cast(reinterpret_cast(&profile.descriptor)); desc.axis = axis.descriptor.data.polyline; prim->initialize_with_components(&dc, desc); auto subfaces = prim->get_subfaces(); auto side_face = subfaces[0].get_ptr(); // 测试梯度计算 Eigen::Vector3d test_point(1, 0, 1); Eigen::Vector3d gradient = internal::get_eval_sdf_grad_ptr(surface_type::extrude_polyline_side)(make_pointer_wrapper(side_face), test_point); // 梯度应该是单位向量 TEST_APPROX_EQUAL(gradient.norm(), 1.0, 1e-6, "Gradient is normalized"); // 梯度应该指向外部(X方向) TEST_ASSERT(gradient.x() > 0.9, "Gradient points outward in X direction: " << gradient.transpose()); TEST_APPROX_EQUAL(gradient.y(), 0.0, 1e-6, "Gradient Y component near zero"); TEST_APPROX_EQUAL(gradient.z(), 0.0, 1e-6, "Gradient Z component near zero"); TestHelper::safeFree(prim); return true; } bool test_polyline_map_param_roundtrip() { TEST_SECTION("Polyline Parametrization Round-Trip (FIXED)"); primitive_data_center_t dc; auto profile = TestDataFactory::createSquareProfile(1.0); auto axis = TestDataFactory::createStraightAxis(2.0); auto* prim = static_cast(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_POLYLINE, &dc)); extrude_polyline_descriptor_t desc; desc.profile_number = 1; desc.profiles = const_cast(reinterpret_cast(&profile.descriptor)); desc.axis = axis.descriptor.data.polyline; prim->initialize_with_components(&dc, desc); auto subfaces = prim->get_subfaces(); auto side_face = subfaces[0].get_ptr(); auto map_param_to_point_with_weight_func = internal::get_map_param_to_point_with_weight_ptr(surface_type::extrude_polyline_side); auto map_to_param = internal::get_map_point_to_param_ptr(surface_type::extrude_polyline_side); std::cout << "\n[Round-Trip Test] Testing 6 points:\n"; std::vector test_params = { {0.25, 0.0 }, {0.75, 0.25}, {1.5, 0.5 }, {2.5, 0.75}, {3.25, 1.0 }, {0.0, 0.5 } }; for (const auto& uv : test_params) { auto [point1, surface_jacobi1, volume_jacobi1] = map_param_to_point_with_weight_func(make_pointer_wrapper(static_cast(side_face)), uv); Eigen::Vector2d uv2 = map_to_param(make_pointer_wrapper(side_face), point1.head<3>()); auto [point2, surface_jacobi2, volume_jacobi2] = map_param_to_point_with_weight_func(make_pointer_wrapper(static_cast(side_face)), uv2); double point_error = (point1 - point2).head<3>().norm(); TEST_ASSERT(point_error < 1e-6, "Point round-trip for (u,v) = " << uv.transpose() << " (point error: " << point_error << ")"); } TestHelper::safeFree(prim); return true; } bool test_polyline_constraint_curves() { TEST_SECTION("Polyline Constraint Curve Consistency"); primitive_data_center_t dc; auto profile = TestDataFactory::createSquareProfile(1.0); auto axis = TestDataFactory::createStraightAxis(2.0); auto* prim = static_cast(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_POLYLINE, &dc)); extrude_polyline_descriptor_t desc; desc.profile_number = 1; desc.profiles = const_cast(reinterpret_cast(&profile.descriptor)); desc.axis = axis.descriptor.data.polyline; prim->initialize_with_components(&dc, desc); auto subfaces = prim->get_subfaces(); auto side_face = subfaces[0].get_ptr(); auto eval_du = internal::get_eval_du_constraint_ptr(surface_type::extrude_polyline_side); auto eval_dv = internal::get_eval_dv_constraint_ptr(surface_type::extrude_polyline_side); auto map_param_to_point_with_weight_func = internal::get_map_param_to_point_with_weight_ptr(surface_type::extrude_polyline_side); Eigen::Vector2d test_uv(1.5, 0.5); // 测试 du constraint auto du_result = eval_du(make_pointer_wrapper(side_face), test_uv); auto [point, surface_jacobi, volume_jacobi] = map_param_to_point_with_weight_func(make_pointer_wrapper(static_cast(side_face)), test_uv); TEST_VECTOR_APPROX_EQUAL(du_result.f.head<3>(), point.head<3>(), 1e-6, "du constraint returns correct point"); // 验证梯度(数值微分) constexpr double eps = 1e-6; auto [point_u, surface_jacobi_u, volume_jacobi_u] = map_param_to_point_with_weight_func(make_pointer_wrapper(static_cast(side_face)), Eigen::Vector2d(test_uv.x() + eps, test_uv.y())); Eigen::Vector3d grad_numerical = (point_u - point).head<3>() / eps; TEST_VECTOR_APPROX_EQUAL(du_result.grad_f.head<3>(), grad_numerical, 1e-5, "du constraint gradient matches numerical derivative"); // 测试 dv constraint auto dv_result = eval_dv(make_pointer_wrapper(side_face), test_uv); TEST_VECTOR_APPROX_EQUAL(dv_result.f.head<3>(), point.head<3>(), 1e-6, "dv constraint returns correct point"); auto [point_v, surface_jacobi_v, volume_jacobi_v] = map_param_to_point_with_weight_func(make_pointer_wrapper(static_cast(side_face)), Eigen::Vector2d(test_uv.x(), test_uv.y() + eps)); Eigen::Vector3d grad_v_numerical = (point_v - point).head<3>() / eps; TEST_VECTOR_APPROX_EQUAL(dv_result.grad_f.head<3>(), grad_v_numerical, 1e-5, "dv constraint gradient matches numerical derivative"); TestHelper::safeFree(prim); return true; } bool test_polyline_geometry_accessor() { TEST_SECTION("Polyline Descriptor Accessor"); primitive_data_center_t dc; auto profile = TestDataFactory::createSquareProfile(1.0); auto axis = TestDataFactory::createStraightAxis(2.0); auto* prim = static_cast(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_POLYLINE, &dc)); extrude_polyline_descriptor_t desc; desc.profile_number = 1; desc.profiles = const_cast(reinterpret_cast(&profile.descriptor)); desc.axis = axis.descriptor.data.polyline; prim->initialize_with_components(&dc, desc); auto subfaces = prim->get_subfaces(); auto side_face = subfaces[0].get_ptr(); auto get_descriptor = internal::get_geometry_accessor_ptr(surface_type::extrude_polyline_side); auto desc_ptr = get_descriptor(make_pointer_wrapper(side_face)); TEST_ASSERT(desc_ptr != nullptr, "Geometry accessor returns non-null pointer"); // 验证是同一个descriptor auto* face = static_cast(side_face); auto* expected_ptr = face->get_geometry_ptr(); TEST_ASSERT(desc_ptr == expected_ptr, "Geometry accessor returns correct pointer"); TestHelper::safeFree(prim); return true; } /* bool test_polyline_subface_equal() { TEST_SECTION("Polyline Subface Equality"); primitive_data_center_t dc; auto profile = TestDataFactory::createSquareProfile(1.0); auto axis = TestDataFactory::createStraightAxis(2.0); // 创建两个相同的extrude auto* prim1 = static_cast(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_POLYLINE, &dc)); prim1->initialize_with_components(&dc, profile.descriptor, axis.descriptor); auto* prim2 = static_cast(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_POLYLINE, &dc)); prim2->initialize_with_components(&dc, profile.descriptor, axis.descriptor); auto face1 = prim1->get_subfaces()[0].get_ptr(); auto face2 = prim2->get_subfaces()[0].get_ptr(); auto equal_func = internal::get_subface_equal_ptr(surface_type::extrude_polyline_side); bool are_equal = equal_func(make_pointer_wrapper(face1), make_pointer_wrapper(face2)); TEST_ASSERT(are_equal, "Identical subfaces are equal"); bool self_equal = equal_func(make_pointer_wrapper(face1), make_pointer_wrapper(face1)); TEST_ASSERT(self_equal, "Subface equals itself"); TestHelper::safeFree(prim1); TestHelper::safeFree(prim2); return true; } */ // ============================================================================ // GROUP 3: Geometry Data 内部函数测试 // ============================================================================ bool test_polyline_pmc_inside_outside() { TEST_SECTION("Polyline PMC (Point-in-Polygon) - FIXED"); auto profile = TestDataFactory::createSquareProfile(2.0); Eigen::Vector3d proj_x(1, 0, 0); Eigen::Vector3d proj_y(0, 1, 0); Eigen::Vector3d origin(0, 0, 0); internal::aabb_t_dim<2> aabb; internal::polyline_geometry_data geom; geom.build_as_profile(*reinterpret_cast(&profile.descriptor), proj_x, proj_y, origin, aabb); // 测试内部点 Eigen::Vector3d inside_point(0, 0, 0); auto closest_inside = geom.calculate_closest_param(inside_point); bool pmc_inside = geom.pmc(inside_point.head<2>(), closest_inside.first); TEST_ASSERT(!pmc_inside, "Point (0,0) is inside square"); // 测试外部点 Eigen::Vector3d outside_point(2, 2, 0); auto closest_outside = geom.calculate_closest_param(outside_point); bool pmc_outside = geom.pmc(outside_point.head<2>(), closest_outside.first); TEST_ASSERT(pmc_outside, "Point (2,2) is outside square"); return true; } bool test_polyline_isEnd() { TEST_SECTION("Polyline isEnd() Function"); auto profile = TestDataFactory::createSquareProfile(1.0); Eigen::Vector3d proj_x(1, 0, 0); Eigen::Vector3d proj_y(0, 1, 0); Eigen::Vector3d origin(0, 0, 0); internal::aabb_t_dim<2> aabb; internal::polyline_geometry_data geom; geom.build_as_profile(*reinterpret_cast(&profile.descriptor), proj_x, proj_y, origin, aabb); size_t max_t = geom.thetas.size(); // 测试起点 TEST_ASSERT(geom.isEnd(0.0), "t=0 is an endpoint"); // 测试终点 TEST_ASSERT(geom.isEnd(static_cast(max_t)), "t=max is an endpoint"); // 测试中间点 TEST_ASSERT(!geom.isEnd(0.5), "t=0.5 is not an endpoint"); TEST_ASSERT(!geom.isEnd(1.0), "t=1.0 is not an endpoint"); return true; } bool test_polyline_normal_calculation() { TEST_SECTION("Polyline Normal Calculation"); auto profile = TestDataFactory::createSquareProfile(1.0); Eigen::Vector3d proj_x(1, 0, 0); Eigen::Vector3d proj_y(0, 1, 0); Eigen::Vector3d origin(0, 0, 0); internal::aabb_t_dim<2> aabb; internal::polyline_geometry_data geom; geom.build_as_profile(*reinterpret_cast(&profile.descriptor), proj_x, proj_y, origin, aabb); // 测试几个不同的参数值 std::vector test_params = {0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5}; for (double t : test_params) { Eigen::Vector2d normal = geom.calculate_normal(t); Eigen::Vector2d tangent = geom.calculate_tangent(t); // 法线应该是单位向量 TEST_APPROX_EQUAL(normal.norm(), 1.0, 1e-6, "Normal is unit vector at t=" << t); // 切线应该是单位向量 TEST_APPROX_EQUAL(tangent.norm(), 1.0, 1e-6, "Tangent is unit vector at t=" << t); // 法线和切线应该垂直 double dot = normal.dot(tangent); TEST_APPROX_EQUAL(dot, 0.0, 1e-6, "Normal perpendicular to tangent at t=" << t); } return true; } bool test_pmc_vertex_convex_corner() { TEST_SECTION("PMC Vertex Branch - Convex Corner (Square, OR Logic)"); auto profile = TestDataFactory::createSquareProfile(1.0); Eigen::Vector3d proj_x(1, 0, 0); Eigen::Vector3d proj_y(0, 1, 0); Eigen::Vector3d origin(0, 0, 0); internal::aabb_t_dim<2> aabb; internal::polyline_geometry_data geom; geom.build_as_profile(*reinterpret_cast(&profile.descriptor), proj_x, proj_y, origin, aabb); { Eigen::Vector3d p3d(-0.6, 0.6, 0.0); auto result = geom.calculate_closest_param(p3d); TEST_ASSERT(!result.first.is_peak_value, "Point (-0.6,0.6) near convex corner: vertex branch must be active (is_peak_value=false)"); bool pmc_result = geom.pmc(p3d.head<2>(), result.first); TEST_ASSERT(pmc_result, "Point (-0.6,0.6) is outside square: PMC must return true (OUTSIDE)"); } { Eigen::Vector3d p3d(0.0, 0.0, 0.0); auto result = geom.calculate_closest_param(p3d); TEST_ASSERT(result.first.is_peak_value, "Center (0,0): closest point is on a segment interior (is_peak_value=true)"); bool pmc_result = geom.pmc(p3d.head<2>(), result.first); TEST_ASSERT(!pmc_result, "Center (0,0) is inside square: PMC must return false (INSIDE)"); } return true; } bool test_pmc_vertex_reflex_corner() { TEST_SECTION("PMC Vertex Branch - Reflex Corner (L-Shape, AND Logic)"); auto profile = TestDataFactory::createLShapeProfile(1.0); Eigen::Vector3d proj_x(1, 0, 0); Eigen::Vector3d proj_y(0, 1, 0); Eigen::Vector3d origin(0, 0, 0); internal::aabb_t_dim<2> aabb; internal::polyline_geometry_data geom; geom.build_as_profile(*reinterpret_cast(&profile.descriptor), proj_x, proj_y, origin, aabb); { Eigen::Vector3d p3d(0.4, 0.4, 0.0); auto result = geom.calculate_closest_param(p3d); TEST_ASSERT(!result.first.is_peak_value, "Point (0.4,0.4) near reflex corner: vertex branch must be active (is_peak_value=false)"); bool pmc_result = geom.pmc(p3d.head<2>(), result.first); TEST_ASSERT(!pmc_result, "Point (0.4,0.4) is inside L-shape: PMC must return false (INSIDE)"); } return true; } bool test_replace_profile() { TEST_SECTION("Profile Replacement"); primitive_data_center_t dc; auto profile1 = TestDataFactory::createSquareProfile(0.5); auto axis = TestDataFactory::createStraightAxis(2.0); auto* prim = static_cast(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_POLYLINE, &dc)); extrude_polyline_descriptor_t desc; desc.profile_number = 1; desc.profiles = const_cast(reinterpret_cast(&profile1.descriptor)); desc.axis = axis.descriptor.data.polyline; prim->initialize_with_components(&dc, desc); auto side_face_before = prim->subfaces[0].get_ptr(); Eigen::Vector3d test_point(0.3, 0, 1); double sdf_before = internal::get_eval_sdf_ptr(surface_type::extrude_polyline_side)(make_pointer_wrapper(side_face_before), test_point); // 替换为更大的Profile auto profile2 = TestDataFactory::createSquareProfile(1.0); prim->replace_profile(profile2.descriptor); auto side_face_after = prim->subfaces[0].get_ptr(); double sdf_after = internal::get_eval_sdf_ptr(surface_type::extrude_polyline_side)(make_pointer_wrapper(side_face_after), test_point); TEST_ASSERT(sdf_after < sdf_before, "Larger profile should have more negative SDF at same point"); TestHelper::safeFree(prim); return true; } bool test_replace_axis() { TEST_SECTION("Axis Replacement"); primitive_data_center_t dc; auto profile = TestDataFactory::createSquareProfile(0.5); auto axis1 = TestDataFactory::createStraightAxis(2.0); auto* prim = static_cast(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_POLYLINE, &dc)); extrude_polyline_descriptor_t desc; desc.profile_number = 1; desc.profiles = const_cast(reinterpret_cast(&profile.descriptor)); desc.axis = axis1.descriptor.data.polyline; prim->initialize_with_components(&dc, desc); aabb_t aabb_before = prim->fetch_aabb(); // 替换为更长的Axis auto axis2 = TestDataFactory::createStraightAxis(4.0); prim->replace_axis(axis2.descriptor); aabb_t aabb_after = prim->fetch_aabb(); TEST_ASSERT(aabb_after.max().z() > aabb_before.max().z(), "Longer axis should have larger AABB"); TestHelper::safeFree(prim); return true; } bool test_aabb_different_profiles() { TEST_SECTION("AABB for Different Profile Shapes"); primitive_data_center_t dc; auto axis = TestDataFactory::createStraightAxis(1.0); // 测试圆形 Profile { auto profile = TestDataFactory::createCircleProfile(0.5); auto* prim = static_cast(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_POLYLINE, &dc)); extrude_polyline_descriptor_t circle_desc; circle_desc.profile_number = 1; circle_desc.profiles = const_cast(reinterpret_cast(&profile.descriptor)); circle_desc.axis = axis.descriptor.data.polyline; prim->initialize_with_components(&dc, circle_desc); aabb_t aabb = prim->fetch_aabb(); TEST_APPROX_EQUAL(aabb.sizes().x(), 1.0, 0.1, "Circle AABB X size ≈ diameter"); TEST_APPROX_EQUAL(aabb.sizes().y(), 1.0, 0.1, "Circle AABB Y size ≈ diameter"); TestHelper::safeFree(prim); } // 测试三角形 Profile { auto profile = TestDataFactory::createTriangleProfile(1.0); auto* prim = static_cast(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_POLYLINE, &dc)); extrude_polyline_descriptor_t triangle_desc; triangle_desc.profile_number = 1; triangle_desc.profiles = const_cast(reinterpret_cast(&profile.descriptor)); triangle_desc.axis = axis.descriptor.data.polyline; prim->initialize_with_components(&dc, triangle_desc); aabb_t aabb = prim->fetch_aabb(); TEST_ASSERT(aabb.volume() > 0, "Triangle extrude has positive volume"); TestHelper::safeFree(prim); } return true; } // =========================================================================== // Main 测试运行器 // ============================================================================ int main() { SetConsoleOutputCP(CP_UTF8); struct TestCase { std::string name; std::function func; std::string group; }; std::vector tests = { // Group 1: Polyline Geometry {"Polyline Basic Shapes", test_polyline_basic_shapes, "Geometry" }, {"Polyline SDF Calculation", test_polyline_sdf_calculation, "Geometry" }, {"Polyline Closest Point", test_polyline_closest_point, "Geometry" }, {"Polyline Polygon Profiles", test_polyline_polygon_profiles, "Geometry" }, // Group 2: Subface Functions {"Polyline SDF Detailed", test_polyline_eval_sdf_detailed, "Subface" }, {"Polyline SDF Gradient", test_polyline_eval_sdf_grad, "Subface" }, {"Polyline Param Round-Trip", test_polyline_map_param_roundtrip, "Subface" }, {"Polyline Constraint Curves", test_polyline_constraint_curves, "Subface" }, {"Polyline Descriptor Accessor", test_polyline_geometry_accessor, "Subface" }, //{"Polyline Subface Equal", test_polyline_subface_equal, "Subface" }, // Group 3: Geometry Internals {"Polyline PMC Inside/Outside", test_polyline_pmc_inside_outside, "Internals"}, {"Polyline isEnd()", test_polyline_isEnd, "Internals"}, {"Polyline Normal Calculation", test_polyline_normal_calculation, "Internals"}, {"PMC Vertex Convex Corner", test_pmc_vertex_convex_corner, "Internals"}, {"PMC Vertex Reflex Corner", test_pmc_vertex_reflex_corner, "Internals"}, // Group 5: Replace {"Replace Profile", test_replace_profile, "Replace" }, {"Replace Axis", test_replace_axis, "Replace" }, // Group 8: AABB {"AABB Different Profiles", test_aabb_different_profiles, "AABB" }, }; int passed = 0; int total = static_cast(tests.size()); std::string current_group; for (const auto& test : tests) { if (test.group != current_group) { current_group = test.group; std::cout << "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; std::cout << " GROUP: " << current_group << "\n"; std::cout << "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"; } try { if (test.func()) { passed++; } } catch (const std::exception& e) { std::cerr << "❌ EXCEPTION in " << test.name << ": " << e.what() << "\n"; } catch (...) { std::cerr << "❌ UNKNOWN EXCEPTION in " << test.name << "\n"; } } TestHelper::printStatistics(passed, total); return (passed == total) ? 0 : 1; }