Temporary repository used to save branch code
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1200 lines
45 KiB

/**
* @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 <windows.h>
#include <iostream>
#include <iomanip>
#include <cmath>
#include <cassert>
#include <vector>
#include <functional>
#include <chrono>
#include <mimalloc.h>
#include <data/data_center.hpp>
#include <base/primitive.hpp>
#include <base/subface.hpp>
#include <primitive_descriptor.h>
#include <subface/geometry/geometry_data.hpp>
#include <primitive/simple/extrude.hpp>
// ============================================================================
// 测试框架宏定义
// ============================================================================
#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<vector3d> points;
std::vector<double> bulges;
profile_descriptor_t descriptor;
void finalize()
{
descriptor.point_number = static_cast<uint32_t>(points.size());
descriptor.points = points.data();
descriptor.bulge_number = static_cast<uint32_t>(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<vector3d> points;
std::vector<double> 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<uint32_t>(points.size());
descriptor.data.polyline.points = points.data();
descriptor.data.polyline.bulge_number = static_cast<uint32_t>(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<Eigen::Vector3d>& 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<double>(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<const polyline_descriptor_t*>(&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<const polyline_descriptor_t*>(&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<const polyline_descriptor_t*>(&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<const polyline_descriptor_t*>(&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<const polyline_descriptor_t*>(&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<const polyline_descriptor_t*>(&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<const polyline_descriptor_t*>(&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::extrude_polyline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_POLYLINE, &dc));
extrude_polyline_descriptor_t desc;
desc.profile_number = 1;
desc.profiles = const_cast<polyline_descriptor_t*>(reinterpret_cast<const polyline_descriptor_t*>(&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::extrude_polyline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_POLYLINE, &dc));
extrude_polyline_descriptor_t desc;
desc.profile_number = 1;
desc.profiles = const_cast<polyline_descriptor_t*>(reinterpret_cast<const polyline_descriptor_t*>(&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::extrude_polyline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_POLYLINE, &dc));
extrude_polyline_descriptor_t desc;
desc.profile_number = 1;
desc.profiles = const_cast<polyline_descriptor_t*>(reinterpret_cast<const polyline_descriptor_t*>(&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<Eigen::Vector2d> 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<subface*>(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<subface*>(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::extrude_polyline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_POLYLINE, &dc));
extrude_polyline_descriptor_t desc;
desc.profile_number = 1;
desc.profiles = const_cast<polyline_descriptor_t*>(reinterpret_cast<const polyline_descriptor_t*>(&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<subface*>(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<subface*>(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<subface*>(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::extrude_polyline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_POLYLINE, &dc));
extrude_polyline_descriptor_t desc;
desc.profile_number = 1;
desc.profiles = const_cast<polyline_descriptor_t*>(reinterpret_cast<const polyline_descriptor_t*>(&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<internal::extrude_polyline_side_face_t*>(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::extrude_polyline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_POLYLINE, &dc));
prim1->initialize_with_components(&dc, profile.descriptor, axis.descriptor);
auto* prim2 = static_cast<internal::extrude_polyline_t*>(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<const polyline_descriptor_t*>(&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<const polyline_descriptor_t*>(&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<double>(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<const polyline_descriptor_t*>(&profile.descriptor), proj_x, proj_y, origin, aabb);
// 测试几个不同的参数值
std::vector<double> 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<const polyline_descriptor_t*>(&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<const polyline_descriptor_t*>(&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::extrude_polyline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_POLYLINE, &dc));
extrude_polyline_descriptor_t desc;
desc.profile_number = 1;
desc.profiles = const_cast<polyline_descriptor_t*>(reinterpret_cast<const polyline_descriptor_t*>(&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::extrude_polyline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_POLYLINE, &dc));
extrude_polyline_descriptor_t desc;
desc.profile_number = 1;
desc.profiles = const_cast<polyline_descriptor_t*>(reinterpret_cast<const polyline_descriptor_t*>(&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::extrude_polyline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_POLYLINE, &dc));
extrude_polyline_descriptor_t circle_desc;
circle_desc.profile_number = 1;
circle_desc.profiles =
const_cast<polyline_descriptor_t*>(reinterpret_cast<const polyline_descriptor_t*>(&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::extrude_polyline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_POLYLINE, &dc));
extrude_polyline_descriptor_t triangle_desc;
triangle_desc.profile_number = 1;
triangle_desc.profiles =
const_cast<polyline_descriptor_t*>(reinterpret_cast<const polyline_descriptor_t*>(&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<bool(void)> func;
std::string group;
};
std::vector<TestCase> 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<int>(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;
}