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
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;
|
|
}
|