feat: 测试覆盖扩展 + 文档更新
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
@@ -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_;
|
||||
|
||||
@@ -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_; }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=b−ad … 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user