Files
ViewDesignEngine/tests/curves/test_curve_tools.cpp
T
茂之钳 bb0029234f
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
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)
2026-07-27 09:08:01 +08:00

274 lines
9.7 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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());
}