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.
1171 lines
48 KiB
1171 lines
48 KiB
#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:
|
|
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;
|
|
|
|
// CCW winding: matches unit_square_profile convention expected by helixline SDF.
|
|
// Order: bottom-left → bottom-right → top-right → top-left
|
|
// i.e. (-,-) → (+,-) → (+,+) → (-,+)
|
|
// Using CW winding here inverts the profile normal, which flips the SDF sign
|
|
// globally and causes all sign-dependent tests to fail.
|
|
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};
|
|
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;
|
|
|
|
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 createRectangleProfile(double width, double height)
|
|
{
|
|
ProfileData data;
|
|
double half_w = width / 2.0;
|
|
double half_h = height / 2.0;
|
|
|
|
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 axis_descriptor_t createHelixlineAxis(double radius = 1.0,
|
|
double height = 4.0,
|
|
double turns = 2.0,
|
|
bool righthanded = true)
|
|
{
|
|
axis_descriptor_t desc;
|
|
desc.type = AXIS_TYPE_HELIXLINE;
|
|
desc.data.helixline.axis_start = {0, 0, 0};
|
|
desc.data.helixline.axis_end = {0, 0, height};
|
|
desc.data.helixline.start_direction = {radius, 0, 0};
|
|
desc.data.helixline.radius = radius;
|
|
desc.data.helixline.advance_per_round = height / turns;
|
|
desc.data.helixline.is_righthanded = righthanded;
|
|
|
|
return desc;
|
|
}
|
|
|
|
static axis_descriptor_t createTightHelixAxis(double radius = 0.5, double height = 10.0)
|
|
{
|
|
return createHelixlineAxis(radius, height, 10.0, true);
|
|
}
|
|
|
|
static axis_descriptor_t createLooseHelixAxis(double radius = 2.0, double height = 2.0)
|
|
{
|
|
return createHelixlineAxis(radius, height, 0.5, true);
|
|
}
|
|
|
|
static axis_descriptor_t createSlantedHelixAxis(double radius,
|
|
const Eigen::Vector3d& start,
|
|
const Eigen::Vector3d& end,
|
|
double turns = 2.0)
|
|
{
|
|
axis_descriptor_t desc;
|
|
desc.type = AXIS_TYPE_HELIXLINE;
|
|
desc.data.helixline.axis_start = {start.x(), start.y(), start.z()};
|
|
desc.data.helixline.axis_end = {end.x(), end.y(), end.z()};
|
|
desc.data.helixline.start_direction = {radius, 0, 0};
|
|
desc.data.helixline.radius = radius;
|
|
double height = (end - start).norm();
|
|
desc.data.helixline.advance_per_round = height / turns;
|
|
desc.data.helixline.is_righthanded = true;
|
|
|
|
return desc;
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// 测试辅助工具(新架构适配)
|
|
// ============================================================================
|
|
|
|
class TestHelper
|
|
{
|
|
public:
|
|
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 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: Helixline Geometry 基础测试
|
|
// ============================================================================
|
|
|
|
bool test_helixline_construction()
|
|
{
|
|
TEST_SECTION("Helixline Geometry - Construction");
|
|
|
|
auto axis_desc = TestDataFactory::createHelixlineAxis(1.0, 4.0, 2.0, true);
|
|
internal::helixline_geometry_data geom;
|
|
Eigen::Transform<double, 3, Eigen::AffineCompact, Eigen::DontAlign> axis_to_world;
|
|
aabb_t aabb;
|
|
|
|
geom.build_from_descriptor(axis_desc.data.helixline, axis_to_world, aabb);
|
|
|
|
TEST_APPROX_EQUAL(geom.radius, 1.0, 1e-6, "Helixline radius");
|
|
TEST_APPROX_EQUAL(geom.height, 4.0, 1e-6, "Helixline height");
|
|
TEST_APPROX_EQUAL(geom.total_theta, 2.0 * 2.0 * M_PI, 1e-6, "Helixline total theta (4π for 2 turns)");
|
|
|
|
TEST_APPROX_EQUAL(aabb.min().x(), -1.0, 1e-6, "AABB min X");
|
|
TEST_APPROX_EQUAL(aabb.max().x(), 1.0, 1e-6, "AABB max X");
|
|
TEST_APPROX_EQUAL(aabb.min().z(), 0.0, 1e-6, "AABB min Z");
|
|
TEST_APPROX_EQUAL(aabb.max().z(), 4.0, 1e-6, "AABB max Z");
|
|
|
|
return true;
|
|
}
|
|
|
|
bool test_helixline_tangent_normal()
|
|
{
|
|
TEST_SECTION("Helixline Geometry - Tangent & Normal");
|
|
|
|
auto axis_desc = TestDataFactory::createHelixlineAxis(1.0, 4.0, 2.0, true);
|
|
internal::helixline_geometry_data geom;
|
|
Eigen::Transform<double, 3, Eigen::AffineCompact, Eigen::DontAlign> axis_to_world;
|
|
aabb_t aabb;
|
|
|
|
geom.build_from_descriptor(axis_desc.data.helixline, axis_to_world, aabb);
|
|
|
|
// 测试起点(t=0)
|
|
Eigen::Vector3d tangent_0 = geom.calculate_tangent(0.0);
|
|
Eigen::Vector3d normal_0 = geom.calculate_normal(0.0);
|
|
|
|
TEST_APPROX_EQUAL(tangent_0.norm(), 1.0, 1e-6, "Tangent at t=0 is normalized");
|
|
TEST_APPROX_EQUAL(normal_0.norm(), 1.0, 1e-6, "Normal at t=0 is normalized");
|
|
|
|
double dot_0 = tangent_0.dot(normal_0);
|
|
TEST_APPROX_EQUAL(dot_0, 0.0, 1e-6, "Tangent and normal are perpendicular at t=0");
|
|
|
|
// 测试中点(t=0.5)
|
|
Eigen::Vector3d tangent_mid = geom.calculate_tangent(0.5);
|
|
Eigen::Vector3d normal_mid = geom.calculate_normal(0.5);
|
|
|
|
TEST_APPROX_EQUAL(tangent_mid.norm(), 1.0, 1e-6, "Tangent at t=0.5 is normalized");
|
|
TEST_APPROX_EQUAL(normal_mid.norm(), 1.0, 1e-6, "Normal at t=0.5 is normalized");
|
|
|
|
double dot_mid = tangent_mid.dot(normal_mid);
|
|
TEST_APPROX_EQUAL(dot_mid, 0.0, 1e-6, "Tangent and normal are perpendicular at t=0.5");
|
|
|
|
return true;
|
|
}
|
|
|
|
bool test_helixline_closest_point()
|
|
{
|
|
TEST_SECTION("Helixline Geometry - Closest Point");
|
|
|
|
auto axis_desc = TestDataFactory::createHelixlineAxis(1.0, 2.0, 1.0, true);
|
|
internal::helixline_geometry_data geom;
|
|
Eigen::Transform<double, 3, Eigen::AffineCompact, Eigen::DontAlign> axis_to_world;
|
|
aabb_t aabb;
|
|
|
|
geom.build_from_descriptor(axis_desc.data.helixline, axis_to_world, aabb);
|
|
|
|
// 测试点在螺旋线上
|
|
Eigen::Vector3d point_on_helix(1, 0, 0);
|
|
auto result_on = geom.calculate_closest_param(point_on_helix);
|
|
|
|
double distance = (result_on.point - point_on_helix).norm();
|
|
|
|
TEST_APPROX_EQUAL(result_on.t, 0.0, 1e-5, "Closest t for point on helix start");
|
|
TEST_ASSERT(distance < 1e-5, "Distance to helix is very small");
|
|
|
|
// 测试点在中心轴附近
|
|
Eigen::Vector3d point_center(0, 0, 1);
|
|
auto result_center = geom.calculate_closest_param(point_center);
|
|
|
|
TEST_ASSERT(result_center.point.head<2>().norm() > 0.9, "Closest point should be on helix surface");
|
|
|
|
return true;
|
|
}
|
|
|
|
bool test_helixline_left_vs_right()
|
|
{
|
|
TEST_SECTION("Helixline Left-Handed vs Right-Handed");
|
|
|
|
// 右手螺旋
|
|
auto right_axis = TestDataFactory::createHelixlineAxis(1.0, 2.0, 1.0, true);
|
|
internal::helixline_geometry_data right_geom;
|
|
Eigen::Transform<double, 3, Eigen::AffineCompact, Eigen::DontAlign> right_transform;
|
|
aabb_t right_aabb;
|
|
|
|
right_geom.build_from_descriptor(right_axis.data.helixline, right_transform, right_aabb);
|
|
|
|
// 左手螺旋
|
|
auto left_axis = TestDataFactory::createHelixlineAxis(1.0, 2.0, 1.0, false);
|
|
internal::helixline_geometry_data left_geom;
|
|
Eigen::Transform<double, 3, Eigen::AffineCompact, Eigen::DontAlign> left_transform;
|
|
aabb_t left_aabb;
|
|
|
|
left_geom.build_from_descriptor(left_axis.data.helixline, left_transform, left_aabb);
|
|
|
|
// 在 t=0.25 处(1/4圈),法线方向应该相同
|
|
Eigen::Vector3d right_normal = right_geom.calculate_normal(0.25);
|
|
Eigen::Vector3d left_normal = left_geom.calculate_normal(0.25);
|
|
|
|
// 切线方向应该相反
|
|
Eigen::Vector3d right_tangent = right_geom.calculate_tangent(0.25);
|
|
Eigen::Vector3d left_tangent = left_geom.calculate_tangent(0.25);
|
|
|
|
Eigen::Vector3d right_world_tangent = right_transform.linear() * right_tangent;
|
|
Eigen::Vector3d left_world_tangent = left_transform.linear() * left_tangent;
|
|
|
|
TEST_ASSERT(right_normal.y() * left_normal.y() > 0, "Normals have same Y signs");
|
|
TEST_ASSERT(right_world_tangent.y() * left_world_tangent.y() < 0, "Tangents have opposite Y signs");
|
|
|
|
return true;
|
|
}
|
|
|
|
// ============================================================================
|
|
// GROUP 2: Helixline Extrude Subface 函数测试(新架构适配)
|
|
// ============================================================================
|
|
|
|
bool test_helixline_eval_sdf_basic()
|
|
{
|
|
TEST_SECTION("Helixline SDF Basic");
|
|
|
|
primitive_data_center_t dc;
|
|
auto profile = TestDataFactory::createSquareProfile(0.5);
|
|
auto axis = TestDataFactory::createHelixlineAxis(2.0, 2.0, 1.0, true);
|
|
|
|
// ✅ 新架构:添加 &dc
|
|
auto* prim = static_cast<internal::extrude_helixline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_HELIXLINE, &dc));
|
|
extrude_helixline_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.data.helixline;
|
|
prim->initialize_with_components(&dc, desc);
|
|
|
|
auto subfaces = prim->get_subfaces();
|
|
auto side_face = subfaces[0].get_ptr();
|
|
|
|
// 测试点在螺旋线起点附近(应该在Profile内部)
|
|
Eigen::Vector3d inside_point(1.95, 0, 0.01);
|
|
double sdf_inside =
|
|
internal::get_eval_sdf_ptr(surface_type::extrude_helixline_side)(make_pointer_wrapper(side_face), inside_point);
|
|
|
|
TEST_ASSERT(sdf_inside < 0.0, "Point inside helix should have negative SDF");
|
|
|
|
// 测试点在螺旋线外部
|
|
Eigen::Vector3d outside_point(4.0, 0, 0.1);
|
|
double sdf_outside =
|
|
internal::get_eval_sdf_ptr(surface_type::extrude_helixline_side)(make_pointer_wrapper(side_face), outside_point);
|
|
|
|
TEST_ASSERT(sdf_outside > 0.0, "Point outside helix should have positive SDF");
|
|
|
|
TestHelper::safeFree(prim);
|
|
return true;
|
|
}
|
|
|
|
bool test_helixline_parametrization_roundtrip()
|
|
{
|
|
TEST_SECTION("Helixline Parametrization Round-Trip");
|
|
|
|
primitive_data_center_t dc;
|
|
auto profile = TestDataFactory::createSquareProfile(2.0);
|
|
auto axis = TestDataFactory::createHelixlineAxis(5.0, 10.0, 2.0, true);
|
|
|
|
auto* prim = static_cast<internal::extrude_helixline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_HELIXLINE, &dc));
|
|
extrude_helixline_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.data.helixline;
|
|
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_helixline_side);
|
|
auto map_to_param = internal::get_map_point_to_param_ptr(surface_type::extrude_helixline_side);
|
|
|
|
std::vector<Eigen::Vector2d> test_params = {
|
|
{2.500, 0.500},
|
|
{3.999, 0.999},
|
|
{0.001, 0.001},
|
|
{1.234, 0.567}
|
|
};
|
|
|
|
constexpr double point_tol = 1e-2;
|
|
|
|
for (const auto& uv : test_params) {
|
|
auto [point, surface_jacobi, volume_jacobi] =
|
|
map_param_to_point_with_weight_func(make_pointer_wrapper(static_cast<subface*>(side_face)), uv);
|
|
Eigen::Vector2d uv_back = map_to_param(make_pointer_wrapper(side_face), point.head<3>());
|
|
auto [point_back, surface_jacobi_back, volume_jacobi_back] =
|
|
map_param_to_point_with_weight_func(make_pointer_wrapper(static_cast<subface*>(side_face)), uv_back);
|
|
|
|
double point_error = (point.head<3>() - point_back.head<3>()).norm();
|
|
TEST_ASSERT(point_error < point_tol, "Point round-trip error for uv=" << uv.transpose());
|
|
|
|
// 允许参数在模 segment_count 意义下相等(闭合曲线)
|
|
double u_diff = std::abs(uv_back.x() - uv.x());
|
|
double v_diff = std::abs(uv_back.y() - uv.y());
|
|
// Profile 有 4 个段,参数可能在 [0, 4) 循环
|
|
if (u_diff > 2.0) { // 如果差距超过半圈
|
|
u_diff = std::abs(u_diff - 4.0);
|
|
}
|
|
TEST_ASSERT(u_diff < 1e-6, "U parameter mismatch for uv=" << uv.transpose() << " (diff=" << u_diff << ")");
|
|
TEST_ASSERT(v_diff < 1e-6, "V parameter mismatch for uv=" << uv.transpose() << " (diff=" << v_diff << ")");
|
|
}
|
|
|
|
TestHelper::safeFree(prim);
|
|
return true;
|
|
}
|
|
|
|
bool test_helixline_sdf_gradient()
|
|
{
|
|
TEST_SECTION("Helixline SDF Gradient");
|
|
|
|
primitive_data_center_t dc;
|
|
auto profile = TestDataFactory::createSquareProfile(0.3);
|
|
auto axis = TestDataFactory::createHelixlineAxis(1.0, 2.0, 1.0, true);
|
|
|
|
auto* prim = static_cast<internal::extrude_helixline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_HELIXLINE, &dc));
|
|
extrude_helixline_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.data.helixline;
|
|
prim->initialize_with_components(&dc, desc);
|
|
|
|
auto subfaces = prim->get_subfaces();
|
|
auto side_face = subfaces[0].get_ptr();
|
|
|
|
Eigen::Vector3d test_point(1.5, 0.2, 0.5);
|
|
Eigen::Vector3d gradient =
|
|
internal::get_eval_sdf_grad_ptr(surface_type::extrude_helixline_side)(make_pointer_wrapper(side_face), test_point);
|
|
|
|
double grad_norm = gradient.norm();
|
|
TEST_APPROX_EQUAL(grad_norm, 1.0, 1e-3, "Gradient should be unit vector");
|
|
|
|
TestHelper::safeFree(prim);
|
|
return true;
|
|
}
|
|
|
|
bool test_helixline_constraint_curves()
|
|
{
|
|
TEST_SECTION("Helixline Constraint Curves");
|
|
|
|
primitive_data_center_t dc;
|
|
auto profile = TestDataFactory::createSquareProfile(0.3);
|
|
auto axis = TestDataFactory::createHelixlineAxis(1.0, 2.0, 1.0, true);
|
|
|
|
auto* prim = static_cast<internal::extrude_helixline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_HELIXLINE, &dc));
|
|
extrude_helixline_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.data.helixline;
|
|
prim->initialize_with_components(&dc, desc);
|
|
|
|
auto subfaces = prim->get_subfaces();
|
|
auto side_face = subfaces[0].get_ptr();
|
|
|
|
Eigen::Vector2d uv(0.5, 0.25);
|
|
|
|
auto du_func = internal::get_eval_du_constraint_ptr(surface_type::extrude_helixline_side);
|
|
auto dv_func = internal::get_eval_dv_constraint_ptr(surface_type::extrude_helixline_side);
|
|
|
|
auto du_result = du_func(make_pointer_wrapper(side_face), uv);
|
|
auto dv_result = dv_func(make_pointer_wrapper(side_face), uv);
|
|
|
|
// 验证切向量非零
|
|
TEST_ASSERT(du_result.grad_f.norm() > 1e-6, "U-direction tangent is non-zero");
|
|
TEST_ASSERT(dv_result.grad_f.norm() > 1e-6, "V-direction tangent is non-zero");
|
|
|
|
// 验证两个方向的切向量不平行(叉积非零)
|
|
Eigen::Vector3d cross_product = du_result.grad_f.head<3>().cross(dv_result.grad_f.head<3>());
|
|
TEST_ASSERT(cross_product.norm() > 1e-6, "U and V tangents are not parallel");
|
|
|
|
TestHelper::safeFree(prim);
|
|
return true;
|
|
}
|
|
|
|
// ============================================================================
|
|
// GROUP 3: Handedness 测试
|
|
// ============================================================================
|
|
|
|
bool test_helixline_righthanded_vs_lefthanded()
|
|
{
|
|
TEST_SECTION("Helixline Right-Handed vs Left-Handed - Detailed");
|
|
|
|
primitive_data_center_t dc;
|
|
auto profile = TestDataFactory::createSquareProfile(0.3);
|
|
|
|
// 右手螺旋
|
|
auto right_axis = TestDataFactory::createHelixlineAxis(1.0, 2.0, 1.0, true);
|
|
|
|
auto* right_prim =
|
|
static_cast<internal::extrude_helixline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_HELIXLINE, &dc));
|
|
extrude_helixline_descriptor_t right_desc;
|
|
right_desc.profile_number = 1;
|
|
right_desc.profiles =
|
|
const_cast<polyline_descriptor_t*>(reinterpret_cast<const polyline_descriptor_t*>(&profile.descriptor));
|
|
right_desc.axis = right_axis.data.helixline;
|
|
right_prim->initialize_with_components(&dc, right_desc);
|
|
// 左手螺旋
|
|
auto left_axis = TestDataFactory::createHelixlineAxis(1.0, 2.0, 1.0, false);
|
|
|
|
auto* left_prim =
|
|
static_cast<internal::extrude_helixline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_HELIXLINE, &dc));
|
|
extrude_helixline_descriptor_t left_desc;
|
|
left_desc.profile_number = 1;
|
|
left_desc.profiles =
|
|
const_cast<polyline_descriptor_t*>(reinterpret_cast<const polyline_descriptor_t*>(&profile.descriptor));
|
|
left_desc.axis = left_axis.data.helixline;
|
|
left_prim->initialize_with_components(&dc, left_desc);
|
|
|
|
auto map_param_to_point_with_weight_func =
|
|
internal::get_map_param_to_point_with_weight_ptr(surface_type::extrude_helixline_side);
|
|
|
|
// 测试 1/4 圈位置
|
|
Eigen::Vector2d param_quarter(0, 0.25);
|
|
|
|
auto [right_point, right_surface_jacobi, right_volume_jacobi] =
|
|
map_param_to_point_with_weight_func(make_pointer_wrapper(right_prim->subfaces[0].get_ptr()), param_quarter);
|
|
auto [left_point, left_surface_jacobi, left_volume_jacobi] =
|
|
map_param_to_point_with_weight_func(make_pointer_wrapper(left_prim->subfaces[0].get_ptr()), param_quarter);
|
|
|
|
// 右手系:逆时针旋转,Y 坐标应该 > 0
|
|
// 左手系:顺时针旋转,Y 坐标应该 < 0
|
|
TEST_ASSERT(right_point.y() > 0, "Right-handed helix rotates counter-clockwise");
|
|
TEST_ASSERT(left_point.y() < 0, "Left-handed helix rotates clockwise");
|
|
|
|
// 验证X坐标相同(径向距离)
|
|
TEST_ASSERT(std::abs(right_point.x() - left_point.x()) < 1e-6, "X coordinates should match");
|
|
|
|
// Z坐标应该相同(而不是关于中心对称)
|
|
// 原因:左右螺旋是XZ平面的镜像,镜像操作中Z坐标不变
|
|
TEST_ASSERT(std::abs(right_point.z() - left_point.z()) < 1e-6,
|
|
"Z coordinates should be the same\n"
|
|
" Right: "
|
|
<< right_point.z()
|
|
<< "\n"
|
|
" Left: "
|
|
<< left_point.z()
|
|
<< "\n"
|
|
" Z-coordinates must be equal for XZ-plane mirror symmetry");
|
|
|
|
TestHelper::safeFree(right_prim);
|
|
TestHelper::safeFree(left_prim);
|
|
return true;
|
|
}
|
|
|
|
// ============================================================================
|
|
// GROUP 4-6: Replace 和 Profile 测试
|
|
// ============================================================================
|
|
|
|
bool test_helixline_replace_profile()
|
|
{
|
|
TEST_SECTION("Helixline Replace Profile");
|
|
|
|
primitive_data_center_t dc;
|
|
auto profile1 = TestDataFactory::createSquareProfile(0.3);
|
|
auto axis = TestDataFactory::createHelixlineAxis(1.0, 2.0, 1.0, true);
|
|
|
|
auto* prim = static_cast<internal::extrude_helixline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_HELIXLINE, &dc));
|
|
extrude_helixline_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.data.helixline;
|
|
prim->initialize_with_components(&dc, desc);
|
|
|
|
auto side_face_before = prim->subfaces[0].get_ptr();
|
|
Eigen::Vector3d test_point(1.2, 0, 1);
|
|
double sdf_before =
|
|
internal::get_eval_sdf_ptr(surface_type::extrude_helixline_side)(make_pointer_wrapper(side_face_before), test_point);
|
|
|
|
// 替换为更大的Profile
|
|
auto profile2 = TestDataFactory::createSquareProfile(0.6);
|
|
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_helixline_side)(make_pointer_wrapper(side_face_after), test_point);
|
|
|
|
TEST_ASSERT(sdf_after < sdf_before, "SDF should decrease with larger profile");
|
|
|
|
TestHelper::safeFree(prim);
|
|
return true;
|
|
}
|
|
|
|
bool test_helixline_replace_axis()
|
|
{
|
|
TEST_SECTION("Helixline Replace Axis");
|
|
|
|
primitive_data_center_t dc;
|
|
auto profile = TestDataFactory::createSquareProfile(0.3);
|
|
auto axis1 = TestDataFactory::createHelixlineAxis(1.0, 2.0, 1.0, true);
|
|
|
|
auto* prim = static_cast<internal::extrude_helixline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_HELIXLINE, &dc));
|
|
extrude_helixline_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.data.helixline;
|
|
prim->initialize_with_components(&dc, desc);
|
|
|
|
aabb_t aabb_before = prim->fetch_aabb();
|
|
|
|
// 替换为更大半径的Axis
|
|
auto axis2 = TestDataFactory::createHelixlineAxis(2.0, 2.0, 1.0, true);
|
|
prim->replace_axis(axis2);
|
|
|
|
aabb_t aabb_after = prim->fetch_aabb();
|
|
|
|
TEST_ASSERT(aabb_after.volume() > aabb_before.volume(), "AABB should grow with larger radius");
|
|
|
|
TestHelper::safeFree(prim);
|
|
return true;
|
|
}
|
|
|
|
bool test_helixline_with_circular_profile()
|
|
{
|
|
TEST_SECTION("Helixline with Circular Profile");
|
|
|
|
primitive_data_center_t dc;
|
|
auto profile = TestDataFactory::createCircleProfile(0.2);
|
|
auto axis = TestDataFactory::createHelixlineAxis(1.0, 2.0, 1.0, true);
|
|
|
|
auto* prim = static_cast<internal::extrude_helixline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_HELIXLINE, &dc));
|
|
extrude_helixline_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.data.helixline;
|
|
prim->initialize_with_components(&dc, desc);
|
|
|
|
TEST_ASSERT(prim != nullptr, "Circular profile helix created successfully");
|
|
|
|
aabb_t aabb = prim->fetch_aabb();
|
|
TEST_ASSERT(aabb.volume() > 0, "AABB has positive volume");
|
|
|
|
TestHelper::safeFree(prim);
|
|
return true;
|
|
}
|
|
|
|
bool test_helixline_with_triangular_profile()
|
|
{
|
|
TEST_SECTION("Helixline with Triangular Profile");
|
|
|
|
primitive_data_center_t dc;
|
|
auto profile = TestDataFactory::createTriangleProfile(0.4);
|
|
auto axis = TestDataFactory::createHelixlineAxis(1.0, 2.0, 1.0, true);
|
|
|
|
auto* prim = static_cast<internal::extrude_helixline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_HELIXLINE, &dc));
|
|
extrude_helixline_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.data.helixline;
|
|
prim->initialize_with_components(&dc, desc);
|
|
|
|
TEST_ASSERT(prim != nullptr, "Triangular profile helix created successfully");
|
|
|
|
TestHelper::safeFree(prim);
|
|
return true;
|
|
}
|
|
|
|
bool test_helixline_with_rectangular_profile()
|
|
{
|
|
TEST_SECTION("Helixline with Rectangular Profile");
|
|
|
|
primitive_data_center_t dc;
|
|
auto profile = TestDataFactory::createRectangleProfile(0.4, 0.2);
|
|
auto axis = TestDataFactory::createHelixlineAxis(1.0, 2.0, 1.0, true);
|
|
|
|
auto* prim = static_cast<internal::extrude_helixline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_HELIXLINE, &dc));
|
|
extrude_helixline_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.data.helixline;
|
|
prim->initialize_with_components(&dc, desc);
|
|
|
|
TEST_ASSERT(prim != nullptr, "Helixline with rectangular profile creates valid extrude");
|
|
|
|
aabb_t aabb = prim->fetch_aabb();
|
|
|
|
TEST_ASSERT(aabb.sizes().x() > 2.0, "AABB X size includes radius + profile width");
|
|
TEST_ASSERT(aabb.sizes().y() > 2.0, "AABB Y size includes radius + profile height");
|
|
|
|
TestHelper::safeFree(prim);
|
|
return true;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Main 测试运行器
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @brief 测试紧密螺旋(多圈)
|
|
* @details 验证:
|
|
* 1. 10 圈的螺旋能够正确创建
|
|
* 2. AABB 高度正确
|
|
*/
|
|
bool test_helixline_tight_spiral()
|
|
{
|
|
TEST_SECTION("Helixline Tight Spiral (Many Turns)");
|
|
|
|
primitive_data_center_t dc;
|
|
auto profile = TestDataFactory::createSquareProfile(0.2);
|
|
auto axis = TestDataFactory::createTightHelixAxis(0.5, 10.0); // 10圈
|
|
|
|
auto* prim = static_cast<internal::extrude_helixline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_HELIXLINE, &dc));
|
|
extrude_helixline_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.data.helixline;
|
|
prim->initialize_with_components(&dc, desc);
|
|
|
|
TEST_ASSERT(prim != nullptr, "Tight helix creates valid extrude");
|
|
|
|
aabb_t aabb = prim->fetch_aabb();
|
|
TEST_APPROX_EQUAL(aabb.max().z(), 10.0, 0.5, "AABB height matches helix height");
|
|
|
|
TestHelper::safeFree(prim);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief 测试疏松螺旋(少圈)
|
|
* @details 验证:
|
|
* 1. 0.5 圈的螺旋能够正确创建
|
|
*/
|
|
bool test_helixline_loose_spiral()
|
|
{
|
|
TEST_SECTION("Helixline Loose Spiral (Few Turns)");
|
|
|
|
primitive_data_center_t dc;
|
|
auto profile = TestDataFactory::createSquareProfile(0.5);
|
|
auto axis = TestDataFactory::createLooseHelixAxis(2.0, 2.0); // 0.5圈
|
|
|
|
auto* prim = static_cast<internal::extrude_helixline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_HELIXLINE, &dc));
|
|
extrude_helixline_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.data.helixline;
|
|
prim->initialize_with_components(&dc, desc);
|
|
|
|
TEST_ASSERT(prim != nullptr, "Loose helix creates valid extrude");
|
|
|
|
aabb_t aabb = prim->fetch_aabb();
|
|
TEST_APPROX_EQUAL(aabb.max().x(), 2.0 + 0.5, 0.5, "AABB includes helix radius + profile");
|
|
|
|
TestHelper::safeFree(prim);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief 测试极小半径螺旋
|
|
* @details 验证:
|
|
* 1. 半径 0.1 的螺旋能够正确创建
|
|
*/
|
|
bool test_helixline_very_small_radius()
|
|
{
|
|
TEST_SECTION("Helixline Very Small Radius");
|
|
|
|
primitive_data_center_t dc;
|
|
auto profile = TestDataFactory::createSquareProfile(0.05);
|
|
auto axis = TestDataFactory::createHelixlineAxis(0.1, 1.0, 1.0, true);
|
|
|
|
auto* prim = static_cast<internal::extrude_helixline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_HELIXLINE, &dc));
|
|
extrude_helixline_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.data.helixline;
|
|
prim->initialize_with_components(&dc, desc);
|
|
|
|
TEST_ASSERT(prim != nullptr, "Very small radius helix is valid");
|
|
|
|
aabb_t aabb = prim->fetch_aabb();
|
|
TEST_ASSERT(aabb.volume() > 0, "AABB has positive volume");
|
|
|
|
TestHelper::safeFree(prim);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief 测试大半径螺旋
|
|
* @details 验证:
|
|
* 1. 大半径螺旋的 AABB 正确
|
|
*/
|
|
bool test_helixline_large_radius()
|
|
{
|
|
TEST_SECTION("Helixline Large Radius");
|
|
|
|
primitive_data_center_t dc;
|
|
auto profile = TestDataFactory::createSquareProfile(0.5);
|
|
auto axis = TestDataFactory::createHelixlineAxis(10.0, 5.0, 1.0, true);
|
|
|
|
auto* prim = static_cast<internal::extrude_helixline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_HELIXLINE, &dc));
|
|
extrude_helixline_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.data.helixline;
|
|
prim->initialize_with_components(&dc, desc);
|
|
|
|
TEST_ASSERT(prim != nullptr, "Large radius helix is valid");
|
|
|
|
aabb_t aabb = prim->fetch_aabb();
|
|
TEST_ASSERT(aabb.sizes().x() > 20.0, "AABB X size > 2 * radius");
|
|
TEST_ASSERT(aabb.sizes().y() > 20.0, "AABB Y size > 2 * radius");
|
|
|
|
TestHelper::safeFree(prim);
|
|
return true;
|
|
}
|
|
|
|
// ============================================================================
|
|
// GROUP 7: 边界情况测试
|
|
// ============================================================================
|
|
|
|
/**
|
|
* @brief 测试单圈完整螺旋
|
|
* @details 验证:
|
|
* 1. 起点和终点在同一 XY 平面位置(但 Z 不同)
|
|
*/
|
|
bool test_helixline_single_turn()
|
|
{
|
|
TEST_SECTION("Helixline Single Complete Turn");
|
|
|
|
primitive_data_center_t dc;
|
|
auto profile = TestDataFactory::createSquareProfile(0.3);
|
|
auto axis = TestDataFactory::createHelixlineAxis(1.0, 2.0, 1.0, true); // 恰好1圈
|
|
|
|
auto* prim = static_cast<internal::extrude_helixline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_HELIXLINE, &dc));
|
|
extrude_helixline_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.data.helixline;
|
|
prim->initialize_with_components(&dc, desc);
|
|
|
|
auto subfaces = prim->get_subfaces();
|
|
auto side_face = subfaces[0].get_ptr();
|
|
|
|
// 起点和终点应该在同一个X-Y平面位置(但Z不同)
|
|
auto map_param_to_point_with_weight_func =
|
|
internal::get_map_param_to_point_with_weight_ptr(surface_type::extrude_polyline_side);
|
|
|
|
auto [start_point, start_surface_jacobi, start_volume_jacobi] =
|
|
map_param_to_point_with_weight_func(make_pointer_wrapper(static_cast<subface*>(side_face)), Eigen::Vector2d(0, 0));
|
|
auto [end_point, end_surface_jacobi, end_volume_jacobi] =
|
|
map_param_to_point_with_weight_func(make_pointer_wrapper(static_cast<subface*>(side_face)), Eigen::Vector2d(0, 1.0));
|
|
|
|
// X-Y位置应该接近(完整一圈)
|
|
TEST_APPROX_EQUAL(start_point.x(), end_point.x(), 0.1, "After one turn, X position returns");
|
|
TEST_APPROX_EQUAL(start_point.y(), end_point.y(), 0.1, "After one turn, Y position returns");
|
|
|
|
// Z位置应该不同
|
|
TEST_ASSERT(std::abs(end_point.z() - start_point.z()) > 1.5, "Z position increases significantly");
|
|
|
|
TestHelper::safeFree(prim);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief 测试螺旋线的参数边界情况
|
|
* @details 验证:
|
|
* 1. v=0 时点在起始位置
|
|
* 2. v=1 时点在结束位置
|
|
* 3. Z方向的增量等于height
|
|
*/
|
|
bool test_helixline_parameter_boundary()
|
|
{
|
|
TEST_SECTION("Helixline Parameter Boundary Cases");
|
|
|
|
primitive_data_center_t dc;
|
|
auto profile = TestDataFactory::createSquareProfile(0.3);
|
|
auto axis = TestDataFactory::createHelixlineAxis(1.0, 2.0, 1.0, true);
|
|
|
|
auto* prim = static_cast<internal::extrude_helixline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_HELIXLINE, &dc));
|
|
extrude_helixline_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.data.helixline;
|
|
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);
|
|
|
|
// 测试起点 (v=0)
|
|
Eigen::Vector2d uv_start(0, 0);
|
|
auto [point_start, surface_jacobi_start, volume_jacobi_start] =
|
|
map_param_to_point_with_weight_func(make_pointer_wrapper(static_cast<subface*>(side_face)), uv_start);
|
|
constexpr double radius = 1.0;
|
|
constexpr double height = 2.0;
|
|
|
|
TEST_ASSERT(std::isfinite(point_start.x()), "Start point is valid");
|
|
|
|
// 考虑Profile偏移的影响
|
|
// Profile顶点[0] = (-0.045, 0.15)
|
|
// 在TBN变换后:X = axis_point.x + B.x * profile_x + N.x * profile_y
|
|
// = 1.0 + 0*(-0.045) + (-1)*0.15 = 0.85
|
|
constexpr double profile_y_offset = 0.15; // Profile的Y偏移量
|
|
const double expected_x = radius - profile_y_offset; // = 0.85
|
|
|
|
TEST_ASSERT(std::abs(point_start.x() - expected_x) < 1e-6,
|
|
"Start X should account for profile offset\n"
|
|
" Expected: "
|
|
<< expected_x
|
|
<< "\n"
|
|
" Actual: "
|
|
<< point_start.x()
|
|
<< "\n"
|
|
" Diff: "
|
|
<< std::abs(point_start.x() - expected_x));
|
|
|
|
TEST_ASSERT(std::abs(point_start.y() - 0) < 0.02, "Start Y should be near 0");
|
|
|
|
// Z坐标不验证绝对值为0,因为Profile起点有偏移
|
|
|
|
// 测试终点 (v=1)
|
|
auto [point_end, surface_jacobi_end, volume_jacobi_end] =
|
|
map_param_to_point_with_weight_func(make_pointer_wrapper(static_cast<subface*>(side_face)), Eigen::Vector2d(0, 1.0));
|
|
|
|
// 终点的X坐标也应该考虑profile偏移
|
|
TEST_ASSERT(std::abs(point_end.x() - expected_x) < 1e-6, "End X should account for profile offset");
|
|
TEST_ASSERT(std::abs(point_end.y() - 0) < 0.02, "End Y should be near 0");
|
|
|
|
// Z方向的增量应该等于height(这才是正确的测试)
|
|
double z_rise = point_end.z() - point_start.z();
|
|
TEST_ASSERT(std::abs(z_rise - height) < 1e-6,
|
|
"Z rise should equal height\n"
|
|
" Start Z: "
|
|
<< point_start.z()
|
|
<< "\n"
|
|
" End Z: "
|
|
<< point_end.z()
|
|
<< "\n"
|
|
" Rise: "
|
|
<< z_rise
|
|
<< "\n"
|
|
" Expected: "
|
|
<< height);
|
|
|
|
TestHelper::safeFree(prim);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief 测试倾斜螺旋线(轴不沿 Z 方向)
|
|
* @details 验证:
|
|
* 1. 倾斜的螺旋线能够正确创建
|
|
*/
|
|
bool test_helixline_slanted_axis()
|
|
{
|
|
TEST_SECTION("Helixline with Slanted Axis");
|
|
|
|
primitive_data_center_t dc;
|
|
auto profile = TestDataFactory::createSquareProfile(0.3);
|
|
|
|
Eigen::Vector3d start(0, 0, 0);
|
|
Eigen::Vector3d end(2, 0, 2);
|
|
auto axis = TestDataFactory::createSlantedHelixAxis(0.5, start, end, 1.0);
|
|
|
|
auto* prim = static_cast<internal::extrude_helixline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_HELIXLINE, &dc));
|
|
extrude_helixline_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.data.helixline;
|
|
prim->initialize_with_components(&dc, desc);
|
|
|
|
TEST_ASSERT(prim != nullptr, "Slanted helix creates valid extrude");
|
|
|
|
aabb_t aabb = prim->fetch_aabb();
|
|
TEST_ASSERT(aabb.volume() > 0, "AABB has positive volume");
|
|
|
|
TestHelper::safeFree(prim);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief 测试零螺距螺旋(理论上退化为圆)
|
|
* @details 验证:
|
|
* 1. 能否处理这种边界情况
|
|
*/
|
|
bool test_helixline_zero_pitch()
|
|
{
|
|
TEST_SECTION("Helixline with Zero Pitch (Edge Case)");
|
|
|
|
primitive_data_center_t dc;
|
|
auto profile = TestDataFactory::createSquareProfile(0.3);
|
|
|
|
// 零螺距:advance_per_round 极大,使得实际上没有上升
|
|
axis_descriptor_t axis;
|
|
axis.type = AXIS_TYPE_HELIXLINE;
|
|
axis.data.helixline.axis_start = {0, 0, 0};
|
|
axis.data.helixline.axis_end = {0, 0, 0.001}; // 极小高度
|
|
axis.data.helixline.start_direction = {1, 0, 0};
|
|
axis.data.helixline.radius = 1.0;
|
|
axis.data.helixline.advance_per_round = 1e-6; // 极小螺距
|
|
axis.data.helixline.is_righthanded = true;
|
|
|
|
auto* prim = static_cast<internal::extrude_helixline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_HELIXLINE, &dc));
|
|
|
|
// 这可能会失败或产生退化几何,测试系统是否能处理
|
|
try {
|
|
extrude_helixline_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.data.helixline;
|
|
prim->initialize_with_components(&dc, desc);
|
|
std::cout << " Zero pitch helix created (may be degenerate)\n";
|
|
TestHelper::safeFree(prim);
|
|
} catch (...) {
|
|
std::cout << " Zero pitch helix failed to create (expected for edge case)\n";
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief 测试多圈螺旋的 SDF 一致性
|
|
* @details 验证:
|
|
* 1. 螺旋每一圈的 SDF 计算一致
|
|
*/
|
|
bool test_helixline_multi_turn_consistency()
|
|
{
|
|
TEST_SECTION("Helixline Multi-Turn SDF Consistency");
|
|
|
|
primitive_data_center_t dc;
|
|
auto profile = TestDataFactory::createSquareProfile(0.3);
|
|
auto axis = TestDataFactory::createHelixlineAxis(1.0, 6.0, 3.0, true); // 3圈
|
|
|
|
auto* prim = static_cast<internal::extrude_helixline_t*>(internal::new_primitive(PRIMITIVE_TYPE_EXTRUDE_HELIXLINE, &dc));
|
|
extrude_helixline_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.data.helixline;
|
|
prim->initialize_with_components(&dc, desc);
|
|
|
|
auto subfaces = prim->get_subfaces();
|
|
auto side_face = subfaces[0].get_ptr();
|
|
|
|
// 在每一圈的相同相位位置测试 SDF
|
|
std::vector<Eigen::Vector3d> test_points = {
|
|
{1.2, 0, 0.5}, // 第1圈 1/4 位置
|
|
{1.2, 0, 2.5}, // 第2圈 1/4 位置
|
|
{1.2, 0, 4.5} // 第3圈 1/4 位置
|
|
};
|
|
|
|
std::vector<double> sdfs;
|
|
for (const auto& point : test_points) {
|
|
double sdf = internal::get_eval_sdf_ptr(surface_type::extrude_helixline_side)(make_pointer_wrapper(side_face), point);
|
|
sdfs.push_back(sdf);
|
|
}
|
|
|
|
// 每一圈相同位置的 SDF 应该相近
|
|
TEST_APPROX_EQUAL(sdfs[0], sdfs[1], 0.1, "SDF consistent between turn 1 and 2");
|
|
TEST_APPROX_EQUAL(sdfs[1], sdfs[2], 0.1, "SDF consistent between turn 2 and 3");
|
|
|
|
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: Helixline Geometry (4个测试)
|
|
{"Helixline Construction", test_helixline_construction, "Geometry" },
|
|
{"Helixline Tangent & Normal", test_helixline_tangent_normal, "Geometry" },
|
|
{"Helixline Closest Point", test_helixline_closest_point, "Geometry" },
|
|
{"Helixline Left vs Right", test_helixline_left_vs_right, "Geometry" },
|
|
|
|
// Group 2: Subface Functions (4个测试)
|
|
{"Helixline SDF Basic", test_helixline_eval_sdf_basic, "Subface" },
|
|
{"Helixline Param Round-Trip", test_helixline_parametrization_roundtrip, "Subface" },
|
|
{"Helixline SDF Gradient", test_helixline_sdf_gradient, "Subface" },
|
|
{"Helixline Constraint Curves", test_helixline_constraint_curves, "Subface" },
|
|
|
|
// Group 3: Handedness (1个测试)
|
|
{"Right vs Left Handed", test_helixline_righthanded_vs_lefthanded, "Handedness"},
|
|
|
|
// Group 4: Replace (2个测试)
|
|
{"Replace Profile", test_helixline_replace_profile, "Replace" },
|
|
{"Replace Axis", test_helixline_replace_axis, "Replace" },
|
|
|
|
// Group 5: Various Profiles (3个测试)
|
|
{"Circular Profile", test_helixline_with_circular_profile, "Profiles" },
|
|
{"Triangular Profile", test_helixline_with_triangular_profile, "Profiles" },
|
|
{"Rectangular Profile", test_helixline_with_rectangular_profile, "Profiles" },
|
|
|
|
// Group 6: Edge Cases
|
|
{"Slant ed Axis", test_helixline_slanted_axis, "EdgeCase" },
|
|
{"Multi-Turn Consistency", test_helixline_multi_turn_consistency, "EdgeCase" },
|
|
};
|
|
|
|
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;
|
|
}
|