docs: doxygen annotations for curves + mesh + sketch
CI / Build & Test (push) Failing after 29s
CI / Release Build (push) Failing after 38s

This commit is contained in:
茂之钳
2026-07-24 11:04:04 +00:00
parent b97b0415f5
commit 4c9ee4f760
53 changed files with 5307 additions and 506 deletions
+37 -4
View File
@@ -8,12 +8,45 @@ using core::Point2D;
using core::Point3D;
using core::Vector3D;
/// Alpha Shapes: extract surface from point cloud
/// For alpha → ∞, returns convex hull; for alpha → 0, returns all faces
/// @param alpha Radius threshold: faces with circumradius > alpha are removed
/**
* @brief Alpha Shapes 点云表面重建
*
* 从 3D 点云的 Delaunay 四面体化中提取表面,通过参数 α 控制细节级别:
* - α → ∞:返回凸包(所有边界四面体面都保留)
* - α → 0:返回所有 Delaunay 面(非常详细,含内部结构)
* - 中间值:剔除外接球半径 > α 的四面体,保留细节适中的表面
*
* 算法:对每个 Delaunay 四面体,检查其外接球半径。
* 若半径 ≤ α,四面体保留;否则剔除。最终表面 = 保留四面体与
* 被剔除四面体(或外部)之间的边界三角形面。
*
* @param points 输入 3D 点云
* @param alpha α 半径阈值,通常取平均点间距的 1~3 倍
* @return TetrahedronMesh 保留的四面体网格
*
* @code{.cpp}
* // 从点云重建表面
* auto tets = alpha_shapes(point_cloud, 0.5);
* auto [verts, tris] = alpha_shapes_surface(tets);
* @endcode
* @see alpha_shapes_surface delaunay_3d
* @ingroup mesh
*/
TetrahedronMesh alpha_shapes(const std::vector<Point3D>& points, double alpha);
/// Convert alpha shapes tet mesh to triangle surface
/**
* @brief 将 Alpha Shapes 四面体网格的表面提取为三角形网格
*
* 遍历四面体网格,提取所有仅出现在一个四面体上的面(边界面),
* 即为重建的表面三角形。
*
* @param tet_mesh Alpha Shapes 输出的四面体网格
* @return {顶点数组, 三角形索引数组}
*
* @note 表面三角形法向指向四面体外部(右手定则)
* @see alpha_shapes
* @ingroup mesh
*/
std::pair<std::vector<Point3D>, std::vector<std::array<int,3>>>
alpha_shapes_surface(const TetrahedronMesh& tet_mesh);
+23 -2
View File
@@ -8,8 +8,29 @@ using core::Point2D;
using core::Point3D;
using core::Vector3D;
/// Constrained Delaunay Triangulation (CDT) in 2D
/// Ensures specified constraint edges appear in the triangulation
/**
* @brief 约束 Delaunay 三角化(CDT
*
* 在标准 Delaunay 三角化基础上强制指定的约束边必须出现在三角化中。
* 通过边翻转(edge flip)反复消除与约束边相交的三角形边,直到所有
* 约束边都成为三角形边。
*
* 约束边在输出三角化中一定存在(作为三角形边),但不保证满足
* 空圆性质——靠近约束边的三角形可能违反 Delaunay 条件。
*
* @param points 输入 2D 点集
* @param constraints 约束边列表,每项为 {起点索引, 终点索引}
* @return DelaunayResult 含约束边的三角化结果
*
* @note 约束边不能相交(否则行为未定义);自相交约束需先分割交点
* @code{.cpp}
* // 带孔洞的三角化:约束边标记孔洞边界
* auto result = constrained_delaunay_2d(points,
* {{0,1}, {1,2}, {2,1}, {3,0}});
* @endcode
* @see delaunay_2d
* @ingroup mesh
*/
DelaunayResult constrained_delaunay_2d(
const std::vector<Point2D>& points,
const std::vector<std::pair<int, int>>& constraints);
+31 -3
View File
@@ -8,12 +8,40 @@ using core::Point2D;
using core::Point3D;
using core::Vector3D;
/**
* @brief 2D Delaunay 三角化结果
* @ingroup mesh
*/
struct DelaunayResult {
std::vector<Point2D> vertices;
std::vector<std::array<int, 3>> triangles; // indices into vertices
std::vector<Point2D> vertices; ///< 顶点坐标(可能含超级三角形的辅助点)
std::vector<std::array<int, 3>> triangles; ///< 三角形索引(CCW 顺序)
};
/// Bowyer-Watson incremental Delaunay triangulation, O(n²) worst, O(n log n) typical
/**
* @brief 2D Delaunay 三角化(Bowyer-Watson 增量算法)
*
* 构建点集的 Delaunay 三角化:任意三角形的外接圆内不含其他点。
* 该性质使三角化的最小角最大化,避免狭长三角形。
*
* 算法流程:
* 1. 创建包围所有输入点的超级三角形
* 2. 逐点插入:
* a. 找出外接圆包含新点的所有三角形("坏三角形")
* b. 删除坏三角形,形成多边形空洞
* c. 将新点与空洞边界各边连接形成新三角形
* 3. 删除与超级三角形顶点相关的三角形
*
* @param points 输入 2D 点集(至少 3 个不共线点)
* @return DelaunayResult 三角化结果
*
* @note 时间复杂度:平均 O(n log n),最坏 O(n²)
* @code{.cpp}
* auto result = delaunay_2d({{0,0}, {1,0}, {0.5, 0.5}, {0,1}, {1,1}});
* // result.triangles 为 Delaunay 三角形索引
* @endcode
* @see constrained_delaunay_2d
* @ingroup mesh
*/
DelaunayResult delaunay_2d(const std::vector<Point2D>& points);
} // namespace vde::mesh
+26 -3
View File
@@ -8,12 +8,35 @@ using core::Point2D;
using core::Point3D;
using core::Vector3D;
/**
* @brief 四面体网格结构
* @ingroup mesh
*/
struct TetrahedronMesh {
std::vector<Point3D> vertices;
std::vector<std::array<int, 4>> tetrahedra; // 4 vertex indices
std::vector<Point3D> vertices; ///< 顶点坐标
std::vector<std::array<int, 4>> tetrahedra; ///< 四面体索引,每个 {i0,i1,i2,i3}
};
/// 3D Delaunay tetrahedralization (Bowyer-Watson)
/**
* @brief 3D Delaunay 四面体化(Bowyer-Watson 增量算法)
*
* 将 3D 点集剖分为 Delaunay 四面体网格:任意四面体的外接球内不含其他顶点。
* 算法是 2D 版本的自然推广:
* 1. 构建超级四面体包含所有点
* 2. 逐点插入:删除外接球包含新点的四面体 → 空洞 → 重连
* 3. 去除超级四面体相关单元
*
* @param points 输入 3D 点集(至少 4 个不共面点)
* @return TetrahedronMesh 四面体网格
*
* @note 时间复杂度与 2D 类似:平均 O(n log n),最坏 O(n²)
* @note 输出包括内部和边界四面体;通过 alpha_shapes 可提取表面
* @code{.cpp}
* auto tet_mesh = delaunay_3d(point_cloud);
* @endcode
* @see alpha_shapes
* @ingroup mesh
*/
TetrahedronMesh delaunay_3d(const std::vector<Point3D>& points);
} // namespace vde::mesh
+26 -4
View File
@@ -7,10 +7,32 @@ using core::Point2D;
using core::Point3D;
using core::Vector3D;
/// Geodesic distance from source vertices using the Heat Method (Crane et al. 2013)
/// Returns per-vertex distance from the nearest source
/// @param sources Source vertex indices
/// @param t Time step (default = average edge length squared)
/**
* @brief 热方法(Heat Method)计算测地距离
*
* 基于 Crane et al. 2013 的算法,通过求解三个线性系统高效计算
* 曲面上任意点到源点集的测地距离(Geodesic Distance)。
*
* 算法三步:
* 1. 热扩散:求解 (I - t·L)·u = u₀,其中 u₀ 在源点为 1 其余为 0
* 2. 梯度归一化:X = -∇u / |∇u|
* 3. 泊松重建:L·φ = div(X),得到测地距离 φ
*
* 相比精确测地线算法(如 MMP),热方法更快(~线性时间)且易于实现,
* 但精度受时间步长 t 和网格分辨率影响。
*
* @param mesh 输入三角网格
* @param sources 源顶点索引列表(距离为 0 的顶点)
* @param t 时间步长,< 0 时自动设为平均边长平方
* @return 每顶点到最近源点的测地距离
*
* @note 要求网格为连通流形;时间步长 t 越小精度越高但数值越不稳定
* @code{.cpp}
* auto dist = geodesic_distance(mesh, {0, 10, 20});
* // dist[i] = 顶点 i 到 {0,10,20} 中最近顶点的测地距离
* @endcode
* @ingroup mesh
*/
std::vector<double> geodesic_distance(const HalfedgeMesh& mesh,
const std::vector<int>& sources,
double t = -1.0);
+195 -19
View File
@@ -9,67 +9,243 @@ using core::AABB3D;
using core::Vector3D;
using core::Point3D;
/**
* @brief 半边数据结构元素
*
* 每条半边存储:起点顶点索引、所属面索引、前驱/后继/对侧半边索引。
* 对侧半边通过 opposite_index 配对,相邻两半边索引总是 {2k, 2k+1}。
*
* @ingroup mesh
*/
struct Halfedge {
int vertex_index = -1;
int face_index = -1;
int next_index = -1;
int prev_index = -1;
int opposite_index = -1;
int vertex_index = -1; ///< 半边起点(终点 = next 半边的起点)
int face_index = -1; ///< 所属面的索引(边界半边为 -1)
int next_index = -1; ///< 沿面的下一条半边
int prev_index = -1; ///< 沿面的上一条半边
int opposite_index = -1; ///< 对侧半边索引(反方向半边)
};
/**
* @brief 面元素
*
* 存储面的起始半边索引和边数(阶)。
*
* @ingroup mesh
*/
struct Face {
int halfedge_index = -1;
int valence = 3;
int halfedge_index = -1; ///< 该面的任意一条半边
int valence = 3; ///< 边数(三角面 = 3
};
/**
* @brief 半边数据结构三角网格
*
* 核心拓扑数据结构,支持 O(1) 访问面的邻面、顶点的邻面环、
* 边界检测等拓扑查询。数据内部一致性由构造方法保证。
*
* 数据成员:
* - vertices_: 顶点位置数组
* - halfedges_: 半边数组,相邻索引为对侧半边(0↔1, 2↔3, ...)
* - faces_: 面数组,每个面对应一条起始半边
* - face_normals_ / vertex_normals_: 延迟计算的法向缓存
*
* @ingroup mesh
*/
class HalfedgeMesh {
public:
HalfedgeMesh() = default;
// ── Construction ──
// ── Construction ──────────────────────────────────────
/**
* @brief 清空所有数据
*/
void clear();
/**
* @brief 添加顶点
* @param p 3D 坐标
* @return 新顶点的索引(0-based
*/
int add_vertex(const Point3D& p);
/**
* @brief 添加面
* @param vertex_indices 按逆时针顺序排列的顶点索引(至少 3 个)
* @return 新面的索引,失败返回 -1
* @note 自动创建/查找半边并设置对侧关系;面法向遵循右手定则
*/
int add_face(const std::vector<int>& vertex_indices);
/**
* @brief 从三角形列表批量构建半边形网格
* @param verts 顶点位置数组
* @param tris 三角形索引数组,每个 {i0,i1,i2}
* @note 等价于反复调用 add_vertex + add_face,但内部做批处理优化
* @code{.cpp}
* HalfedgeMesh mesh;
* mesh.build_from_triangles(vertices, triangles);
* @endcode
*/
void build_from_triangles(const std::vector<Point3D>& verts,
const std::vector<std::array<int, 3>>& tris);
// ── Accessors ──
// ── Accessors ─────────────────────────────────────────
/**
* @brief 顶点数量
* @return vertices_ 大小
*/
[[nodiscard]] size_t num_vertices() const { return vertices_.size(); }
/**
* @brief 面数量
* @return faces_ 大小
*/
[[nodiscard]] size_t num_faces() const { return faces_.size(); }
/**
* @brief 边数量
* @return halfedges_.size() / 2
*/
[[nodiscard]] size_t num_edges() const { return halfedges_.size() / 2; }
/**
* @brief 访问顶点
* @param idx 顶点索引,0 ≤ idx < num_vertices()
* @return 顶点坐标只读引用
*/
[[nodiscard]] const Point3D& vertex(size_t idx) const { return vertices_[idx]; }
/**
* @brief 访问面
* @param idx 面索引
* @return Face 只读引用
*/
[[nodiscard]] const Face& face(size_t idx) const { return faces_[idx]; }
/**
* @brief 访问半边
* @param idx 半边索引
* @return Halfedge 只读引用
*/
[[nodiscard]] const Halfedge& halfedge(size_t idx) const { return halfedges_[idx]; }
/**
* @brief 更新顶点位置
* @param idx 顶点索引
* @param p 新坐标
* @note 标记法向缓存为脏,下次查询时重新计算
*/
void set_vertex(size_t idx, const Point3D& p) { vertices_[idx] = p; normals_dirty_ = true; }
// ── Topology queries ──
// ── Topology queries ──────────────────────────────────
/**
* @brief 获取面的所有顶点索引(按环绕顺序)
* @param fi 面索引
* @return 顶点索引序列
*/
[[nodiscard]] std::vector<int> face_vertices(int fi) const;
/**
* @brief 获取顶点的邻面索引环
* @param vi 顶点索引
* @return 所有以 vi 为顶点的面索引
*/
[[nodiscard]] std::vector<int> vertex_faces(int vi) const;
/**
* @brief 是否为边界半边
* @param hei 半边索引
* @return 没有所属面时为 true
*/
[[nodiscard]] bool is_boundary_edge(int hei) const { return halfedges_[hei].face_index < 0; }
/**
* @brief 是否为边界顶点
* @param vi 顶点索引
* @return 邻接任何边界半边时为 true
*/
[[nodiscard]] bool is_boundary_vertex(int vi) const;
// ── Normals ──
// ── Normals ───────────────────────────────────────────
/**
* @brief 面法向(几何法向,非归一化?由实现决定)
* @param fi 面索引
* @return (v1-v0)×(v2-v0) 归一化结果
*/
[[nodiscard]] Vector3D face_normal(int fi) const;
/**
* @brief 顶点法向(邻面法向的面积加权平均)
* @param vi 顶点索引
* @return 归一化顶点法向
*/
[[nodiscard]] Vector3D vertex_normal(int vi) const;
/**
* @brief 重新计算所有面法向和顶点法向
* @note 修改顶点位置后自动标记脏位,调用此方法触发重新计算
*/
void update_normals();
// ── Bounds ──
// ── Bounds ────────────────────────────────────────────
/**
* @brief 网格包围盒
* @return AABB3D 轴对齐包围盒
*/
[[nodiscard]] AABB3D bounds() const;
// ── Iterators for range-based for ──
// ── Iterators for range-based for ─────────────────────
/**
* @brief 面迭代器范围(支持 for-range)
*/
class FaceRange;
/**
* @brief 顶点单邻环迭代器
*/
class VertexOneRing;
/**
* @brief 获取所有面范围(range-based for 支持)
* @return FaceRange 对象
* @code{.cpp}
* for (auto& f : mesh.faces_range()) { ... }
* @endcode
*/
[[nodiscard]] FaceRange faces_range() const;
/**
* @brief 获取顶点的 1-ring 邻域迭代器
* @param vi 顶点索引
* @return VertexOneRing 对象
* @code{.cpp}
* for (int vj : mesh.vertex_one_ring(vi)) {
* // vj 是 vi 的直接邻居
* }
* @endcode
*/
[[nodiscard]] VertexOneRing vertex_one_ring(int vi) const;
private:
std::vector<Point3D> vertices_;
std::vector<Halfedge> halfedges_;
std::vector<Face> faces_;
std::vector<Vector3D> face_normals_;
std::vector<Vector3D> vertex_normals_;
bool normals_dirty_ = true;
std::vector<Point3D> vertices_; ///< 顶点位置
std::vector<Halfedge> halfedges_; ///< 半边数组(成对存储)
std::vector<Face> faces_; ///< 面数组
std::vector<Vector3D> face_normals_; ///< 面法向缓存
std::vector<Vector3D> vertex_normals_; ///< 顶点法向缓存
bool normals_dirty_ = true; ///< 法向脏标记
/**
* @brief 查找或创建半边 (v0→v1)
* @param v0 起点顶点索引
* @param v1 终点顶点索引
* @return 半边索引
*/
int find_or_create_edge(int v0, int v1);
};
+80 -8
View File
@@ -3,30 +3,82 @@
#include <vector>
#include <array>
#include <functional>
#include <cmath>
#include <algorithm>
namespace vde::mesh {
using core::Point2D;
using core::Point3D;
using core::Vector3D;
/**
* @brief Marching Cubes 输出网格
* @ingroup mesh
*/
struct MCMesh {
std::vector<Point3D> vertices;
std::vector<std::array<int, 3>> triangles;
std::vector<Point3D> vertices; ///< 顶点坐标
std::vector<std::array<int, 3>> triangles; ///< 三角形索引
};
/// Marching Cubes for scalar field f(x,y,z)
/// Extracts isosurface at value = iso_level within the bounding box
/**
* @brief Marching Cubes 等值面提取
*
* 从标量场 f(x,y,z) 中提取等值面 S = { (x,y,z) | f(x,y,z) = iso_level }。
* 将包围盒划分为 resolution×resolution×resolution 体素网格,
* 在每个体素的 8 个角求值 f,查表确定等值面穿越该体素的三角形配置。
*
* @param f 标量场函数 f(x,y,z) → double
* @param iso_level 等值面值(默认 0
* @param bmin 包围盒最小角
* @param bmax 包围盒最大角
* @param resolution 每轴采样分辨率(≥ 1),总网格 = resolution³ 个立方体
* @return MCMesh 三角形网格
*
* @note 使用经典的 256 查表法(Lorensen & Cline 1987
* @code{.cpp}
* // 提取球面等值面
* auto f = [](double x,double y,double z) {
* return sdf_sphere(x,y,z, 0,0,0, 1.0);
* };
* auto mesh = marching_cubes(f, 0.0,
* {-2,-2,-2}, {2,2,2}, 64);
* @endcode
* @see sdf_sphere sdf_box
* @ingroup mesh
*/
MCMesh marching_cubes(const std::function<double(double,double,double)>& f,
double iso_level,
const Point3D& bmin, const Point3D& bmax,
int resolution);
/// SDF sphere
/**
* @brief SDF 球体函数
*
* @param x,y,z 采样点坐标
* @param cx,cy,cz 球心坐标
* @param r 半径
* @return 有符号距离:内部 < 0,表面 = 0,外部 > 0
*
* @code{.cpp}
* auto sphere_sdf = [](double x,double y,double z) {
* return sdf_sphere(x,y,z, 0,0,0, 1.0);
* };
* @endcode
* @ingroup mesh
*/
inline double sdf_sphere(double x, double y, double z, double cx, double cy, double cz, double r) {
return std::sqrt((x-cx)*(x-cx) + (y-cy)*(y-cy) + (z-cz)*(z-cz)) - r;
}
/// SDF box
/**
* @brief SDF 立方体函数(轴对齐,中心在原点)
*
* @param x,y,z 采样点坐标
* @param hx,hy,hz 半边长
* @return 有符号距离
*
* @ingroup mesh
*/
inline double sdf_box(double x, double y, double z, double hx, double hy, double hz) {
double dx = std::abs(x) - hx, dy = std::abs(y) - hy, dz = std::abs(z) - hz;
return std::sqrt(std::max(dx,0.0)*std::max(dx,0.0)
@@ -35,14 +87,34 @@ inline double sdf_box(double x, double y, double z, double hx, double hy, double
+ std::min(std::max({dx,dy,dz}), 0.0);
}
/// SDF smooth union
/**
* @brief SDF 平滑并集(Smooth Union
*
* 对两个 SDF 做平滑混合,k 控制过渡区宽度。
*
* @param d1,d2 两个 SDF 值
* @param k 平滑宽度(> 0),值越大过渡越平滑
* @return 混合后的 SDF 值
*
* @ingroup mesh
*/
namespace { inline double lerp_impl(double a, double b, double t) { return a + t * (b - a); } }
inline double sdf_smooth_union(double d1, double d2, double k) {
double h = std::clamp(0.5 + 0.5*(d2-d1)/k, 0.0, 1.0);
return lerp_impl(d2, d1, h) - k*h*(1.0-h);
}
/// SDF smooth subtraction
/**
* @brief SDF 平滑差集(Smooth Subtraction
*
* d1 减去 d2,过渡区由 k 控制。
*
* @param d1,d2 两个 SDF 值
* @param k 平滑宽度(> 0
* @return 混合后的 SDF 值
*
* @ingroup mesh
*/
inline double sdf_smooth_subtraction(double d1, double d2, double k) {
double h = std::clamp(0.5 - 0.5*(d2+d1)/k, 0.0, 1.0);
return lerp_impl(d2, -d1, h) + k*h*(1.0-h);
+32 -1
View File
@@ -6,8 +6,39 @@ using core::Point2D;
using core::Point3D;
using core::Vector3D;
enum class BooleanOp { Union, Intersection, Difference, SymDiff };
/**
* @brief 布尔运算类型(CSG 组合)
* @ingroup mesh
*/
enum class BooleanOp {
Union, ///< A B 并集
Intersection, ///< A ∩ B 交集
Difference, ///< A \ B 差集
SymDiff ///< A Δ B 对称差集
};
/**
* @brief 三角网格布尔运算
*
* 对两个封闭三角网格执行 CSGConstructive Solid Geometry)布尔操作。
* 内部流程:
* 1. 计算两个网格的三角形-三角形交线
* 2. 沿交线细分三角形(conforming 三角化)
* 3. 根据布尔类型对每个子面片做内/外分类
* 4. 缝合属于结果的部分形成新网格
*
* @param a 第一个三角网格(必须为封闭流形)
* @param b 第二个三角网格(必须为封闭流形)
* @param op 布尔运算类型
* @return 结果三角网格
*
* @note 要求输入网格为封闭流形(watertight),否则分类不可靠
* @code{.cpp}
* auto result = mesh_boolean(cube, sphere, BooleanOp::Difference);
* // result = 立方体减去球体的交集部分
* @endcode
* @ingroup mesh
*/
HalfedgeMesh mesh_boolean(const HalfedgeMesh& a, const HalfedgeMesh& b, BooleanOp op);
} // namespace vde::mesh
+33 -6
View File
@@ -7,15 +7,42 @@ using core::Point2D;
using core::Point3D;
using core::Vector3D;
/**
* @brief 离散曲率计算结果
* @ingroup mesh
*/
struct CurvatureResult {
std::vector<double> gaussian; // per-vertex Gaussian curvature
std::vector<double> mean; // per-vertex mean curvature
std::vector<double> area; // per-vertex Voronoi area
std::vector<double> gaussian; ///< 每顶点高斯曲率 K = κ₁·κ₂
std::vector<double> mean; ///< 每顶点平均曲率 H = (κ₁+κ₂)/2
std::vector<double> area; ///< 每顶点 Voronoi 面积(曲率归一化权重)
};
/// Discrete curvature using Cotan formula (Meyer et al. 2003)
/// Gaussian: K(v) = (2π - Σθ_j) / A_v (interior vertices)
/// Mean: H(v) = ||Σ(cot α + cot β)·e|| / (2·A_v)
/**
* @brief 离散曲率计算(Cotan 公式,Meyer et al. 2003
*
* 对三角网格每个顶点计算离散高斯曲率和平均曲率。
*
* 高斯曲率(内点):
* K(v) = (2π - Σ_j θ_j) / A_v
* 其中 θ_j 是顶点 v 处面角的总和,A_v 是 Voronoi 面积
*
* 平均曲率:
* H(v) = || Σ_j (cot α_j + cot β_j) · e_j || / (2 · A_v)
* 其中 α_j,β_j 是与边 e_j 相对的两个角
*
* 边界顶点使用角度缺损公式修正。
*
* @param mesh 输入三角网格
* @return CurvatureResult 每顶点曲率
*
* @note 要求网格为流形;边界顶点曲率使用近似公式
* @code{.cpp}
* auto crv = compute_curvature(mesh);
* double max_curvature = *std::max_element(crv.gaussian.begin(), crv.gaussian.end());
* // 可用于特征检测、网格分割等
* @endcode
* @ingroup mesh
*/
CurvatureResult compute_curvature(const HalfedgeMesh& mesh);
} // namespace vde::mesh
+54 -4
View File
@@ -8,12 +8,62 @@ using core::Point2D;
using core::Point3D;
using core::Vector3D;
/// Tutte embedding (harmonic parameterization) for a mesh with fixed boundary
/// Boundary vertices are mapped to a circle; interior vertices are solved via Laplacian
/// Returns per-vertex UV coordinates as Point2D
/**
* @brief Tutte 参数化(调和参数化)
*
* 将边界顶点固定映射到单位圆上,内部顶点通过求解 Laplace 方程
* Δu = 0, Δv = 0 得到 UV 坐标。这是最经典、最稳定的网格参数化方法。
*
* 算法:
* 1. 检测网格边界环
* 2. 将边界顶点按弧长比例映射到单位圆上
* 3. 对每个内部顶点求解均匀 Laplace(权重 w_ij = 1):
* v_i = Σ_j v_j / deg(i)
* 4. 构造稀疏线性系统 LU 求解
*
* 优点:极稳定,保证无翻转(对凸边界网格)
* 缺点:边界固定,内部可能扭曲(非共形)
*
* @param mesh 输入三角网格(需含边界)
* @return 每顶点 UV 坐标(Point2D),顺序与 mesh 顶点一致
*
* @note 网格必须有边界(开网格);闭曲面需先切割为拓扑圆盘
* @code{.cpp}
* auto uv = tutte_parameterization(mesh);
* // uv[i] 为顶点 i 的 (u,v) 坐标,范围大致在 [-1,1]²
* @endcode
* @see lscm_parameterization
* @ingroup mesh
*/
[[nodiscard]] std::vector<Point2D> tutte_parameterization(const HalfedgeMesh& mesh);
/// LSCM (Least Squares Conformal Maps) — simplified version
/**
* @brief LSCMLeast Squares Conformal Maps)参数化
*
* 基于 Lévy et al. 2002 的最小二乘共形映射。通过最小化共形能量
* 使参数化尽可能保角(角度畸变小),同时允许边界自由。
*
* 与 Tutte 的区别:
* - 无需固定边界 → 边界自然展开,畸变更小
* - 需要固定至少 2 个顶点消除刚体自由度(内部处理)
* - 共形性更好,适合纹理映射
* - 不保证无翻转(局部极小可能翻转)
*
* 能量函数:
* E(u,v) = Σ_T || ∇(u+iv) ||² · A_T
* 对每个三角形 T 最小化梯度扰动平方和
*
* @param mesh 输入三角网格
* @return 每顶点 UV 坐标(Point2D
*
* @note 简化实现,不包含完整 Lévy 论文中所有的退化处理
* @code{.cpp}
* auto uv = lscm_parameterization(mesh);
* // 适合纹理 mapping
* @endcode
* @see tutte_parameterization
* @ingroup mesh
*/
[[nodiscard]] std::vector<Point2D> lscm_parameterization(const HalfedgeMesh& mesh);
} // namespace vde::mesh
+167 -47
View File
@@ -3,93 +3,213 @@
#include <cmath>
#include <vector>
#include <string>
#include <array>
namespace vde::mesh {
using core::Point2D;
using core::Point3D;
using core::Vector3D;
// ── Triangle quality ────────────────────────────────
// ════════════════════════════════════════════════════════
// Triangle quality
// ════════════════════════════════════════════════════════
/**
* @brief 三角网格质量指标
* @ingroup mesh
*/
struct MeshQuality {
double min_angle_deg = 0;
double max_angle_deg = 0;
double avg_aspect_ratio = 0;
double max_aspect_ratio = 0;
size_t degenerate_faces = 0;
double min_angle_deg = 0; ///< 最小三角形内角(度),≥ 0
double max_angle_deg = 0; ///< 最大三角形内角(度),≤ 180
double avg_aspect_ratio = 0; ///< 平均长宽比(外接圆半径 / 最短边)
double max_aspect_ratio = 0; ///< 最大长宽比
size_t degenerate_faces = 0; ///< 退化面数(面积 ≈ 0)
};
/**
* @brief 评估三角网格质量
*
* 对网格中每个三角面计算内角、长宽比,统计全局最值及平均值。
* 质量好的三角网格应有:
* - min_angle > 20°(CFD 模拟要求 > 25°)
* - max_angle < 120°(避免狭长退化)
* - avg_aspect_ratio 接近 1
*
* @param mesh 输入三角网格
* @return MeshQuality 质量指标汇总
*
* @code{.cpp}
* auto q = evaluate_mesh_quality(mesh);
* if (q.min_angle_deg < 15.0) {
* // 需要 remesh 或优化
* }
* @endcode
* @ingroup mesh
*/
MeshQuality evaluate_mesh_quality(const HalfedgeMesh& mesh);
// ── Tetrahedron quality ─────────────────────────────
// ════════════════════════════════════════════════════════
// Tetrahedron quality
// ════════════════════════════════════════════════════════
/**
* @brief 四面体网格质量指标
* @ingroup mesh
*/
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;
double min_jacobian = 0.0; ///< 最小归一化 Jacobian [0, 1]0=退化 1=正四面体
double max_jacobian = 0.0; ///< 最大归一化 Jacobian
double avg_jacobian = 0.0; ///< 平均归一化 Jacobian
double min_dihedral_deg = 0.0; ///< 最小二面角(度),太小会导致刚度矩阵病态
double max_dihedral_deg = 0.0; ///< 最大二面角(度),接近 180° 表示退化
double max_aspect_ratio = 0.0; ///< 最大长宽比(外接球半径 / 最短边)
size_t degenerate_tets = 0; ///< 退化四面体数(体积 ≈ 0 或 Jacobian ≈ 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).
/**
* @brief 评估三角网格作为四面体边界的质量
*
* 将每个三角面与其面重心构造虚拟四面体,评估这些四面体的质量。
* 适用于将封闭三角表面解释为体积边界时的质量检测。
*
* @param mesh 封闭三角网格(volume boundary
* @return TetQuality 四面体质量指标
*
* @note 这不是真正的四面体网格质量评估——仅用于表面网格的代理指标
* @ingroup mesh
*/
TetQuality evaluate_tet_quality(const HalfedgeMesh& mesh);
// ── Generic element quality ─────────────────────────
// ════════════════════════════════════════════════════════
// 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;
/**
* @brief 单元类型枚举
* @ingroup mesh
*/
enum class ElementType {
Tri, ///< 三角形
Quad, ///< 四边形(Stub
Tet, ///< 四面体
Hex ///< 六面体(Stub
};
/// Dispatch by element type; implemented for Tri and Tet (Quad/Hex return stub).
/**
* @brief 通用单元质量指标
* @ingroup mesh
*/
struct ElemQuality {
double min_jacobian = 0.0; ///< 最小归一化 Jacobian
double max_jacobian = 0.0; ///< 最大归一化 Jacobian
double avg_jacobian = 0.0; ///< 平均归一化 Jacobian
size_t total_elements = 0; ///< 单元总数
size_t degenerate = 0; ///< 退化单元数
};
/**
* @brief 按单元类型分派的网格质量评估
*
* @param mesh 输入网格
* @param type 单元类型
* @return ElemQuality 质量指标
*
* @note 目前仅 Tri 和 Tet 完整实现;Quad / Hex 返回 stub(全零)
* @ingroup mesh
*/
ElemQuality evaluate_element_quality(const HalfedgeMesh& mesh, ElementType type);
// ── Quality report ──────────────────────────────────
// ════════════════════════════════════════════════════════
// Quality report
// ════════════════════════════════════════════════════════
/**
* @brief 详细质量报告
* @ingroup mesh
*/
struct QualityReport {
std::string element_type; // "triangle" / "tetrahedron"
size_t total = 0;
size_t degenerate = 0;
std::string element_type; ///< "triangle" "tetrahedron"
// Binned histogram (quality ∈ [0,1])
size_t total = 0; ///< 单元总数
size_t degenerate = 0; ///< 退化单元数
/// 质量直方图(k=10 个区间,[0,0.1), [0.1,0.2), ..., [0.9,1.0]
static constexpr int kBins = 10;
std::array<size_t, kBins> histogram{};
// Grade counts (A=excellent B=good C=acceptable D=poor F=fail)
/// 分级计数(A=优秀 B=良好 C=合格 D=差 F=不合格)
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;
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.
/**
* @brief 生成三角网格的完整质量报告
*
* 包含直方图、分级统计及基本统计量。
* 分级标准(按归一化 Jacobian):
* - A: [0.9, 1.0] — 优秀
* - B: [0.7, 0.9) — 良好
* - C: [0.5, 0.7) — 合格
* - D: [0.3, 0.5) — 差
* - F: [0.0, 0.3) — 不合格
*
* @param mesh 输入三角网格
* @return QualityReport 完整质量报告
*
* @ingroup 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.
/**
* @brief 生成四面体网格的完整质量报告
*
* 将表面网格的面解释为四面体单元的边界(面 + 面心 = 虚拟四面体)。
*
* @param mesh 封闭三角网格
* @return QualityReport 完整质量报告
*
* @ingroup mesh
*/
QualityReport mesh_quality_report_tet(const HalfedgeMesh& mesh);
// ── Utility ─────────────────────────────────────────
// ════════════════════════════════════════════════════════
// Utility
// ════════════════════════════════════════════════════════
/// Compute the scaled Jacobian (determinant / (product of edge-lengths))
/// for a triangle; returns [1,1], 1 = equilateral.
/**
* @brief 三角形归一化 Jacobian
*
* 计算三角形 (a,b,c) 的带符号面积,归一化到 [-1, 1]。
* 值 1 = 等边三角形(最优),0 = 退化(共线),-1 = 翻转(法向反了)。
*
* 公式:J = det([b-a, c-a, n]) / (|b-a|·|c-a|·|b-c|? or area-based?)
* 实际实现使用面积与理想等边三角形面积之比。
*
* @param a,b,c 三角形三个顶点
* @return 归一化 Jacobian [-1, 1]
*
* @ingroup mesh
*/
double tri_scaled_jacobian(const Point3D& a, const Point3D& b, const Point3D& c);
/// Compute the scaled Jacobian for a tetrahedron; returns [0,1].
/**
* @brief 四面体归一化 Jacobian
*
* 计算四面体 (a,b,c,d) 的体积 Jacobian,归一化到 [0, 1]。
* 值 1 = 正四面体(最优),0 = 退化(共面)。
*
* 公式:J = det([b-a, c-a, d-a]) / (归一化因子)
*
* @param a,b,c,d 四面体四个顶点
* @return 归一化 Jacobian [0, 1]
*
* @ingroup mesh
*/
double tet_scaled_jacobian(const Point3D& a, const Point3D& b,
const Point3D& c, const Point3D& d);
+31 -4
View File
@@ -6,13 +6,40 @@ using core::Point2D;
using core::Point3D;
using core::Vector3D;
/**
* @brief 网格修复选项
* @ingroup mesh
*/
struct RepairOptions {
bool fill_holes = true;
bool remove_duplicates = true;
bool fix_orientation = true;
bool remove_degenerate = true;
bool fill_holes = true; ///< 是否填充孔洞(补面)
bool remove_duplicates = true; ///< 是否合并重合顶点(距离 < ε)
bool fix_orientation = true; ///< 是否统一面法向(外翻)
bool remove_degenerate = true; ///< 是否删除退化面(面积为 0 的三角形)
};
/**
* @brief 自动网格修复
*
* 对输入网格执行一系列修复操作,输出水密的流形网格:
* - fill_holes: 检测边界环并用 fan 三角化填充
* - remove_duplicates: 空间哈希合并间距 < 1e-8 的重复顶点
* - fix_orientation: 从最大连通分量开始遍历传播一致的半边朝向
* - remove_degenerate: 剔除面积 < ε 的退化三角形
*
* @param mesh 输入网格(可能有缺陷)
* @param opts 修复选项(默认全部启用)
* @return 修复后的网格
*
* @note 修复为启发式算法,不保证 100% 成功;孔洞过大或非流形边可能仍有残留问题
* @code{.cpp}
* auto fixed = repair_mesh(broken_mesh); // 默认全修复
*
* RepairOptions opts;
* opts.fill_holes = false; // 仅做去重 + 定向
* auto cleaned = repair_mesh(mesh, opts);
* @endcode
* @ingroup mesh
*/
HalfedgeMesh repair_mesh(const HalfedgeMesh& mesh, const RepairOptions& opts = {});
} // namespace vde::mesh
+32 -5
View File
@@ -6,14 +6,41 @@ using core::Point2D;
using core::Point3D;
using core::Vector3D;
/**
* @brief 网格简化选项
* @ingroup mesh
*/
struct SimplifyOptions {
double target_ratio = 0.5; // Target face ratio (0, 1)
int max_iterations = 100;
bool preserve_boundary = true;
double target_ratio = 0.5; ///< 目标面数比例 (0, 1]0.5 表示保留 50% 的面
int max_iterations = 100; ///< 最大迭代次数(边塌缩上限)
bool preserve_boundary = true; ///< 是否保护边界边不被塌缩
};
/// QEM (Quadric Error Metrics) mesh simplification
/// Returns simplified mesh with ~target_ratio * original face count
/**
* @brief QEMQuadric Error Metrics)网格简化
*
* 基于 Garland & Heckbert 1997 的 QEM 算法,通过迭代边塌缩减少三角面数量。
* 每条边维护一个 4×4 误差二次型 Q = Σ Q_f(面片的顶点-平面距离平方和),
* 塌缩目标点为 argmin v^T Q v,塌缩代价为该最小值。
*
* 算法流程:
* 1. 为每个顶点计算初始 Q(关联所有邻面的平面二次型之和)
* 2. 为每条边计算最优塌缩点及代价,插入最小堆
* 3. 循环弹出最小代价边,执行塌缩,更新受影响边的代价
* 4. 直到面数达到 target_ratio 或堆为空
*
* @param mesh 输入三角网格
* @param opts 简化选项
* @return 简化后的网格
*
* @note preserve_boundary 模式下,边界边通过惩罚项(大代价)被保护
* @code{.cpp}
* SimplifyOptions opts;
* opts.target_ratio = 0.1; // 保留 10% 面
* auto lod = simplify_mesh(high_res_mesh, opts);
* @endcode
* @ingroup mesh
*/
HalfedgeMesh simplify_mesh(const HalfedgeMesh& mesh, const SimplifyOptions& opts = {});
} // namespace vde::mesh
+118 -22
View File
@@ -6,38 +6,134 @@ using core::Point2D;
using core::Point3D;
using core::Vector3D;
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)
// 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;
/**
* @brief 网格光顺方法
* @ingroup mesh
*/
enum class SmoothMethod {
Laplacian, ///< 标准拉普拉斯光顺:v ← v + λ·L(v),快速但会收缩
Taubin, ///< Taubin λ|μ 光顺:先正后负步交替,体积保持
HCLaplacian, ///< HC 拉普拉斯(Humphrey's Classes):推拉两步保持形状/体积
Bilateral ///< 双边滤波:法向加权去噪,保持尖锐特征
};
/// Generic dispatcher — delegates to specific smooth functions
/**
* @brief 网格光顺参数
* @ingroup mesh
*/
struct SmoothOptions {
int iterations = 10; ///< 迭代次数
double lambda = 0.5; ///< 拉普拉斯步长(Standard / Taubin 正步)
/// Taubin 负步系数,典型值 mu = -(lambda + ε),默认 -0.53
/// 必须满足 mu < -lambda 以实现体积保持
double mu = -0.53;
/// HC 拉普拉斯参数
double hc_alpha = 0.0; ///< 回推强度,0 = 自动计算(推荐)
double hc_beta = 0.5; ///< 前推混合系数
/// 双边滤波参数
double bilateral_sigma_c = 0.0; ///< 空间 σ,0 = 自动按平均边长计算
double bilateral_sigma_n = 0.3; ///< 法向 σ(弧度),控制特征保持的敏感度
int bilateral_iters = 5; ///< 双边内部迭代次数
SmoothMethod method = SmoothMethod::Laplacian; ///< 光顺方法选择
};
/**
* @brief 网格光顺(通用分发器)
*
* 根据 opts.method 分派到具体的光顺实现:
* - Laplacian → smooth_laplacian()
* - Taubin → smooth_taubin()
* - HCLaplacian → smooth_hc_laplacian()
* - Bilateral → smooth_bilateral()
*
* @param mesh 输入三角网格
* @param opts 光顺选项
* @return 光顺后的网格(顶点位置更新,拓扑不变)
*
* @code{.cpp}
* SmoothOptions opts;
* opts.method = SmoothMethod::Taubin;
* opts.iterations = 20;
* auto smooth = smooth_mesh(noisy_mesh, opts);
* @endcode
* @ingroup mesh
*/
HalfedgeMesh smooth_mesh(const HalfedgeMesh& mesh, const SmoothOptions& opts = {});
/// Standard Laplacian smoothing
/**
* @brief 标准拉普拉斯光顺
*
* 均匀拉普拉斯:v ← v + λ·(avg(neighbor_positions) - v)
* 最简单但会引入体积收缩,适用于轻度平滑。
*
* @param mesh 输入网格
* @param iterations 迭代次数
* @param lambda 步长系数 (0, 1]
* @return 光顺后的网格
*
* @note 迭代过多会导致网格坍塌;对于体积关键的应用优先选 Taubin 或 HC
* @see smooth_taubin smooth_hc_laplacian
* @ingroup mesh
*/
HalfedgeMesh smooth_laplacian(const HalfedgeMesh& mesh, int iterations, double lambda);
/// Taubin λ|μ smoothing (volume-preserving)
/**
* @brief Taubin λ|μ 光顺(体积保持)
*
* 两阶段交替:
* 1. 正步(收缩滤波):v ← v + λ·L(v)
* 2. 负步(膨胀滤波):v ← v + μ·L(v),其中 μ < 0 且 |μ| > |λ|
*
* 传递函数在低频通带增益 ≈ 1,有效去噪同时保持体积。
*
* @param mesh 输入网格
* @param iterations 完整 λ/μ 循环次数
* @param lambda 正步系数 (0, 1]
* @param mu 负步系数,需满足 mu < 0 且 |mu| > lambda
* @return 光顺后的网格
*
* @note 典型值:λ = 0.5, μ = -0.53;|mu| 过大可能导致不稳定振荡
* @ingroup mesh
*/
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
/**
* @brief HC 拉普拉斯光顺(Humphrey's Classes
*
* 两遍处理以保持原始形状特征:
* 1. 前推:标准拉普拉斯光顺,记录位移 b_i = v_i' - v_i
* 2. 回推:v_i'' = v_i' - (α·b_i + β·avg(neighbor_b))
*
* 相比标准拉普拉斯显著减轻体积收缩,同时比 Taubin 更好地保持尖锐特征。
*
* @param mesh 输入网格
* @param opts 光顺选项(使用 hc_alpha 和 hc_beta 字段)
* @return 光顺后的网格
*
* @ingroup mesh
*/
HalfedgeMesh smooth_hc_laplacian(const HalfedgeMesh& mesh, const SmoothOptions& opts = {});
/// Bilateral mesh filtering — normal-weighted, preserves sharp features
/**
* @brief 双边网格滤波
*
* 基于法向加权的各向异性去噪,保持尖锐边缘和特征:
* - 空间权重:w_c = exp(-d²/(2·σ_c²)),基于顶点间距
* - 法向权重:w_n = exp(-θ²/(2·σ_n²)),基于法向夹角
* - 更新:v ← v + Σ_j w_c(j)·w_n(j)·(v_j - v) / Σ_j w_c(j)·w_n(j)
*
* 相比于各向同性的拉普拉斯类方法,双边滤波在 CAD/机械零件网格上效果最优。
*
* @param mesh 输入网格
* @param opts 光顺选项(使用 bilateral_ 前缀字段)
* @return 光顺后的网格
*
* @ingroup mesh
*/
HalfedgeMesh smooth_bilateral(const HalfedgeMesh& mesh, const SmoothOptions& opts = {});
} // namespace vde::mesh