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
+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