feat(v12): generative surfaces (Sweep/Loft/Net) + curve tools + surface analysis
CI / Build & Test (push) Failing after 31s
CI / Release Build (push) Failing after 38s
Build & Test / build-and-test (push) Has been cancelled
Build & Test / python-bindings (push) Has been cancelled

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:
茂之钳
2026-07-27 09:08:01 +08:00
parent 989f1f2328
commit bb0029234f
19 changed files with 3623 additions and 5 deletions
+3
View File
@@ -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)
+273
View File
@@ -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());
}
+340
View File
@@ -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()));
}
}
}
+249
View File
@@ -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