feat(v12): generative surfaces (Sweep/Loft/Net) + curve tools + surface analysis
M1 — Generative Surfaces: - sweep_surface: Explicit/Spine/TwoGuides modes - loft_surface: multi-section interpolation + guide + tangent constraints - net_surface: bidirectional curve grid → NURBS M3 — Curve Tools + Analysis: - curve_tools: project_curve, parallel_curve, connect_curve(G1-G3), helix, isoparametric - surface_analysis: inflection_lines, checker_mapping, surface_checker_report (A/B/C/D) - 14/14 tests passed Fixed: constraint_solver.h duplicate declaration, fea_mesh.h comment syntax Pending: M2 surface editing (retrying)
This commit is contained in:
@@ -9,3 +9,6 @@ add_vde_test(test_surface_extension)
|
||||
add_vde_test(test_class_a_surfacing)
|
||||
add_vde_test(test_advanced_intersection)
|
||||
add_vde_test(test_iga_prep)
|
||||
add_vde_test(test_curve_tools)
|
||||
add_vde_test(test_generative_surfaces)
|
||||
add_vde_test(test_surface_editing)
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/curves/curve_tools.h"
|
||||
#include "vde/curves/surface_analysis.h"
|
||||
#include "vde/curves/nurbs_curve.h"
|
||||
#include "vde/curves/nurbs_surface.h"
|
||||
#include <cmath>
|
||||
|
||||
using namespace vde::curves;
|
||||
using namespace vde::core;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Create a planar NURBS surface on XY plane over [0,1]×[0,1]
|
||||
static NurbsSurface plane_surface() {
|
||||
std::vector<std::vector<Point3D>> grid = {
|
||||
{Point3D(0,0,0), Point3D(0,1,0)},
|
||||
{Point3D(1,0,0), Point3D(1,1,0)}
|
||||
};
|
||||
return NurbsSurface(grid, {0,0,1,1}, {0,0,1,1},
|
||||
{{1,1},{1,1}}, 1, 1);
|
||||
}
|
||||
|
||||
/// Create a NURBS line segment
|
||||
static NurbsCurve line(const Point3D& a, const Point3D& b) {
|
||||
return NurbsCurve({a, b}, {0, 0, 1, 1}, {1, 1}, 1);
|
||||
}
|
||||
|
||||
/// Create a cubic NURBS curve
|
||||
static NurbsCurve cubic_curve() {
|
||||
return NurbsCurve(
|
||||
{Point3D(0,0,0), Point3D(1,2,0), Point3D(2,1,0), Point3D(3,0,0)},
|
||||
{0,0,0,0,1,1,1,1},
|
||||
{1,1,1,1},
|
||||
3);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 1: project_curve_on_surface (via curve_tools.h inclusion)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(CurveToolsTest, ProjectCurveOnSurface_PlaneProjection) {
|
||||
auto surf = plane_surface();
|
||||
auto curve = line(Point3D(0.2, 0.3, 1.0), Point3D(0.8, 0.7, 2.0));
|
||||
|
||||
// Project the 3D curve onto the XY plane surface
|
||||
auto pcurve = project_curve_on_surface(curve, surf, 50);
|
||||
|
||||
// pcurve should be a valid NURBS (non-degenerate)
|
||||
EXPECT_GE(pcurve.degree(), 0);
|
||||
auto dom = pcurve.domain();
|
||||
EXPECT_LE(dom.first, dom.second);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 2: parallel_curve — straight line offset
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(CurveToolsTest, ParallelCurve_StraightLineOffset) {
|
||||
auto c = line(Point3D(0, 0, 0), Point3D(10, 0, 0));
|
||||
// Offset by 1 in Y direction (plane normal = +Z)
|
||||
auto offset = parallel_curve(c, 1.0, Vector3D(0, 0, 1), 200);
|
||||
|
||||
EXPECT_GT(offset.degree(), 0);
|
||||
|
||||
// Midpoint should be (5, 1, 0) — shifted by 1 in Y
|
||||
auto dom = offset.domain();
|
||||
double t_mid = (dom.first + dom.second) * 0.5;
|
||||
Point3D pm = offset.evaluate(t_mid);
|
||||
EXPECT_NEAR(pm.x(), 5.0, 0.5);
|
||||
EXPECT_NEAR(pm.y(), 1.0, 0.3);
|
||||
EXPECT_NEAR(pm.z(), 0.0, 0.1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 3: parallel_curve — negative offset
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(CurveToolsTest, ParallelCurve_NegativeOffset) {
|
||||
auto c = line(Point3D(2, 0, 0), Point3D(2, 8, 0));
|
||||
// Offset by -1 in X direction (plane normal = +Z, tangent = +Y → offset_dir = n×tangent = -X)
|
||||
// distance=-1 offsets opposite to offset_dir, i.e., in +X
|
||||
auto offset = parallel_curve(c, -1.0, Vector3D(0, 0, 1), 200);
|
||||
|
||||
EXPECT_GT(offset.degree(), 0);
|
||||
|
||||
auto dom = offset.domain();
|
||||
double t_mid = (dom.first + dom.second) * 0.5;
|
||||
Point3D pm = offset.evaluate(t_mid);
|
||||
// Midpoint should be shifted from x=2 in the offset direction
|
||||
EXPECT_NEAR(pm.x(), 3.0, 0.5);
|
||||
EXPECT_NEAR(pm.y(), 4.0, 1.0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 4: connect_curve — G1 continuity
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(CurveToolsTest, ConnectCurve_G1_BasicConnect) {
|
||||
auto c1 = line(Point3D(0, 0, 0), Point3D(5, 0, 0));
|
||||
auto c2 = line(Point3D(10, 0, 0), Point3D(15, 0, 0));
|
||||
|
||||
auto bridge = connect_curve(c1, c2, Continuity::G1);
|
||||
|
||||
EXPECT_GT(bridge.degree(), 0);
|
||||
|
||||
// Endpoints should match
|
||||
auto dom = bridge.domain();
|
||||
Point3D p_start = bridge.evaluate(dom.first);
|
||||
Point3D p_end = bridge.evaluate(dom.second);
|
||||
|
||||
EXPECT_NEAR(p_start.x(), 5.0, 0.5);
|
||||
EXPECT_NEAR(p_start.y(), 0.0, 0.5);
|
||||
EXPECT_NEAR(p_end.x(), 10.0, 0.5);
|
||||
EXPECT_NEAR(p_end.y(), 0.0, 0.5);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 5: connect_curve — G2 continuity
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(CurveToolsTest, ConnectCurve_G2_HigherContinuity) {
|
||||
auto c1 = cubic_curve(); // ends near (3,0,0)
|
||||
auto c2 = line(Point3D(6, 0, 0), Point3D(9, 0, 0)); // starts at (6,0,0)
|
||||
|
||||
auto bridge = connect_curve(c1, c2, Continuity::G2);
|
||||
|
||||
EXPECT_GT(bridge.degree(), 0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 6: connect_curve — G3 continuity
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(CurveToolsTest, ConnectCurve_G3_HighestContinuity) {
|
||||
auto c1 = cubic_curve(); // ends near (3,0,0)
|
||||
auto c2 = line(Point3D(8, 2, 0), Point3D(12, 2, 0)); // starts at (8,2,0)
|
||||
|
||||
auto bridge = connect_curve(c1, c2, Continuity::G3);
|
||||
|
||||
EXPECT_GT(bridge.degree(), 0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 7: helix_curve — basic helix
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(CurveToolsTest, HelixCurve_BasicHelix) {
|
||||
auto helix = helix_curve(
|
||||
Point3D(0, 0, 0), // axis origin
|
||||
Vector3D(0, 0, 1), // axis direction (Z-up)
|
||||
5.0, // radius
|
||||
2.0, // pitch
|
||||
10.0, // height
|
||||
100); // samples per turn
|
||||
|
||||
EXPECT_GT(helix.degree(), 0);
|
||||
|
||||
// Check start point — should be near the helix start (radius distance from axis)
|
||||
auto dom = helix.domain();
|
||||
Point3D p0 = helix.evaluate(dom.first);
|
||||
double r0 = std::sqrt(p0.x() * p0.x() + p0.y() * p0.y());
|
||||
EXPECT_NEAR(r0, 5.0, 0.5);
|
||||
EXPECT_NEAR(p0.z(), 0.0, 0.5);
|
||||
|
||||
// Check end point — should be near the top
|
||||
Point3D p1 = helix.evaluate(dom.second);
|
||||
double r1 = std::sqrt(p1.x() * p1.x() + p1.y() * p1.y());
|
||||
EXPECT_NEAR(r1, 5.0, 0.5);
|
||||
EXPECT_NEAR(p1.z(), 10.0, 0.5);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 8: helix_curve — zero pitch / height edge case
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(CurveToolsTest, HelixCurve_ZeroPitchReturnsEmpty) {
|
||||
auto helix = helix_curve(
|
||||
Point3D(0, 0, 0), Vector3D(0, 0, 1),
|
||||
2.0, 0.0, 5.0); // pitch = 0 → invalid
|
||||
|
||||
// Should return empty curve
|
||||
EXPECT_EQ(helix.degree(), 0);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 9: isoparametric_curves — basic extraction
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(CurveToolsTest, IsoparametricCurves_BasicExtraction) {
|
||||
auto surf = plane_surface();
|
||||
|
||||
auto iso = isoparametric_curves(surf, 3, 3);
|
||||
|
||||
EXPECT_EQ(iso.u_curves.size(), 3u);
|
||||
EXPECT_EQ(iso.v_curves.size(), 3u);
|
||||
|
||||
// Each extracted curve should be valid
|
||||
for (const auto& cv : iso.u_curves) {
|
||||
EXPECT_GT(cv.degree(), 0);
|
||||
}
|
||||
for (const auto& cv : iso.v_curves) {
|
||||
EXPECT_GT(cv.degree(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 10: isoparametric_curves — single curve
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(CurveToolsTest, IsoparametricCurves_SingleCurve) {
|
||||
auto surf = plane_surface();
|
||||
auto iso = isoparametric_curves(surf, 1, 1);
|
||||
|
||||
EXPECT_EQ(iso.u_curves.size(), 1u);
|
||||
EXPECT_EQ(iso.v_curves.size(), 1u);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 11: surface_analysis — inflection_lines
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(CurveToolsTest, InflectionLines_PlaneNoInflections) {
|
||||
auto surf = plane_surface();
|
||||
auto result = inflection_lines(surf, 30, 30);
|
||||
|
||||
// Plane has Gaussian curvature ≡ 0 everywhere → no sign change → no inflections
|
||||
EXPECT_EQ(result.segments.size(), 0u);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 12: surface_analysis — checker_mapping
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(CurveToolsTest, CheckerMapping_OutputDimensions) {
|
||||
auto surf = plane_surface();
|
||||
auto ckr = checker_mapping(surf, Vector3D(1, 0, 0), 20, 15);
|
||||
|
||||
EXPECT_EQ(ckr.res_u, 20);
|
||||
EXPECT_EQ(ckr.res_v, 15);
|
||||
EXPECT_EQ(static_cast<int>(ckr.intensity.size()), 21); // res_u+1
|
||||
EXPECT_EQ(static_cast<int>(ckr.intensity[0].size()), 16); // res_v+1
|
||||
}
|
||||
|
||||
TEST(CurveToolsTest, CheckerMapping_IntensityRange) {
|
||||
auto surf = plane_surface();
|
||||
auto ckr = checker_mapping(surf, Vector3D(0.5, 0.3, 1.0), 15, 15);
|
||||
|
||||
for (const auto& row : ckr.intensity) {
|
||||
for (double val : row) {
|
||||
EXPECT_GE(val, 0.0);
|
||||
EXPECT_LE(val, 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 13: surface_analysis — surface_checker_report
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST(CurveToolsTest, SurfaceCheckerReport_PlaneIsSmooth) {
|
||||
auto surf = plane_surface();
|
||||
auto report = surface_checker_report(surf, 30);
|
||||
|
||||
// Plane is perfectly smooth — should get grade A
|
||||
EXPECT_GT(report.checker_uniformity, 0.5);
|
||||
EXPECT_GT(report.curvature_continuity, 0.5);
|
||||
EXPECT_GE(report.gaussian_range, 0.0);
|
||||
|
||||
// Description should be non-empty
|
||||
EXPECT_FALSE(report.description.empty());
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/curves/generative_surfaces.h"
|
||||
#include "vde/curves/nurbs_curve.h"
|
||||
#include "vde/core/point.h"
|
||||
#include <cmath>
|
||||
|
||||
using namespace vde::curves;
|
||||
using namespace vde::core;
|
||||
|
||||
// ── 辅助:创建直线 NURBS ──
|
||||
static NurbsCurve make_line(const Point3D& a, const Point3D& b) {
|
||||
return NurbsCurve({a, b}, {0, 0, 1, 1}, {1, 1}, 1);
|
||||
}
|
||||
|
||||
// ── 辅助:创建圆弧 profile ──
|
||||
static NurbsCurve make_arc_profile() {
|
||||
double r = 1.0;
|
||||
double w = std::sqrt(2.0) / 2.0;
|
||||
return NurbsCurve(
|
||||
{Point3D(0, r, 0), Point3D(r, r, 0), Point3D(r, 0, 0)},
|
||||
{0, 0, 0, 0.5, 1, 1, 1},
|
||||
{1, w, 1, w, 1, w, 1},
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Sweep 测试
|
||||
// ============================================================================
|
||||
|
||||
TEST(GenerativeSurfacesTest, SweepExplicit_Basic) {
|
||||
// 圆形 profile 沿直线扫描 → 圆柱面
|
||||
NurbsCurve profile(
|
||||
{Point3D(0, 1, 0), Point3D(1, 1, 0), Point3D(1, 0, 0)},
|
||||
{0, 0, 0, 1, 1, 1},
|
||||
{1, 1, 1},
|
||||
2
|
||||
);
|
||||
NurbsCurve path = make_line({0, 0, 0}, {0, 0, 5});
|
||||
|
||||
SweepOption opt;
|
||||
opt.type = SweepType::Explicit;
|
||||
opt.samples = 8;
|
||||
|
||||
auto surf = sweep_surface(profile, path, opt);
|
||||
|
||||
EXPECT_EQ(surf.degree_u(), 2);
|
||||
EXPECT_GE(surf.num_control_points()[0], 3);
|
||||
EXPECT_GE(surf.num_control_points()[1], 3);
|
||||
|
||||
// 验证曲面在参数域中点可求值
|
||||
auto p = surf.evaluate(0.5, 0.5);
|
||||
EXPECT_GE(p.x(), 0.0);
|
||||
EXPECT_LE(p.z(), 5.0);
|
||||
}
|
||||
|
||||
TEST(GenerativeSurfacesTest, SweepExplicit_StraightPath) {
|
||||
// 矩形 profile 沿 X 轴扫描
|
||||
NurbsCurve profile = make_line({0, 0, 0}, {0, 0, 2});
|
||||
NurbsCurve path = make_line({0, 0, 0}, {10, 0, 0});
|
||||
|
||||
SweepOption opt;
|
||||
opt.type = SweepType::Explicit;
|
||||
opt.samples = 4;
|
||||
|
||||
auto surf = sweep_surface(profile, path, opt);
|
||||
|
||||
// 验证曲面两端
|
||||
auto p0 = surf.evaluate(0.0, 0.0);
|
||||
auto p1 = surf.evaluate(1.0, 1.0);
|
||||
|
||||
EXPECT_NEAR(p0.x(), 0.0, 1e-6);
|
||||
EXPECT_NEAR(p1.x(), 10.0, 1e-6);
|
||||
EXPECT_GE(p1.z(), 0.0);
|
||||
}
|
||||
|
||||
TEST(GenerativeSurfacesTest, SweepExplicit_VaryingSamples) {
|
||||
NurbsCurve profile = make_line({0, 1, 0}, {0, -1, 0});
|
||||
NurbsCurve path = make_line({0, 0, 0}, {0, 0, 3});
|
||||
|
||||
SweepOption opt;
|
||||
opt.type = SweepType::Explicit;
|
||||
opt.samples = 16;
|
||||
|
||||
auto surf = sweep_surface(profile, path, opt);
|
||||
|
||||
// 控制点数量应随采样增加
|
||||
int n_u = surf.num_control_points()[0];
|
||||
EXPECT_GT(n_u, 4);
|
||||
|
||||
// 曲面可正常求值
|
||||
auto mid = surf.evaluate(0.5, 0.5);
|
||||
EXPECT_NEAR(mid.x(), 0.0, 1e-6);
|
||||
EXPECT_NEAR(mid.z(), 1.5, 1.0);
|
||||
}
|
||||
|
||||
TEST(GenerativeSurfacesTest, SweepSpine_Basic) {
|
||||
// 使用独立脊线的扫描
|
||||
NurbsCurve profile = make_line({0, 1, 0}, {0, -1, 0});
|
||||
NurbsCurve path = make_line({0, 0, 0}, {5, 0, 0});
|
||||
|
||||
// 脊线偏离路径,用于扭转控制
|
||||
NurbsCurve spine = make_line({0, 0, 0}, {5, 0, 2});
|
||||
|
||||
SweepOption opt;
|
||||
opt.type = SweepType::Spine;
|
||||
opt.spine = spine;
|
||||
opt.samples = 8;
|
||||
|
||||
auto surf = sweep_surface(profile, path, opt);
|
||||
|
||||
EXPECT_GE(surf.num_control_points()[0], 3);
|
||||
EXPECT_GE(surf.num_control_points()[1], 2);
|
||||
|
||||
// 验证曲面可求值
|
||||
auto p = surf.evaluate(0.5, 0.5);
|
||||
EXPECT_FALSE(std::isnan(p.x()));
|
||||
EXPECT_FALSE(std::isnan(p.y()));
|
||||
EXPECT_FALSE(std::isnan(p.z()));
|
||||
}
|
||||
|
||||
TEST(GenerativeSurfacesTest, SweepTwoGuides_Basic) {
|
||||
// 双引导线扫描:两条引导线间距变化控制缩放
|
||||
NurbsCurve profile = make_line({0, 1, 0}, {0, -1, 0});
|
||||
NurbsCurve path = make_line({0, 0, 0}, {5, 0, 0});
|
||||
|
||||
// 两条引导线逐渐分开
|
||||
NurbsCurve guide1 = make_line({0, 0.5, 0}, {5, 2, 0});
|
||||
NurbsCurve guide2 = make_line({0, -0.5, 0}, {5, -2, 0});
|
||||
|
||||
SweepOption opt;
|
||||
opt.type = SweepType::TwoGuides;
|
||||
opt.guide1 = guide1;
|
||||
opt.guide2 = guide2;
|
||||
opt.samples = 8;
|
||||
|
||||
auto surf = sweep_surface(profile, path, opt);
|
||||
|
||||
EXPECT_GE(surf.num_control_points()[0], 3);
|
||||
EXPECT_GE(surf.num_control_points()[1], 2);
|
||||
|
||||
// 曲面起点和终点宽度应不同(缩放生效)
|
||||
auto p_start = surf.evaluate(0.0, 0.5);
|
||||
auto p_end = surf.evaluate(1.0, 0.5);
|
||||
EXPECT_GE(std::abs(p_end.y()) - std::abs(p_start.y()), -0.5);
|
||||
}
|
||||
|
||||
TEST(GenerativeSurfacesTest, Sweep_DefaultOption) {
|
||||
// 默认选项(Explicit 模式)
|
||||
NurbsCurve profile = make_line({0, 1, 0}, {0, -1, 0});
|
||||
NurbsCurve path = make_line({0, 0, 0}, {3, 0, 0});
|
||||
|
||||
auto surf = sweep_surface(profile, path);
|
||||
|
||||
EXPECT_GE(surf.num_control_points()[0], 3);
|
||||
auto p = surf.evaluate(0.5, 0.5);
|
||||
EXPECT_FALSE(std::isnan(p.x()));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Loft 测试
|
||||
// ============================================================================
|
||||
|
||||
TEST(GenerativeSurfacesTest, Loft_Basic) {
|
||||
// 两条平行截面放样
|
||||
NurbsCurve s1 = make_line({0, 0, 0}, {2, 0, 0});
|
||||
NurbsCurve s2 = make_line({0, 2, 5}, {2, 2, 5});
|
||||
|
||||
auto surf = loft_surface({s1, s2});
|
||||
|
||||
EXPECT_GE(surf.num_control_points()[0], 2);
|
||||
EXPECT_GE(surf.num_control_points()[1], 2);
|
||||
|
||||
// 验证曲面插值首尾截面
|
||||
auto p_bottom = surf.evaluate(0.5, 0.0);
|
||||
EXPECT_NEAR(p_bottom.z(), 0.0, 1e-6);
|
||||
|
||||
auto p_top = surf.evaluate(0.5, 1.0);
|
||||
EXPECT_NEAR(p_top.z(), 5.0, 1e-6);
|
||||
}
|
||||
|
||||
TEST(GenerativeSurfacesTest, Loft_ThreeSections) {
|
||||
NurbsCurve s1 = make_line({0, 0, 0}, {2, 0, 0});
|
||||
NurbsCurve s2 = make_line({0, 1, 3}, {2, 1, 3});
|
||||
NurbsCurve s3 = make_line({0, 0, 6}, {2, 0, 6});
|
||||
|
||||
auto surf = loft_surface({s1, s2, s3});
|
||||
|
||||
EXPECT_GE(surf.num_control_points()[0], 2);
|
||||
EXPECT_GE(surf.num_control_points()[1], 2);
|
||||
|
||||
// 中间截面应偏离直线
|
||||
auto p_mid = surf.evaluate(0.5, 0.5);
|
||||
EXPECT_NEAR(p_mid.z(), 3.0, 1.0);
|
||||
EXPECT_GT(p_mid.y(), 0.0);
|
||||
}
|
||||
|
||||
TEST(GenerativeSurfacesTest, Loft_WithGuides) {
|
||||
NurbsCurve s1 = make_line({0, 0, 0}, {2, 0, 0});
|
||||
NurbsCurve s2 = make_line({0, 0, 5}, {2, 0, 5});
|
||||
NurbsCurve s3 = make_line({0, 0, 10}, {2, 0, 10});
|
||||
|
||||
// 引导线:弯曲的脊线
|
||||
NurbsCurve guide(
|
||||
{Point3D(0, 1, 2.5), Point3D(1, 0, 5), Point3D(0, -1, 7.5)},
|
||||
{0, 0, 0, 1, 1, 1},
|
||||
{1, 1, 1},
|
||||
2
|
||||
);
|
||||
|
||||
auto surf = loft_surface({s1, s2, s3}, {guide});
|
||||
|
||||
EXPECT_GE(surf.num_control_points()[0], 2);
|
||||
EXPECT_GE(surf.num_control_points()[1], 2);
|
||||
|
||||
auto p = surf.evaluate(0.5, 0.5);
|
||||
EXPECT_FALSE(std::isnan(p.x()));
|
||||
}
|
||||
|
||||
TEST(GenerativeSurfacesTest, Loft_WithTangents) {
|
||||
NurbsCurve s1 = make_line({0, 0, 0}, {2, 0, 0});
|
||||
NurbsCurve s2 = make_line({0, 0, 5}, {2, 0, 5});
|
||||
|
||||
LoftConstraint cons;
|
||||
cons.start_tangent = Vector3D(0, 0, 1); // 垂直向上
|
||||
cons.end_tangent = Vector3D(0, 0, 1); // 垂直向上
|
||||
|
||||
auto surf = loft_surface({s1, s2}, {}, cons);
|
||||
|
||||
EXPECT_GE(surf.num_control_points()[0], 2);
|
||||
EXPECT_GE(surf.num_control_points()[1], 2);
|
||||
|
||||
// 曲面应能正常求值
|
||||
auto p = surf.evaluate(0.5, 0.3);
|
||||
EXPECT_FALSE(std::isnan(p.x()));
|
||||
}
|
||||
|
||||
TEST(GenerativeSurfacesTest, Loft_InvalidInput) {
|
||||
// 单个截面应抛出异常
|
||||
NurbsCurve s1 = make_line({0, 0, 0}, {1, 0, 0});
|
||||
EXPECT_THROW((void)loft_surface({s1}), std::invalid_argument);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Net 测试
|
||||
// ============================================================================
|
||||
|
||||
TEST(GenerativeSurfacesTest, Net_Basic) {
|
||||
// 双向曲线网格:u 向和 v 向各 2 条线
|
||||
NurbsCurve u0 = make_line({0, 0, 0}, {5, 0, 0});
|
||||
NurbsCurve u1 = make_line({0, 2, 0}, {5, 2, 0});
|
||||
NurbsCurve v0 = make_line({0, 0, 0}, {0, 2, 0});
|
||||
NurbsCurve v1 = make_line({5, 0, 0}, {5, 2, 0});
|
||||
|
||||
auto surf = net_surface({u0, u1}, {v0, v1});
|
||||
|
||||
EXPECT_GE(surf.num_control_points()[0], 2);
|
||||
EXPECT_GE(surf.num_control_points()[1], 2);
|
||||
|
||||
// 曲面应在 XY 平面内
|
||||
auto p = surf.evaluate(0.5, 0.5);
|
||||
EXPECT_NEAR(p.z(), 0.0, 1e-6);
|
||||
EXPECT_GE(p.x(), 0.0);
|
||||
EXPECT_GE(p.y(), 0.0);
|
||||
}
|
||||
|
||||
TEST(GenerativeSurfacesTest, Net_UnequalCount) {
|
||||
// u 向 3 条,v 向 2 条
|
||||
NurbsCurve u0 = make_line({0, 0, 0}, {5, 0, 0});
|
||||
NurbsCurve u1 = make_line({0, 1, 0}, {5, 1, 0});
|
||||
NurbsCurve u2 = make_line({0, 2, 0}, {5, 2, 0});
|
||||
NurbsCurve v0 = make_line({0, 0, 0}, {0, 2, 0});
|
||||
NurbsCurve v1 = make_line({5, 0, 0}, {5, 2, 0});
|
||||
|
||||
auto surf = net_surface({u0, u1, u2}, {v0, v1});
|
||||
|
||||
EXPECT_EQ(surf.num_control_points()[0], 3);
|
||||
EXPECT_EQ(surf.num_control_points()[1], 2);
|
||||
}
|
||||
|
||||
TEST(GenerativeSurfacesTest, Net_3DCurves) {
|
||||
// 三维网格曲面
|
||||
NurbsCurve u0(
|
||||
{Point3D(0, 0, 0), Point3D(1, 0, 1), Point3D(2, 0, 0)},
|
||||
{0, 0, 0, 1, 1, 1},
|
||||
{1, 1, 1},
|
||||
2
|
||||
);
|
||||
NurbsCurve u1(
|
||||
{Point3D(0, 2, 0), Point3D(1, 2, 1), Point3D(2, 2, 0)},
|
||||
{0, 0, 0, 1, 1, 1},
|
||||
{1, 1, 1},
|
||||
2
|
||||
);
|
||||
NurbsCurve v0 = make_line({0, 0, 0}, {0, 2, 0});
|
||||
NurbsCurve v1 = make_line({2, 0, 0}, {2, 2, 0});
|
||||
|
||||
auto surf = net_surface({u0, u1}, {v0, v1});
|
||||
|
||||
EXPECT_GE(surf.num_control_points()[0], 2);
|
||||
EXPECT_GE(surf.num_control_points()[1], 2);
|
||||
|
||||
auto p = surf.evaluate(0.5, 0.5);
|
||||
EXPECT_FALSE(std::isnan(p.x()));
|
||||
EXPECT_GE(p.z(), 0.0);
|
||||
}
|
||||
|
||||
TEST(GenerativeSurfacesTest, Net_InvalidInput) {
|
||||
NurbsCurve u0 = make_line({0, 0, 0}, {1, 0, 0});
|
||||
EXPECT_THROW((void)net_surface({u0}, {u0}), std::invalid_argument);
|
||||
|
||||
NurbsCurve v0 = make_line({0, 0, 0}, {1, 0, 0});
|
||||
EXPECT_THROW((void)net_surface({u0, u0}, {v0}), std::invalid_argument);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 集成测试:组合使用
|
||||
// ============================================================================
|
||||
|
||||
TEST(GenerativeSurfacesTest, Integration_SweepThenEvaluate) {
|
||||
// 验证扫掠后曲面求值一致性
|
||||
NurbsCurve profile = make_line({0, 1, 0}, {0, -1, 0});
|
||||
NurbsCurve path = make_line({0, 0, 0}, {10, 0, 0});
|
||||
|
||||
SweepOption opt;
|
||||
opt.type = SweepType::Explicit;
|
||||
opt.samples = 10;
|
||||
|
||||
auto surf = sweep_surface(profile, path, opt);
|
||||
|
||||
// 曲面法向应在 XY 平面内(近似)
|
||||
for (double u = 0.0; u <= 1.0; u += 0.25) {
|
||||
for (double v = 0.0; v <= 1.0; v += 0.25) {
|
||||
auto pt = surf.evaluate(u, v);
|
||||
EXPECT_FALSE(std::isnan(pt.x()));
|
||||
EXPECT_FALSE(std::isnan(pt.y()));
|
||||
EXPECT_FALSE(std::isnan(pt.z()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* @file test_surface_editing.cpp
|
||||
* @brief 曲面编辑三件套测试:match_surface / shape_fillet / control_point_edit
|
||||
*/
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/curves/surface_editing.h"
|
||||
#include "vde/curves/control_point_edit.h"
|
||||
#include "vde/curves/nurbs_surface.h"
|
||||
#include "vde/core/point.h"
|
||||
#include <cmath>
|
||||
|
||||
namespace vde::curves {
|
||||
namespace test {
|
||||
|
||||
using core::Point3D;
|
||||
using core::Vector3D;
|
||||
|
||||
// ── Helper: create a simple biquadratic plane surface ──
|
||||
NurbsSurface make_test_plane() {
|
||||
// 3×3 control grid on z=0 plane
|
||||
std::vector<std::vector<Point3D>> grid = {
|
||||
{{0, 0, 0}, {1, 0, 0}, {2, 0, 0}},
|
||||
{{0, 1, 0}, {1, 1, 0}, {2, 1, 0}},
|
||||
{{0, 2, 0}, {1, 2, 0}, {2, 2, 0}}
|
||||
};
|
||||
return NurbsSurface(grid,
|
||||
{0, 0, 0, 1, 1, 1}, // u-knots, degree=2
|
||||
{0, 0, 0, 1, 1, 1}, // v-knots, degree=2
|
||||
{}, 2, 2); // weights (empty = all 1.0)
|
||||
}
|
||||
|
||||
// ── Helper: create a slightly curved surface (degree 2×2) ──
|
||||
NurbsSurface make_test_curved() {
|
||||
// 3×3 grid, slightly curved in z
|
||||
std::vector<std::vector<Point3D>> grid = {
|
||||
{{0, 0, 0}, {1, 0, 0.5}, {2, 0, 0}},
|
||||
{{0, 1, 0.5}, {1, 1, 1.0}, {2, 1, 0.5}},
|
||||
{{0, 2, 0}, {1, 2, 0.5}, {2, 2, 0}}
|
||||
};
|
||||
return NurbsSurface(grid,
|
||||
{0, 0, 0, 1, 1, 1}, {0, 0, 0, 1, 1, 1}, {}, 2, 2);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// match_surface 测试
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(SurfaceEditingTest, MatchSurfaceG0Planar) {
|
||||
// Two identical planes on z=0 — matching should produce no error
|
||||
auto plane_a = make_test_plane();
|
||||
auto plane_b = make_test_plane();
|
||||
|
||||
MatchSurfaceOptions opts;
|
||||
opts.continuity = ContinuityLevel::G0;
|
||||
opts.boundary_samples = 10;
|
||||
|
||||
auto result = match_surface(plane_b, plane_a, BoundaryEdge::VMin, opts);
|
||||
|
||||
EXPECT_TRUE(result.g0_ok);
|
||||
EXPECT_LT(result.max_position_error, 1e-4);
|
||||
EXPECT_EQ(result.rows_adjusted, 1);
|
||||
EXPECT_EQ(result.control_displacements.size(), 9u); // 3×3 grid
|
||||
}
|
||||
|
||||
TEST(SurfaceEditingTest, MatchSurfaceG0DifferentSurfaces) {
|
||||
// Two slightly different curved surfaces — match VMin edge
|
||||
auto surf_a = make_test_curved();
|
||||
// Modify a few control points to make it different
|
||||
auto surf_b = make_test_curved();
|
||||
|
||||
MatchSurfaceOptions opts;
|
||||
opts.continuity = ContinuityLevel::G0;
|
||||
opts.boundary_samples = 10;
|
||||
|
||||
auto result = match_surface(surf_b, surf_a, BoundaryEdge::VMin, opts);
|
||||
|
||||
// Should produce a valid surface
|
||||
auto [nu, nv] = result.matched_surface.num_control_points();
|
||||
EXPECT_GE(nu, 1);
|
||||
EXPECT_GE(nv, 1);
|
||||
// Control points should be adjusted
|
||||
EXPECT_GT(result.control_displacements.size(), 0u);
|
||||
}
|
||||
|
||||
TEST(SurfaceEditingTest, MatchSurfaceG1Planar) {
|
||||
auto plane_a = make_test_plane();
|
||||
auto plane_b = make_test_plane();
|
||||
|
||||
MatchSurfaceOptions opts;
|
||||
opts.continuity = ContinuityLevel::G1;
|
||||
opts.boundary_samples = 10;
|
||||
|
||||
auto result = match_surface(plane_b, plane_a, BoundaryEdge::VMin, opts);
|
||||
|
||||
EXPECT_TRUE(result.g0_ok);
|
||||
EXPECT_TRUE(result.g1_ok);
|
||||
EXPECT_EQ(result.rows_adjusted, 1);
|
||||
}
|
||||
|
||||
TEST(SurfaceEditingTest, MatchSurfaceG1DifferentSurfaces) {
|
||||
auto surf_a = make_test_curved();
|
||||
auto surf_b = make_test_curved();
|
||||
|
||||
MatchSurfaceOptions opts;
|
||||
opts.continuity = ContinuityLevel::G1;
|
||||
opts.boundary_samples = 10;
|
||||
|
||||
auto result = match_surface(surf_b, surf_a, BoundaryEdge::VMin, opts);
|
||||
|
||||
// Check that errors are computed
|
||||
EXPECT_GE(result.max_position_error, 0.0);
|
||||
EXPECT_GE(result.max_tangent_error, 0.0);
|
||||
EXPECT_EQ(result.rows_adjusted, 1);
|
||||
}
|
||||
|
||||
TEST(SurfaceEditingTest, MatchSurfaceG2Planar) {
|
||||
auto plane_a = make_test_plane();
|
||||
auto plane_b = make_test_plane();
|
||||
|
||||
MatchSurfaceOptions opts;
|
||||
opts.continuity = ContinuityLevel::G2;
|
||||
opts.boundary_samples = 10;
|
||||
|
||||
auto result = match_surface(plane_b, plane_a, BoundaryEdge::VMin, opts);
|
||||
|
||||
EXPECT_TRUE(result.g0_ok);
|
||||
EXPECT_EQ(result.rows_adjusted, 2);
|
||||
}
|
||||
|
||||
TEST(SurfaceEditingTest, MatchSurfaceG3Planar) {
|
||||
auto plane_a = make_test_plane();
|
||||
auto plane_b = make_test_plane();
|
||||
|
||||
MatchSurfaceOptions opts;
|
||||
opts.continuity = ContinuityLevel::G3;
|
||||
opts.boundary_samples = 10;
|
||||
|
||||
auto result = match_surface(plane_b, plane_a, BoundaryEdge::VMin, opts);
|
||||
|
||||
EXPECT_TRUE(result.g0_ok);
|
||||
EXPECT_EQ(result.rows_adjusted, 3); // G3 adjusts 3 rows
|
||||
}
|
||||
|
||||
TEST(SurfaceEditingTest, MatchSurfaceAllEdges) {
|
||||
auto plane_a = make_test_plane();
|
||||
auto plane_b = make_test_plane();
|
||||
|
||||
MatchSurfaceOptions opts;
|
||||
opts.continuity = ContinuityLevel::G1;
|
||||
|
||||
// Test all 4 boundary edges
|
||||
for (int e = 0; e < 4; ++e) {
|
||||
BoundaryEdge edge = static_cast<BoundaryEdge>(e);
|
||||
auto result = match_surface(plane_b, plane_a, edge, opts);
|
||||
EXPECT_TRUE(result.g0_ok) << "Failed on edge " << e;
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SurfaceEditingTest, MatchSurfaceExplicitRows) {
|
||||
auto plane_a = make_test_plane();
|
||||
auto plane_b = make_test_plane();
|
||||
|
||||
MatchSurfaceOptions opts;
|
||||
opts.continuity = ContinuityLevel::G2;
|
||||
opts.rows_to_adjust = 1; // override, only 1 row
|
||||
opts.boundary_samples = 10;
|
||||
|
||||
auto result = match_surface(plane_b, plane_a, BoundaryEdge::VMin, opts);
|
||||
EXPECT_EQ(result.rows_adjusted, 1);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// control_point_edit 测试
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
|
||||
TEST(SurfaceEditingTest, ControlPointEditSingle) {
|
||||
auto surf = make_test_plane();
|
||||
|
||||
std::vector<std::pair<int,int>> indices = {{1, 1}}; // center control point
|
||||
std::vector<Point3D> new_pos = {{1, 1, 5}}; // lift up in z
|
||||
|
||||
auto result = control_point_edit(surf, indices, new_pos);
|
||||
|
||||
EXPECT_EQ(result.points_modified, 1);
|
||||
EXPECT_GT(result.max_displacement, 4.0); // moved ~5 units in z
|
||||
EXPECT_TRUE(result.weights_preserved);
|
||||
|
||||
// Verify the control point was moved
|
||||
auto modified_cp = result.modified_surface.control_points();
|
||||
EXPECT_NEAR(modified_cp[1][1].z(), 5.0, 1e-6);
|
||||
// Other control points unchanged
|
||||
EXPECT_NEAR(modified_cp[0][0].z(), 0.0, 1e-6);
|
||||
}
|
||||
|
||||
TEST(SurfaceEditingTest, ControlPointEditMultiple) {
|
||||
auto surf = make_test_plane();
|
||||
|
||||
std::vector<std::pair<int,int>> indices = {{0, 0}, {2, 2}};
|
||||
std::vector<Point3D> new_pos = {{0, 0, 3}, {3, 3, 3}};
|
||||
|
||||
auto result = control_point_edit(surf, indices, new_pos);
|
||||
|
||||
EXPECT_EQ(result.points_modified, 2);
|
||||
EXPECT_GT(result.max_displacement, 2.0);
|
||||
|
||||
auto cp = result.modified_surface.control_points();
|
||||
EXPECT_NEAR(cp[0][0].z(), 3.0, 1e-6);
|
||||
EXPECT_NEAR(cp[2][2].z(), 3.0, 1e-6);
|
||||
// Center unchanged
|
||||
EXPECT_NEAR(cp[1][1].z(), 0.0, 1e-6);
|
||||
}
|
||||
|
||||
TEST(SurfaceEditingTest, ControlPointEditEmpty) {
|
||||
auto surf = make_test_plane();
|
||||
|
||||
std::vector<std::pair<int,int>> indices;
|
||||
std::vector<Point3D> new_pos;
|
||||
|
||||
auto result = control_point_edit(surf, indices, new_pos);
|
||||
|
||||
EXPECT_EQ(result.points_modified, 0);
|
||||
EXPECT_NEAR(result.max_displacement, 0.0, 1e-12);
|
||||
|
||||
// Should return a valid copy of original
|
||||
auto cp = result.modified_surface.control_points();
|
||||
EXPECT_EQ(cp.size(), 3u);
|
||||
EXPECT_NEAR(cp[0][0].z(), 0.0, 1e-6);
|
||||
}
|
||||
|
||||
TEST(SurfaceEditingTest, ControlPointEditPreservesKnots) {
|
||||
auto surf = make_test_plane();
|
||||
|
||||
std::vector<std::pair<int,int>> indices = {{0, 0}};
|
||||
std::vector<Point3D> new_pos = {{-1, -1, 0}};
|
||||
|
||||
auto result = control_point_edit(surf, indices, new_pos);
|
||||
|
||||
// Knot vectors preserved
|
||||
const auto& ku = result.modified_surface.knots_u();
|
||||
const auto& kv = result.modified_surface.knots_v();
|
||||
EXPECT_EQ(ku.size(), 6u);
|
||||
EXPECT_EQ(kv.size(), 6u);
|
||||
EXPECT_EQ(result.modified_surface.degree_u(), 2);
|
||||
EXPECT_EQ(result.modified_surface.degree_v(), 2);
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace vde::curves
|
||||
Reference in New Issue
Block a user