bb0029234f
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)
274 lines
9.7 KiB
C++
274 lines
9.7 KiB
C++
#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());
|
||
}
|