feat: 测试覆盖扩展 + 文档更新

This commit is contained in:
茂之钳
2026-07-23 12:36:39 +00:00
parent 41a8992332
commit f2203c7255
42 changed files with 3448 additions and 106 deletions
+33
View File
@@ -8,7 +8,40 @@ using core::Point2D;
using core::Polygon2D;
using mesh::HalfedgeMesh;
/// ── Core 3D mesh boolean operations ──────────────────────
/// Union: A B — keep faces of each mesh whose centroids lie outside the other
HalfedgeMesh mesh_union(const HalfedgeMesh& a, const HalfedgeMesh& b);
/// Intersection: A ∩ B — keep faces whose centroids lie inside the other
HalfedgeMesh mesh_intersection(const HalfedgeMesh& a, const HalfedgeMesh& b);
/// Difference: A B — keep A-faces outside B
HalfedgeMesh mesh_difference(const HalfedgeMesh& a, const HalfedgeMesh& b);
/// Symmetric difference: (A B) (B A)
HalfedgeMesh mesh_sym_diff(const HalfedgeMesh& a, const HalfedgeMesh& b);
/// ── Helpers ──────────────────────────────────────────────
/// Crop mesh `a` by mesh `b`'s boundary: keep faces of `a` whose
/// centroids lie OUTSIDE the volume of `b`.
/// If `invert_b` is true, test against the flipped-volume (inside↔outside) of `b`.
HalfedgeMesh clip_mesh(const HalfedgeMesh& a, const HalfedgeMesh& b,
bool invert_b = false);
/// Invert all face orientations in the mesh
HalfedgeMesh invert_mesh(const HalfedgeMesh& m);
/// Merge two meshes into one (concatenate vertices and faces)
HalfedgeMesh merge_meshes(const HalfedgeMesh& a, const HalfedgeMesh& b);
/// Convenience dispatcher — route by BooleanOp enum
HalfedgeMesh mesh_boolean(const HalfedgeMesh& a, const HalfedgeMesh& b, BooleanOp op);
/// ── Point classification ─────────────────────────────────
/// Test whether point `p` is inside `mesh` using ray-casting
bool is_point_inside_mesh(const core::Point3D& p, const HalfedgeMesh& mesh);
} // namespace vde::boolean
+44
View File
@@ -2,13 +2,16 @@
#include "vde/core/point.h"
#include "vde/core/line.h"
#include "vde/core/triangle.h"
#include "vde/core/aabb.h"
#include <optional>
#include <vector>
namespace vde::collision {
using core::Point3D;
using core::Vector3D;
using core::Triangle3D;
using core::Ray3Dd;
using core::AABB3D;
struct RayTriResult {
double t;
@@ -16,6 +19,7 @@ struct RayTriResult {
Point3D point;
};
// Ray-Triangle (Möller-Trumbore)
std::optional<RayTriResult> ray_triangle_intersect(
const Point3D& origin, const Vector3D& dir, const Triangle3D& tri);
@@ -24,4 +28,44 @@ inline std::optional<RayTriResult> ray_triangle_intersect(
return ray_triangle_intersect(ray.origin(), ray.direction(), tri);
}
// Ray-AABB (slab method). Returns true if hit, with t interval.
bool ray_aabb_intersect(const Ray3Dd& ray, const AABB3D& box,
double& tmin_out, double& tmax_out);
// Ray-Sphere
struct RaySphereResult {
double t;
Point3D point;
};
std::optional<RaySphereResult> ray_sphere_intersect(
const Point3D& origin, const Vector3D& dir,
const Point3D& center, double radius);
inline std::optional<RaySphereResult> ray_sphere_intersect(
const Ray3Dd& ray, const Point3D& center, double radius) {
return ray_sphere_intersect(ray.origin(), ray.direction(), center, radius);
}
// Ray-Plane (returns t value)
std::optional<double> ray_plane_intersect(
const Point3D& origin, const Vector3D& dir,
const Point3D& plane_point, const Vector3D& plane_normal);
inline std::optional<double> ray_plane_intersect(
const Ray3Dd& ray, const Point3D& plane_point,
const Vector3D& plane_normal) {
return ray_plane_intersect(ray.origin(), ray.direction(),
plane_point, plane_normal);
}
// Ray-Mesh — traverse all triangles, return closest hit
std::optional<RayTriResult> ray_mesh_intersect(
const Point3D& origin, const Vector3D& dir,
const std::vector<Triangle3D>& triangles);
inline std::optional<RayTriResult> ray_mesh_intersect(
const Ray3Dd& ray, const std::vector<Triangle3D>& triangles) {
return ray_mesh_intersect(ray.origin(), ray.direction(), triangles);
}
} // namespace vde::collision
+15
View File
@@ -1,9 +1,24 @@
#pragma once
#include "vde/core/triangle.h"
#include "vde/core/aabb.h"
namespace vde::collision {
using core::Triangle3D;
using core::AABB3D;
using core::Point3D;
// Separating-axis test for two triangles
bool tri_tri_intersect(const Triangle3D& t1, const Triangle3D& t2);
// Detailed tri-tri intersection — returns the intersection segment
struct TriTriIntersection {
bool intersects;
Point3D p0, p1;
};
TriTriIntersection tri_tri_intersect_detailed(const Triangle3D& t1,
const Triangle3D& t2);
// Fast SAT test: triangle vs AABB (13 separating axes)
bool tri_aabb_overlap(const Triangle3D& tri, const AABB3D& box);
} // namespace vde::collision
+12
View File
@@ -3,16 +3,28 @@
#include "vde/core/line.h"
#include "vde/core/plane.h"
#include "vde/core/triangle.h"
#include "vde/core/aabb.h"
namespace vde::core {
// ── Point distances ──────────────────────────
double distance(const Point3D& a, const Point3D& b);
double distance(const Point3D& p, const Line3D<double>& line);
double distance(const Point3D& p, const Segment3D<double>& seg);
double distance(const Point3D& p, const Plane<double>& plane);
double distance(const Point3D& p, const Triangle<double>& tri);
double distance(const Point3D& p, const AABB<double>& box);
// ── Feature-pair distances ───────────────────
double distance(const Segment3D<double>& a, const Segment3D<double>& b);
double distance(const Line3D<double>& a, const Line3D<double>& b);
double distance(const AABB<double>& a, const AABB<double>& b);
double distance(const Triangle<double>& a, const Triangle<double>& b);
// ── Closest-point queries ────────────────────
Point3D closest_point(const Point3D& p, const Triangle<double>& tri);
Point3D closest_point(const Point3D& p, const Segment3D<double>& seg);
Point3D closest_point(const Point3D& p, const AABB<double>& box);
Point3D closest_point(const Point3D& p, const Plane<double>& plane);
} // namespace vde::core
+21
View File
@@ -1,6 +1,8 @@
#pragma once
#include "vde/core/point.h"
#include "vde/core/aabb.h"
#include <vector>
#include <array>
namespace vde::core {
@@ -19,12 +21,31 @@ public:
/// Absolute area
[[nodiscard]] double area() const { return std::abs(signed_area()); }
/// Perimeter (sum of edge lengths)
[[nodiscard]] double perimeter() const;
/// Area-weighted centroid
[[nodiscard]] Point2D centroid() const;
/// Axis-aligned bounding box (2D, stored in AABB3D with z=0)
[[nodiscard]] AABB<double> bounding_box() const;
/// Point-in-polygon test (ray casting)
[[nodiscard]] bool contains(const Point2D& p) const;
/// Is the polygon counter-clockwise?
[[nodiscard]] bool is_ccw() const { return signed_area() > 0; }
/// Douglas-Peucker simplification. Returns a new simplified polygon.
[[nodiscard]] Polygon2D simplify(double tolerance) const;
/// Ear-clipping triangulation. Returns triangles as triples of vertex indices.
/// Assumes a simple polygon (no self-intersections). CCW ordering required.
[[nodiscard]] std::vector<std::array<int, 3>> triangulate() const;
/// Andrew's monotone chain convex hull of a point set.
static Polygon2D convex_hull_2d(const std::vector<Point2D>& points);
private:
std::vector<Point2D> vertices_;
};
+4
View File
@@ -24,6 +24,10 @@ public:
/// Evaluate basis functions at t
[[nodiscard]] std::vector<double> basis_functions(double t, int span = -1) const;
/// Evaluate first derivatives of non-zero basis functions at t
/// Returns dN_{span-degree+i, degree}/du for i = 0..degree
[[nodiscard]] std::vector<double> basis_derivatives(double t, int span = -1) const;
private:
std::vector<Point3D> cp_;
std::vector<double> knots_;
+4
View File
@@ -14,6 +14,10 @@ public:
int degree_u, int degree_v);
[[nodiscard]] Point3D evaluate(double u, double v) const;
[[nodiscard]] Vector3D derivative_u(double u, double v) const;
[[nodiscard]] Vector3D derivative_v(double u, double v) const;
[[nodiscard]] Vector3D normal(double u, double v) const;
[[nodiscard]] int degree_u() const { return degree_u_; }
[[nodiscard]] int degree_v() const { return degree_v_; }
[[nodiscard]] const std::vector<double>& knots_u() const { return knots_u_; }
+18 -1
View File
@@ -2,12 +2,13 @@
#include "vde/core/point.h"
#include <vector>
#include <array>
#include <functional>
namespace vde::curves {
using core::Point3D;
using core::Vector3D;
/// Tessellate a parametric surface into a triangle mesh
/// Tessellate a parametric surface into a uniform triangle mesh
/// @param eval Function(u,v) -> Point3D
/// @param res_u, res_v Number of samples in u/v directions
/// @return Pairs of (vertices, triangle indices)
@@ -15,4 +16,20 @@ std::pair<std::vector<Point3D>, std::vector<std::array<int, 3>>>
tessellate(const std::function<Point3D(double,double)>& eval,
int res_u, int res_v);
/// Adaptive tessellation based on normal-angle deviation
/// Recursively subdivides the parameter domain when the angular deviation
/// between corner normals exceeds the threshold.
/// @param eval Function(u,v) -> Point3D
/// @param normal_fn Function(u,v) -> Vector3D (unit normal)
/// @param min_res Minimum subdivisions per direction (1 = single quad)
/// @param max_depth Maximum recursion depth (limits total subdivisions)
/// @param angle_threshold_deg Maximum normal-angle deviation in degrees
/// @return Pairs of (vertices, triangle indices)
std::pair<std::vector<Point3D>, std::vector<std::array<int, 3>>>
adaptive_tessellate(
const std::function<Point3D(double,double)>& eval,
const std::function<Vector3D(double,double)>& normal_fn,
int min_res = 2, int max_depth = 6,
double angle_threshold_deg = 5.0);
} // namespace vde::curves
+19 -1
View File
@@ -7,8 +7,26 @@ namespace vde::foundation {
struct ObjMeshData {
std::vector<Point3D> vertices;
std::vector<Point2D> texcoords;
std::vector<Vector3D> normals;
std::vector<std::vector<int>> faces; // vertex indices (1-based in file, 0-based here)
/// Vertex indices per face (0-based internally).
/// Always populated regardless of face format.
std::vector<std::vector<int>> faces;
/// Texcoord indices per face (0-based). Empty if the file had no vt data.
/// Same length as faces; each sub-vector same length as corresponding face.
std::vector<std::vector<int>> face_texcoords;
/// Normal indices per face (0-based). Empty if the file had no vn data.
std::vector<std::vector<int>> face_normals;
/// Material name per face group (appears before the faces that use it).
/// The i-th entry gives the material active for the i-th face.
std::vector<std::string> face_materials;
/// Helpers
[[nodiscard]] bool has_texcoords() const { return !texcoords.empty(); }
};
ObjMeshData read_obj(const std::string& filepath);
+6
View File
@@ -10,7 +10,13 @@ struct StlTriangle {
Point3D v0, v1, v2;
};
/// Auto-detect format (binary or ASCII) and read.
std::vector<StlTriangle> read_stl(const std::string& filepath);
/// Always write binary STL.
void write_stl(const std::string& filepath, const std::vector<StlTriangle>& tris);
/// Write ASCII STL (human-readable).
void write_stl_ascii(const std::string& filepath, const std::vector<StlTriangle>& tris);
} // namespace vde::foundation
+76
View File
@@ -1,12 +1,16 @@
#pragma once
#include "vde/mesh/halfedge_mesh.h"
#include <cmath>
#include <vector>
#include <string>
namespace vde::mesh {
using core::Point2D;
using core::Point3D;
using core::Vector3D;
// ── Triangle quality ────────────────────────────────
struct MeshQuality {
double min_angle_deg = 0;
double max_angle_deg = 0;
@@ -17,4 +21,76 @@ struct MeshQuality {
MeshQuality evaluate_mesh_quality(const HalfedgeMesh& mesh);
// ── Tetrahedron quality ─────────────────────────────
struct TetQuality {
double min_jacobian = 0.0; // scaled Jacobian (normalised, 0=bad … 1=regular)
double max_jacobian = 0.0;
double avg_jacobian = 0.0;
double min_dihedral_deg = 0.0; // minimum dihedral angle (degrees)
double max_dihedral_deg = 0.0; // maximum dihedral angle
double max_aspect_ratio = 0.0; // circumsphere-radius / shortest-edge
size_t degenerate_tets = 0;
size_t total_tets = 0;
};
/// Evaluate tetrahedron quality from a triangle mesh.
/// This function interprets the mesh as a tetrahedral mesh where
/// each face belongs to a tet defined by the face vertices + face centroid
/// (suitable for closed triangle surfaces interpreted as volume boundaries).
TetQuality evaluate_tet_quality(const HalfedgeMesh& mesh);
// ── Generic element quality ─────────────────────────
enum class ElementType { Tri, Quad, Tet, Hex };
struct ElemQuality {
double min_jacobian = 0.0;
double max_jacobian = 0.0;
double avg_jacobian = 0.0;
size_t total_elements = 0;
size_t degenerate = 0;
};
/// Dispatch by element type; implemented for Tri and Tet (Quad/Hex return stub).
ElemQuality evaluate_element_quality(const HalfedgeMesh& mesh, ElementType type);
// ── Quality report ──────────────────────────────────
struct QualityReport {
std::string element_type; // "triangle" / "tetrahedron"
size_t total = 0;
size_t degenerate = 0;
// Binned histogram (quality ∈ [0,1])
static constexpr int kBins = 10;
std::array<size_t, kBins> histogram{};
// Grade counts (A=excellent B=good C=acceptable D=poor F=fail)
size_t grade_A = 0, grade_B = 0, grade_C = 0, grade_D = 0, grade_F = 0;
double min_quality = 0.0;
double max_quality = 0.0;
double avg_quality = 0.0;
double stddev_quality = 0.0;
};
/// Generate a full quality report from a triangle surface mesh.
QualityReport mesh_quality_report(const HalfedgeMesh& mesh);
/// Generate a full quality report from a tetrahedral mesh.
/// The mesh is treated as having 4-vertex faces (tets) in the
/// same index order as the surface-mesh face loop.
QualityReport mesh_quality_report_tet(const HalfedgeMesh& mesh);
// ── Utility ─────────────────────────────────────────
/// Compute the scaled Jacobian (determinant / (product of edge-lengths))
/// for a triangle; returns [1,1], 1 = equilateral.
double tri_scaled_jacobian(const Point3D& a, const Point3D& b, const Point3D& c);
/// Compute the scaled Jacobian for a tetrahedron; returns [0,1].
double tet_scaled_jacobian(const Point3D& a, const Point3D& b,
const Point3D& c, const Point3D& d);
} // namespace vde::mesh
+26 -3
View File
@@ -6,15 +6,38 @@ using core::Point2D;
using core::Point3D;
using core::Vector3D;
enum class SmoothMethod { Laplacian, Taubin };
enum class SmoothMethod { Laplacian, Taubin, HCLaplacian, Bilateral };
struct SmoothOptions {
int iterations = 10;
double lambda = 0.5; // Laplacian weight
double mu = -0.53; // Taubin shrink (mu < -lambda for Taubin)
double lambda = 0.5; // Laplacian weight
double mu = -0.53; // Taubin shrink (mu < -lambda for Taubin)
// HC Laplacian parameters
double hc_alpha = 0.0; // push-back strength (0 = default auto)
double hc_beta = 0.5; // push-forward blend
// Bilateral parameters
double bilateral_sigma_c = 0.0; // spatial sigma (0 = auto from avg edge length)
double bilateral_sigma_n = 0.3; // normal sigma (radians)
int bilateral_iters = 5;
SmoothMethod method = SmoothMethod::Laplacian;
};
/// Generic dispatcher — delegates to specific smooth functions
HalfedgeMesh smooth_mesh(const HalfedgeMesh& mesh, const SmoothOptions& opts = {});
/// Standard Laplacian smoothing
HalfedgeMesh smooth_laplacian(const HalfedgeMesh& mesh, int iterations, double lambda);
/// Taubin λ|μ smoothing (volume-preserving)
HalfedgeMesh smooth_taubin(const HalfedgeMesh& mesh, int iterations, double lambda, double mu);
/// HC Laplacian (Humphrey's Classes) — two-pass with push-back to preserve volume
HalfedgeMesh smooth_hc_laplacian(const HalfedgeMesh& mesh, const SmoothOptions& opts = {});
/// Bilateral mesh filtering — normal-weighted, preserves sharp features
HalfedgeMesh smooth_bilateral(const HalfedgeMesh& mesh, const SmoothOptions& opts = {});
} // namespace vde::mesh
+19 -1
View File
@@ -1,6 +1,8 @@
#pragma once
#include "vde/spatial/spatial_index.h"
#include "vde/core/aabb.h"
#include <queue>
#include <limits>
namespace vde::spatial {
using core::Point3D;
@@ -8,6 +10,15 @@ using core::Vector3D;
using core::Ray3Dd;
using core::Triangle3D;
template <typename T>
struct RTreeNode {
AABB3D bbox;
bool is_leaf = true;
std::vector<size_t> children; // internal: indices into nodes_
std::vector<size_t> item_indices; // leaf: indices into items_
size_t parent = static_cast<size_t>(-1);
};
template <typename T>
class RTree : public SpatialIndex<T> {
public:
@@ -19,9 +30,16 @@ public:
std::vector<T> query_ray(const Ray3Dd& ray) const override;
void clear() override;
size_t size() const override { return count_; }
size_t node_count() const { return nodes_.size(); }
private:
void query_range_recursive(size_t node_idx, const AABB3D& range,
std::vector<T>& result) const;
std::vector<T> items_;
std::vector<AABB3D> bounds_;
std::vector<AABB3D> item_bounds_;
std::vector<RTreeNode<T>> nodes_;
size_t root_idx_ = 0;
size_t count_ = 0;
};