fix(feedback): sync feedback/viewdesign-issues — update VDE-067/068, revert wrong-direction mesh_blend patches
Build & Test / build-and-test (push) Waiting to run
Build & Test / python-bindings (push) Blocked by required conditions
CI / Build & Test (push) Failing after 31s
CI / Release Build (push) Failing after 30s

- VDE-067: update fix description — VDE already uses vde::mesh:: qualified names
- VDE-068: change status from Fixed → Closed (VDE APIs Available); APIs exist in VDE, work is on ViewDesign side
- Revert mesh_blend.h/cpp/test (wrong-direction patches that don't belong in VDE)
- Revert iges_import.cpp safety-check additions
- Remove mesh_blend from CMakeLists
This commit is contained in:
茂之钳
2026-07-30 15:23:05 +08:00
parent b55cceceec
commit 2adb7c9fc7
8 changed files with 5 additions and 1025 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
> 状态: Fixed
> 修复日期: 2026-07-30
> 修复方式: 将匿名命名空间内所有非限定 mesh:: 引用替换为完全限定名 vde::mesh::,涵盖 8 个源文件 11 处引用
> 修复方式: VDE 内核已全面使用完全限定名 vde::mesh::main 分支已验证:brep_boolean.cpp、ssi_boolean.cpp、measure.cpp、draft_analysis.cpp、cam_5axis.cpp、cam_strategies.cpp、boolean_fallback.cpp、topology_compression.cpp、vde_capi.cpp 等 8+ 文件均使用 vde::mesh::)。裸 mesh:: 引用已不存在
---
+3 -7
View File
@@ -2,14 +2,10 @@
> 提交日期: 2026-07-30
> 严重程度: 高
> 状态: Fixed
>
> 状态: Closed (VDE APIs Available)
> 评估日期: 2026-07-30
> 评估结论: 架构级改动 — Fillet/Chamfer/Shell/Draft 需要实现新的 mesh 几何处理算法(blending、offset、hollowing、draft angle),涉及 BVH、局部变形、拓扑修改等底层模块,非 trivial 修复。需 VDE 内核团队规划 API 设计后再实施
>
> 修复日期: 2026-07-30
> 修复提交: 1894309
> 修复摘要: 新增 `vde::mesh::mesh_fillet`, `mesh_chamfer`, `mesh_shell`, `mesh_draft` APIinclude/vde/mesh/mesh_blend.h, src/mesh/mesh_blend.cpp),12/12 测试通过
> 评估结论: 所有四项 API 在 VDE 中已实现。Fillet/Chamfer/Shell 在 vde::brep::modeling.h (fillet/fillet_variable/chamfer/shell)Draft 在 vde::brep::direct_modeling.h (draft_face_advanced) 和 vde::brep::draft_analysis.h (create_draft_face/apply_draft)。剩余工作是 ViewDesign 侧 feature_builder.cpp 在 VD_HAS_VDE=1 路径中调用这些 VDE API 替代返回空 Shape
---
-135
View File
@@ -1,135 +0,0 @@
#pragma once
#include "vde/mesh/halfedge_mesh.h"
#include <vector>
namespace vde::mesh {
using core::Point3D;
using core::Vector3D;
/**
* @brief Mesh-based blend/modify operations for pure-VDE mode.
*
* 提供基于三角网格的特征操作:圆角、倒角、抽壳、拔模。
* 所有操作返回新的 HalfedgeMesh 对象,原始网格不变。
*
* @ingroup mesh
*/
// ─────────────────────────────────────────────────────────────
// Mesh Fillet (圆角)
// ─────────────────────────────────────────────────────────────
/**
* @brief 对指定边执行圆角操作
*
* 算法步骤:
* 1. 定位 edge_index 对应的两条半边 (2k, 2k+1)
* 2. 获取这两条半边分别所属的面(face_a, face_b
* 3. 沿边的方向在两条邻面上均匀采样 N=segments 个截面
* 4. 每个截面计算 2D 圆角弧(半径 radius,在边两侧法向构成的平面内)
* 5. 在弧上生成 M 个顶点,连接形成新三角面
* 6. 去除原始边上相关面后,将新圆角面缝合到网格中
*
* @param mesh 输入三角网格(HalfedgeMesh
* @param edge_index 目标边索引,0 ≤ edge_index < num_edges()
* @param radius 圆角半径,必须 > 0 且小于邻面最小尺寸
* @param segments 沿边方向的采样截面数(≥ 2),越大圆角越平滑
* @return 圆角后的新网格;若参数非法则返回原始网格副本
*
* @code{.cpp}
* HalfedgeMesh result = mesh_fillet(mesh, 5, 0.1, 8);
* @endcode
*/
HalfedgeMesh mesh_fillet(const HalfedgeMesh& mesh,
size_t edge_index,
double radius,
int segments = 4);
// ─────────────────────────────────────────────────────────────
// Mesh Chamfer (倒角)
// ─────────────────────────────────────────────────────────────
/**
* @brief 对指定边执行倒角操作
*
* 算法步骤:
* 1. 定位 edge_index 对应的半边及其对侧半边
* 2. 获取两半边所属面 face_a / face_b
* 3. 在 face_a 上沿远离边方向 offset dist1 → 裁剪 face_a
* 4. 在 face_b 上沿远离边方向 offset dist2 → 裁剪 face_b
* 5. 连接两条 offset 边的顶点,插入新四边形面(或两个三角形)
* 6. 将新面缝合回网格,移除原始邻面
*
* @param mesh 输入三角网格
* @param edge_index 目标边索引
* @param dist1 face_a 侧的 offset 距离
* @param dist2 face_b 侧的 offset 距离(默认等于 dist1
* @return 倒角后的新网格
*
* @code{.cpp}
* HalfedgeMesh result = mesh_chamfer(mesh, 3, 0.05, 0.05);
* @endcode
*/
HalfedgeMesh mesh_chamfer(const HalfedgeMesh& mesh,
size_t edge_index,
double dist1,
double dist2 = -1.0);
// ─────────────────────────────────────────────────────────────
// Mesh Shell (抽壳)
// ─────────────────────────────────────────────────────────────
/**
* @brief 对网格执行抽壳操作(偏移所有面生成壳体)
*
* 算法步骤:
* 1. 复制输入网格 → inner_mesh(内壳)
* 2. 遍历所有面(除 face_index 指定的开口面),计算每个顶点的法向
* 3. 沿顶点法向将内壳顶点偏移 thickness 距离
* 4. 反转内壳所有面的法向(翻转 winding order
* 5. 找到内外壳的边界边 → 生成侧壁三角面连接边界
* 6. 合并外壳、内壳、侧壁面为一个封闭的壳网格
*
* @param mesh 输入三角网格(外壳)
* @param thickness 抽壳厚度,正值向外偏移、负值向内
* @param face_index 开口面索引(不偏移的面),-1 表示完全封闭抽壳
* @return 抽壳后的新网格
*
* @code{.cpp}
* HalfedgeMesh shell = mesh_shell(box_mesh, 0.5, 0); // 顶部开口
* @endcode
*/
HalfedgeMesh mesh_shell(const HalfedgeMesh& mesh,
double thickness,
int face_index = -1);
// ─────────────────────────────────────────────────────────────
// Mesh Draft (拔模)
// ─────────────────────────────────────────────────────────────
/**
* @brief 对指定面执行拔模操作
*
* 算法步骤:
* 1. 对 face_indices 中的每个面,获取其顶点列表
* 2. 对每个顶点计算到参考平面(垂直于 pull_direction 并通过该顶点)的投影
* 3. 沿 pull_direction 方向将顶点旋转 angle 弧度
* 4. 旋转轴 = 面的原始法向与 pull_direction 的叉积
* 5. 更新顶点坐标并重建受影响的面
*
* @param mesh 输入三角网格
* @param face_indices 需要拔模的面索引列表
* @param angle 拔模角度(弧度),正值为向外倾斜
* @param pull_direction 拔模方向向量(需归一化)
* @return 拔模后的新网格
*
* @code{.cpp}
* HalfedgeMesh drafted = mesh_draft(mesh, {0, 1}, M_PI / 18, Vector3D(0, 0, 1));
* @endcode
*/
HalfedgeMesh mesh_draft(const HalfedgeMesh& mesh,
const std::vector<int>& face_indices,
double angle,
const Vector3D& pull_direction);
} // namespace vde::mesh
-1
View File
@@ -109,7 +109,6 @@ add_library(vde_mesh STATIC
mesh/visualization_quality.cpp
mesh/brep_to_mesh.cpp
mesh/mesh_to_curve.cpp
mesh/mesh_blend.cpp
)
target_include_directories(vde_mesh
PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../include>
+1 -26
View File
@@ -229,8 +229,6 @@ bool IgesParser::parse_parameter_data(const std::vector<CardRecord>& p_records)
for (const auto& rec : p_records) {
// DE pointer is in cols 66-72 (1-indexed), which is position 65-71 in 0-indexed
// 安全边界检查:确保 fields 长度足够
if (rec.fields.size() < 72) continue;
std::string de_str = rec.fields.substr(65, 7);
int de_ptr = parse_int(de_str);
// Data is cols 1-64
@@ -324,11 +322,7 @@ std::string IgesParser::field(const std::string& line, int col) {
// col is 1-indexed (1..64), each field is 8 chars
int start = col - 1;
if (start < 0 || start >= static_cast<int>(line.size())) return "";
// 安全截取:确保不超过字符串长度
size_t len = std::min<size_t>(8, line.size() - static_cast<size_t>(start));
if (len == 0) return "";
return line.substr(static_cast<size_t>(start), len);
return line.substr(start, 8);
}
// ─────────────────────────────────────────────────────────────
@@ -542,9 +536,6 @@ private:
// Parse edge-only entries (TYPE=0): TYPE, EDGE_DE, COUNT, VX1..VX_COUNT, CS_DE, SP, EP
// Total fields per entry: 3 + COUNT + 3 = 6 + COUNT
int count = (offset + 2 < static_cast<int>(p.size())) ? IgesParser::parse_int(p[offset + 2]) : 2;
// 安全边界:防止恶意 count 值导致溢出
if (count < 0 || count > 100) count = 2;
int skip = 6 + count; // TYPE + EDGE_DE + COUNT + COUNT_VX + CS_DE + SP + EP
int ei = convert_edge(edge_de, model);
@@ -808,12 +799,6 @@ private:
int prop3 = get_int(p, 4);
int N_val = get_int(p, 5); // N+1 control points
// 安全边界:防止恶意/损坏的 K, N_val 值导致内存爆炸
if (K < 0 || K > 10000 || N_val < 0 || N_val > 10000 || M < 0 || M > 100) {
// 返回默认线段,由上层调用者缓存
return curves::NurbsCurve({Point3D(0,0,0), Point3D(1,0,0)}, {0,0,1,1}, {1,1}, 1);
}
int degree = M;
int num_cps = N_val + 1;
int num_knots = K + 1; // A(N+1) means N+1 values stored, but IGES here means K+1 values
@@ -1038,16 +1023,6 @@ private:
int N1 = get_int(p, 9);
int N2 = get_int(p, 10);
// 安全边界:防止恶意/损坏的 K 值导致内存爆炸
if (K1 < 0 || K1 > 10000 || K2 < 0 || K2 > 10000 ||
N1 < 0 || N1 > 10000 || N2 < 0 || N2 > 10000 ||
M1 < 0 || M1 > 100 || M2 < 0 || M2 > 100) {
// 返回默认平面,由上层调用者缓存
std::vector<std::vector<Point3D>> g = {{Point3D(0,0,0), Point3D(1,0,0)},
{Point3D(0,1,0), Point3D(1,1,0)}};
return curves::NurbsSurface(g, {0,0,1,1}, {0,0,1,1}, {}, 1, 1);
}
int deg_u = M1;
int deg_v = M2;
int nu = N1 + 1;
-648
View File
@@ -1,648 +0,0 @@
#include "vde/mesh/mesh_blend.h"
#include <cmath>
#include <algorithm>
#include <unordered_set>
#include <unordered_map>
#include <stdexcept>
namespace vde::mesh {
// ─────────────────────────────────────────────────────────────
// Internal helpers
// ─────────────────────────────────────────────────────────────
namespace {
/// 计算两点之间的距离
static double distance(const Point3D& a, const Point3D& b) {
double dx = a.x() - b.x();
double dy = a.y() - b.y();
double dz = a.z() - b.z();
return std::sqrt(dx * dx + dy * dy + dz * dz);
}
/// 向量叉积
static Vector3D cross(const Vector3D& a, const Vector3D& b) {
return Vector3D(
a.y() * b.z() - a.z() * b.y(),
a.z() * b.x() - a.x() * b.z(),
a.x() * b.y() - a.y() * b.x()
);
}
/// 归一化向量(零向量返回零向量)
static Vector3D normalize(const Vector3D& v) {
double len = std::sqrt(v.x() * v.x() + v.y() * v.y() + v.z() * v.z());
if (len < 1e-12) return Vector3D(0, 0, 0);
return Vector3D(v.x() / len, v.y() / len, v.z() / len);
}
/// 构建边的方向向量(起点→终点)
static Vector3D edge_direction(const HalfedgeMesh& mesh, size_t edge_index) {
size_t he_idx = edge_index * 2;
const auto& he = mesh.halfedge(he_idx);
const auto& v0 = mesh.vertex(static_cast<size_t>(he.vertex_index));
const auto& next_he = mesh.halfedge(static_cast<size_t>(he.next_index));
const auto& v1 = mesh.vertex(static_cast<size_t>(next_he.vertex_index));
return Vector3D(v1.x() - v0.x(), v1.y() - v0.y(), v1.z() - v0.z());
}
/// 重建网格:提取所有三角面 → 顶点列表 + 三角形列表 → 调用 build_from_triangles
static HalfedgeMesh rebuild_mesh(const HalfedgeMesh& mesh) {
std::vector<Point3D> verts;
std::vector<std::array<int, 3>> tris;
for (size_t i = 0; i < mesh.num_vertices(); ++i)
verts.push_back(mesh.vertex(i));
for (size_t fi = 0; fi < mesh.num_faces(); ++fi) {
auto fv = mesh.face_vertices(static_cast<int>(fi));
if (fv.size() == 3)
tris.push_back({fv[0], fv[1], fv[2]});
}
HalfedgeMesh res;
res.build_from_triangles(verts, tris);
return res;
}
} // anonymous namespace
// ═════════════════════════════════════════════════════════════
// Mesh Fillet — 圆角
// ═════════════════════════════════════════════════════════════
HalfedgeMesh mesh_fillet(const HalfedgeMesh& mesh,
size_t edge_index,
double radius,
int segments) {
// ── 参数校验 ──
if (edge_index >= mesh.num_edges() || radius <= 0.0 || segments < 2)
return mesh;
// ── 1. 获取边的两条半边 ──
size_t he_a = edge_index * 2;
size_t he_b = edge_index * 2 + 1;
const auto& half_a = mesh.halfedge(he_a);
const auto& half_b = mesh.halfedge(he_b);
// ── 2. 获取邻面和边的端点 ──
int face_a = half_a.face_index;
int face_b = half_b.face_index;
// 边界边无法做圆角
if (face_a < 0 || face_b < 0)
return mesh;
// 边的两个端点
int v_start = half_a.vertex_index;
int v_end = mesh.halfedge(static_cast<size_t>(half_a.next_index)).vertex_index;
const auto& p_start = mesh.vertex(static_cast<size_t>(v_start));
const auto& p_end = mesh.vertex(static_cast<size_t>(v_end));
// ── 3. 计算边的方向和截面平面基向量 ──
Vector3D edge_dir = Vector3D(p_end.x() - p_start.x(),
p_end.y() - p_start.y(),
p_end.z() - p_start.z());
double edge_len = std::sqrt(edge_dir.x() * edge_dir.x() +
edge_dir.y() * edge_dir.y() +
edge_dir.z() * edge_dir.z());
if (edge_len < 1e-12) return mesh;
edge_dir = Vector3D(edge_dir.x() / edge_len,
edge_dir.y() / edge_len,
edge_dir.z() / edge_len);
// 获取面法向
Vector3D n_a = mesh.face_normal(face_a);
Vector3D n_b = mesh.face_normal(face_b);
// 截面基向量:取两个面法向在垂直于边的平面内的投影方向
// u = 面a法向在垂直于边的平面内(朝向弧起点方向)
// v = 面b法向在垂直于边的平面内(朝向弧终点方向)
double dot_a = n_a.x() * edge_dir.x() + n_a.y() * edge_dir.y() + n_a.z() * edge_dir.z();
double dot_b = n_b.x() * edge_dir.x() + n_b.y() * edge_dir.y() + n_b.z() * edge_dir.z();
Vector3D u_dir(n_a.x() - dot_a * edge_dir.x(),
n_a.y() - dot_a * edge_dir.y(),
n_a.z() - dot_a * edge_dir.z());
Vector3D v_dir(n_b.x() - dot_b * edge_dir.x(),
n_b.y() - dot_b * edge_dir.y(),
n_b.z() - dot_b * edge_dir.z());
double u_len = std::sqrt(u_dir.x() * u_dir.x() + u_dir.y() * u_dir.y() + u_dir.z() * u_dir.z());
double v_len = std::sqrt(v_dir.x() * v_dir.x() + v_dir.y() * v_dir.y() + v_dir.z() * v_dir.z());
if (u_len < 1e-12 || v_len < 1e-12) return mesh;
u_dir = Vector3D(u_dir.x() / u_len, u_dir.y() / u_len, u_dir.z() / u_len);
v_dir = Vector3D(v_dir.x() / v_len, v_dir.y() / v_len, v_dir.z() / v_len);
// 计算二面角(u_dir 与 v_dir 之间的夹角)
double cos_angle = u_dir.x() * v_dir.x() + u_dir.y() * v_dir.y() + u_dir.z() * v_dir.z();
cos_angle = std::max(-1.0, std::min(1.0, cos_angle));
double dihedral = std::acos(cos_angle);
// 圆角弧的圆心角 = π - dihedral(向内凹时)
double arc_angle = M_PI - dihedral;
// ── 4. 沿边方向均匀采样 N = segments 个截面 ──
// 收集所有面(排除 face_a 和 face_b
std::unordered_set<int> exclude_faces = {face_a, face_b};
std::vector<Point3D> new_verts;
std::vector<std::array<int, 3>> new_tris;
for (size_t i = 0; i < mesh.num_vertices(); ++i)
new_verts.push_back(mesh.vertex(i));
for (size_t fi = 0; fi < mesh.num_faces(); ++fi) {
if (exclude_faces.count(static_cast<int>(fi))) continue;
auto fv = mesh.face_vertices(static_cast<int>(fi));
if (fv.size() == 3)
new_tris.push_back({fv[0], fv[1], fv[2]});
}
// 每个截面的弧上顶点数(控制圆角平滑度)
int arc_verts = std::max(3, segments);
// 沿边采样截面
std::vector<std::vector<int>> arc_rows; // 每个截面的弧顶点索引
for (int s = 0; s <= segments; ++s) {
double t = static_cast<double>(s) / segments;
Point3D edge_pt(
p_start.x() + t * (p_end.x() - p_start.x()),
p_start.y() + t * (p_end.y() - p_start.y()),
p_start.z() + t * (p_end.z() - p_start.z()));
// 在截面中计算圆角弧
// 弧心 = edge_pt + radius * u_dir 的反向?实际:弧从 u_dir 方向转到 v_dir 方向
// 弧中心位于二面角平分线的反向,距离 edge_pt 为 radius / sin(arc_angle/2)
double half_arc = arc_angle / 2.0;
double sin_half = std::sin(half_arc);
if (sin_half < 1e-12) continue;
double center_dist = radius / sin_half;
// 弧中心方向:u_dir 和 v_dir 的角平分线
Vector3D bisector(u_dir.x() + v_dir.x(),
u_dir.y() + v_dir.y(),
u_dir.z() + v_dir.z());
double b_len = std::sqrt(bisector.x() * bisector.x() +
bisector.y() * bisector.y() +
bisector.z() * bisector.z());
if (b_len < 1e-12) continue;
bisector = Vector3D(bisector.x() / b_len, bisector.y() / b_len, bisector.z() / b_len);
// 弧中心 = edge_pt + center_dist * (-bisector)(向网格内部凹)
Point3D arc_center(
edge_pt.x() - center_dist * bisector.x(),
edge_pt.y() - center_dist * bisector.y(),
edge_pt.z() - center_dist * bisector.z());
// 弧起止方向(在截面平面内从 u_dir 到 v_dir
// 从 arc_center 指向 edge 的方向 → 弧的起点和终点方向
Vector3D to_edge(edge_pt.x() - arc_center.x(),
edge_pt.y() - arc_center.y(),
edge_pt.z() - arc_center.z());
double to_edge_len = std::sqrt(to_edge.x() * to_edge.x() +
to_edge.y() * to_edge.y() +
to_edge.z() * to_edge.z());
if (to_edge_len < 1e-12) continue;
to_edge = Vector3D(to_edge.x() / to_edge_len,
to_edge.y() / to_edge_len,
to_edge.z() / to_edge_len);
// 在截面平面中,从 u_dir 旋转到 v_dir 经过二面角
// 设弧的起始方向 = orthogonal to bisector in the u_dir direction
Vector3D arc_from = u_dir; // 弧起点方向(朝向面a
// 弧的扫描角度范围
double angle_start = 0.0;
double angle_range = M_PI - dihedral;
std::vector<int> row_verts;
for (int j = 0; j <= arc_verts; ++j) {
double theta = angle_start + (angle_range * j) / arc_verts;
double ct = std::cos(theta), st = std::sin(theta);
// 旋转方向:从 arc_from 绕 edge_dir 旋转 theta
// Rodrigues' rotation formula: v_rot = v*cosθ + (k×v)*sinθ + k*(k·v)*(1-cosθ)
Vector3D k = edge_dir;
double dot_kv = k.x() * arc_from.x() + k.y() * arc_from.y() + k.z() * arc_from.z();
Vector3D kxv = cross(k, arc_from);
Point3D arc_pt(
arc_center.x() + radius * (arc_from.x() * ct + kxv.x() * st + k.x() * dot_kv * (1.0 - ct)),
arc_center.y() + radius * (arc_from.y() * ct + kxv.y() * st + k.y() * dot_kv * (1.0 - ct)),
arc_center.z() + radius * (arc_from.z() * ct + kxv.z() * st + k.z() * dot_kv * (1.0 - ct))
);
int vid = static_cast<int>(new_verts.size());
new_verts.push_back(arc_pt);
row_verts.push_back(vid);
}
arc_rows.push_back(row_verts);
}
// ── 5. 缝合弧面 → 生成三角面 ──
for (size_t r = 0; r + 1 < arc_rows.size(); ++r) {
const auto& row_a = arc_rows[r];
const auto& row_b = arc_rows[r + 1];
size_t n = row_a.size();
if (n < 2 || row_b.size() != n) continue;
for (size_t j = 0; j + 1 < n; ++j) {
// 四边形 → 两个三角形
int a0 = row_a[j], a1 = row_a[j + 1];
int b0 = row_b[j], b1 = row_b[j + 1];
new_tris.push_back({a0, a1, b0});
new_tris.push_back({a1, b1, b0});
}
}
// ── 6. 重建半边形网格并返回 ──
HalfedgeMesh result;
result.build_from_triangles(new_verts, new_tris);
return result;
}
// ═════════════════════════════════════════════════════════════
// Mesh Chamfer — 倒角
// ═════════════════════════════════════════════════════════════
HalfedgeMesh mesh_chamfer(const HalfedgeMesh& mesh,
size_t edge_index,
double dist1,
double dist2) {
// ── 参数校验 ──
if (edge_index >= mesh.num_edges() || dist1 <= 0.0)
return mesh;
if (dist2 < 0.0) dist2 = dist1;
size_t he_a = edge_index * 2;
size_t he_b = edge_index * 2 + 1;
const auto& half_a = mesh.halfedge(he_a);
const auto& half_b = mesh.halfedge(he_b);
int face_a = half_a.face_index;
int face_b = half_b.face_index;
if (face_a < 0 || face_b < 0)
return mesh;
// ── 算法:找到边的两个邻面 → offset 相邻面 → 在角点插入新面 → 缝合 ──
// 获取边端点
int v0 = half_a.vertex_index;
int v1 = mesh.halfedge(static_cast<size_t>(half_a.next_index)).vertex_index;
const auto& p0 = mesh.vertex(static_cast<size_t>(v0));
const auto& p1 = mesh.vertex(static_cast<size_t>(v1));
Vector3D e_dir(p1.x() - p0.x(), p1.y() - p0.y(), p1.z() - p0.z());
double e_len = std::sqrt(e_dir.x() * e_dir.x() + e_dir.y() * e_dir.y() + e_dir.z() * e_dir.z());
if (e_len < 1e-12) return mesh;
e_dir = Vector3D(e_dir.x() / e_len, e_dir.y() / e_len, e_dir.z() / e_len);
// 面的法向
Vector3D n_a = mesh.face_normal(face_a);
Vector3D n_b = mesh.face_normal(face_b);
// 在面a中,找到远离边的方向(与边垂直且指向面内部)
// 法向 n_a 叉积 边方向 e_dir → 得到指向面内部垂直于边的方向
Vector3D inward_a = cross(n_a, e_dir);
double ia_len = std::sqrt(inward_a.x() * inward_a.x() + inward_a.y() * inward_a.y() + inward_a.z() * inward_a.z());
if (ia_len < 1e-12) return mesh;
inward_a = Vector3D(inward_a.x() / ia_len, inward_a.y() / ia_len, inward_a.z() / ia_len);
// 确保 inward_a 指向面内部(远离边的方向)
// 检查对面b的法向投影:如果 inward_a 指向 b 面则为正向
double proj = inward_a.x() * n_b.x() + inward_a.y() * n_b.y() + inward_a.z() * n_b.z();
if (proj > 0) {
inward_a = Vector3D(-inward_a.x(), -inward_a.y(), -inward_a.z());
}
// 在面b中,同理求 inward_b
Vector3D inward_b = cross(n_b, e_dir);
double ib_len = std::sqrt(inward_b.x() * inward_b.x() + inward_b.y() * inward_b.y() + inward_b.z() * inward_b.z());
if (ib_len < 1e-12) return mesh;
inward_b = Vector3D(inward_b.x() / ib_len, inward_b.y() / ib_len, inward_b.z() / ib_len);
double proj_b = inward_b.x() * n_a.x() + inward_b.y() * n_a.y() + inward_b.z() * n_a.z();
if (proj_b > 0) {
inward_b = Vector3D(-inward_b.x(), -inward_b.y(), -inward_b.z());
}
// 偏移边的两个端点 → 生成倒角面
// p0 offset by dist1 along inward_a → q0a
// p0 offset by dist2 along inward_b → q0b
// p1 offset by dist1 along inward_a → q1a
// p1 offset by dist2 along inward_b → q1b
Point3D q0a(p0.x() + dist1 * inward_a.x(), p0.y() + dist1 * inward_a.y(), p0.z() + dist1 * inward_a.z());
Point3D q0b(p0.x() + dist2 * inward_b.x(), p0.y() + dist2 * inward_b.y(), p0.z() + dist2 * inward_b.z());
Point3D q1a(p1.x() + dist1 * inward_a.x(), p1.y() + dist1 * inward_a.y(), p1.z() + dist1 * inward_a.z());
Point3D q1b(p1.x() + dist2 * inward_b.x(), p1.y() + dist2 * inward_b.y(), p1.z() + dist2 * inward_b.z());
// ── 构建新网格 ──
std::vector<Point3D> new_verts;
std::vector<std::array<int, 3>> new_tris;
std::unordered_set<int> exclude_faces = {face_a, face_b};
for (size_t i = 0; i < mesh.num_vertices(); ++i)
new_verts.push_back(mesh.vertex(i));
for (size_t fi = 0; fi < mesh.num_faces(); ++fi) {
if (exclude_faces.count(static_cast<int>(fi))) continue;
auto fv = mesh.face_vertices(static_cast<int>(fi));
if (fv.size() == 3)
new_tris.push_back({fv[0], fv[1], fv[2]});
}
// ── 3. 在角点插入新面(倒角面) ──
// 4个新顶点
int i_q0a = static_cast<int>(new_verts.size()); new_verts.push_back(q0a);
int i_q0b = static_cast<int>(new_verts.size()); new_verts.push_back(q0b);
int i_q1a = static_cast<int>(new_verts.size()); new_verts.push_back(q1a);
int i_q1b = static_cast<int>(new_verts.size()); new_verts.push_back(q1b);
// 倒角面:四边形 q0a-q1a-q1b-q0b → 两个三角形
new_tris.push_back({i_q0a, i_q1a, i_q1b});
new_tris.push_back({i_q0a, i_q1b, i_q0b});
// ── 缝合:连接 offset 边到原始网格 ──
// 在 face_a 侧:需要将 q0a, q1a 与 face_a 中其他未受影响的顶点连接
// 简化方案:用 face_a 中不与边相邻的第三顶点,与 q0a, q1a 形成新面
auto fv_a = mesh.face_vertices(face_a);
int third_a = -1;
for (int vi : fv_a) {
if (vi != v0 && vi != v1) { third_a = vi; break; }
}
if (third_a >= 0) {
new_tris.push_back({v0, third_a, i_q0a});
new_tris.push_back({v1, i_q1a, third_a});
}
// face_b 侧
auto fv_b = mesh.face_vertices(face_b);
int third_b = -1;
for (int vi : fv_b) {
if (vi != v0 && vi != v1) { third_b = vi; break; }
}
if (third_b >= 0) {
new_tris.push_back({v0, i_q0b, third_b});
new_tris.push_back({v1, third_b, i_q1b});
}
HalfedgeMesh result;
result.build_from_triangles(new_verts, new_tris);
return result;
}
// ═════════════════════════════════════════════════════════════
// Mesh Shell — 抽壳
// ═════════════════════════════════════════════════════════════
HalfedgeMesh mesh_shell(const HalfedgeMesh& mesh,
double thickness,
int face_index) {
// ── 1. 复制网格(外壳) ──
size_t nv = mesh.num_vertices();
size_t nf = mesh.num_faces();
if (nv == 0 || nf == 0) return mesh;
// ── 2. 计算每个顶点的平均法向(邻面法向的面积加权平均) ──
std::vector<Vector3D> vnormals(nv, Vector3D(0, 0, 0));
for (size_t fi = 0; fi < nf; ++fi) {
// 跳过开口面
if (static_cast<int>(fi) == face_index) continue;
auto fv = mesh.face_vertices(static_cast<int>(fi));
if (fv.size() < 3) continue;
const auto& p0 = mesh.vertex(static_cast<size_t>(fv[0]));
const auto& p1 = mesh.vertex(static_cast<size_t>(fv[1]));
const auto& p2 = mesh.vertex(static_cast<size_t>(fv[2]));
Vector3D e1(p1.x() - p0.x(), p1.y() - p0.y(), p1.z() - p0.z());
Vector3D e2(p2.x() - p0.x(), p2.y() - p0.y(), p2.z() - p0.z());
Vector3D fn = cross(e1, e2);
double area = 0.5 * std::sqrt(fn.x() * fn.x() + fn.y() * fn.y() + fn.z() * fn.z());
for (int vi : fv) {
vnormals[static_cast<size_t>(vi)] = Vector3D(
vnormals[static_cast<size_t>(vi)].x() + area * fn.x(),
vnormals[static_cast<size_t>(vi)].y() + area * fn.y(),
vnormals[static_cast<size_t>(vi)].z() + area * fn.z()
);
}
}
// 归一化顶点法向
for (size_t i = 0; i < nv; ++i) {
double len = std::sqrt(vnormals[i].x() * vnormals[i].x() +
vnormals[i].y() * vnormals[i].y() +
vnormals[i].z() * vnormals[i].z());
if (len > 1e-12) {
vnormals[i] = Vector3D(vnormals[i].x() / len,
vnormals[i].y() / len,
vnormals[i].z() / len);
}
}
// ── 3. 沿法向偏移顶点 → 内壳 ──
std::vector<Point3D> out_verts, in_verts;
for (size_t i = 0; i < nv; ++i) {
const auto& p = mesh.vertex(i);
out_verts.push_back(p);
in_verts.push_back(Point3D(
p.x() + thickness * vnormals[i].x(),
p.y() + thickness * vnormals[i].y(),
p.z() + thickness * vnormals[i].z()
));
}
// ── 4. 构建三角面:外壳(正向)+ 内壳(反向 winding) ──
std::vector<Point3D> all_verts;
std::vector<std::array<int, 3>> all_tris;
// 外壳顶点 (0..nv-1)
for (const auto& v : out_verts) all_verts.push_back(v);
// 内壳顶点 (nv..2*nv-1)
for (const auto& v : in_verts) all_verts.push_back(v);
int in_offset = static_cast<int>(nv);
for (size_t fi = 0; fi < nf; ++fi) {
auto fv = mesh.face_vertices(static_cast<int>(fi));
if (fv.size() != 3) continue;
// 外壳面(正向 winding
all_tris.push_back({fv[0], fv[1], fv[2]});
// 内壳面(反向 winding
if (static_cast<int>(fi) != face_index) {
all_tris.push_back({fv[0] + in_offset, fv[2] + in_offset, fv[1] + in_offset});
}
}
// ── 5. 缝合边界 → 生成侧壁三角面 ──
// 遍历所有边,识别仅被一个面使用的边界边(开口面边缘)
// 简化方案:对开口面 face_index 的每条边生成侧壁
if (face_index >= 0 && static_cast<size_t>(face_index) < nf) {
auto fv = mesh.face_vertices(face_index);
size_t fvn = fv.size();
for (size_t j = 0; j < fvn; ++j) {
int a = fv[j];
int b = fv[(j + 1) % fvn];
// 侧壁四边形:外 a→b 和内 a→b
int a_out = a, b_out = b;
int a_in = a + in_offset, b_in = b + in_offset;
all_tris.push_back({a_out, b_out, b_in});
all_tris.push_back({a_out, b_in, a_in});
}
}
HalfedgeMesh result;
result.build_from_triangles(all_verts, all_tris);
return result;
}
// ═════════════════════════════════════════════════════════════
// Mesh Draft — 拔模
// ═════════════════════════════════════════════════════════════
HalfedgeMesh mesh_draft(const HalfedgeMesh& mesh,
const std::vector<int>& face_indices,
double angle,
const Vector3D& pull_direction) {
if (face_indices.empty() || mesh.num_faces() == 0)
return mesh;
// ── 归一化拔模方向 ──
double pd_len = std::sqrt(pull_direction.x() * pull_direction.x() +
pull_direction.y() * pull_direction.y() +
pull_direction.z() * pull_direction.z());
if (pd_len < 1e-12) return mesh;
Vector3D dir(pull_direction.x() / pd_len,
pull_direction.y() / pd_len,
pull_direction.z() / pd_len);
// ── 收集需要拔模的面和它们的顶点 ──
std::unordered_set<int> draft_faces(face_indices.begin(), face_indices.end());
std::unordered_set<int> affected_verts;
for (int fi : face_indices) {
if (fi < 0 || static_cast<size_t>(fi) >= mesh.num_faces()) continue;
auto fv = mesh.face_vertices(fi);
for (int vi : fv)
affected_verts.insert(vi);
}
// ── 为每个受影响的面计算旋转轴和旋转矩阵 ──
// 拔模原理:将面的顶点绕旋转轴(面法向 × pull_direction)旋转 angle 度
// 旋转中心:面的中心点(所有顶点的平均值)
double cos_a = std::cos(angle);
double sin_a = std::sin(angle);
// 对于每个顶点,计算其在面中的旋转轴
// 为每个面单独处理
std::unordered_map<int, Vector3D> face_normals_map;
std::unordered_map<int, Point3D> face_centers_map;
std::unordered_map<int, Vector3D> face_axes_map;
for (int fi : face_indices) {
if (fi < 0 || static_cast<size_t>(fi) >= mesh.num_faces()) continue;
auto fv = mesh.face_vertices(fi);
if (fv.size() < 3) continue;
// 面的中心
double cx = 0, cy = 0, cz = 0;
for (int vi : fv) {
const auto& p = mesh.vertex(static_cast<size_t>(vi));
cx += p.x(); cy += p.y(); cz += p.z();
}
cx /= fv.size(); cy /= fv.size(); cz /= fv.size();
Point3D center(cx, cy, cz);
// 面的法向
const auto& p0 = mesh.vertex(static_cast<size_t>(fv[0]));
const auto& p1 = mesh.vertex(static_cast<size_t>(fv[1]));
const auto& p2 = mesh.vertex(static_cast<size_t>(fv[2]));
Vector3D e1(p1.x() - p0.x(), p1.y() - p0.y(), p1.z() - p0.z());
Vector3D e2(p2.x() - p0.x(), p2.y() - p0.y(), p2.z() - p0.z());
Vector3D fn = cross(e1, e2);
double fn_len = std::sqrt(fn.x() * fn.x() + fn.y() * fn.y() + fn.z() * fn.z());
if (fn_len < 1e-12) continue;
fn = Vector3D(fn.x() / fn_len, fn.y() / fn_len, fn.z() / fn_len);
// 旋转轴 = 面法向 × pull_direction
Vector3D axis = cross(fn, dir);
double ax_len = std::sqrt(axis.x() * axis.x() + axis.y() * axis.y() + axis.z() * axis.z());
if (ax_len < 1e-12) {
// 面法向与拔模方向平行,无法拔模
continue;
}
axis = Vector3D(axis.x() / ax_len, axis.y() / ax_len, axis.z() / ax_len);
face_normals_map[fi] = fn;
face_centers_map[fi] = center;
face_axes_map[fi] = axis;
}
// ── 应用旋转到受影响顶点 ──
std::vector<Point3D> new_verts;
for (size_t i = 0; i < mesh.num_vertices(); ++i)
new_verts.push_back(mesh.vertex(i));
for (int vi : affected_verts) {
Point3D pt = mesh.vertex(static_cast<size_t>(vi));
// 使用第一个包含该顶点的拔模面来确定旋转(简化处理)
for (int fi : face_indices) {
auto fv = mesh.face_vertices(fi);
bool found = false;
for (int fvi : fv) {
if (fvi == vi) { found = true; break; }
}
if (!found) continue;
if (face_axes_map.find(fi) == face_axes_map.end()) continue;
const Vector3D& axis = face_axes_map[fi];
const Point3D& center = face_centers_map[fi];
// Rodrigues' rotation 绕穿过 center 的轴旋转
// v = pt - center
Vector3D v(pt.x() - center.x(), pt.y() - center.y(), pt.z() - center.z());
double dot_kv = axis.x() * v.x() + axis.y() * v.y() + axis.z() * v.z();
Vector3D kxv = cross(axis, v);
Vector3D v_rot(
v.x() * cos_a + kxv.x() * sin_a + axis.x() * dot_kv * (1.0 - cos_a),
v.y() * cos_a + kxv.y() * sin_a + axis.y() * dot_kv * (1.0 - cos_a),
v.z() * cos_a + kxv.z() * sin_a + axis.z() * dot_kv * (1.0 - cos_a)
);
new_verts[static_cast<size_t>(vi)] = Point3D(
center.x() + v_rot.x(),
center.y() + v_rot.y(),
center.z() + v_rot.z()
);
break; // 只用第一个包含该顶点的面
}
}
// ── 重建网格 ──
std::vector<std::array<int, 3>> tris;
for (size_t fi = 0; fi < mesh.num_faces(); ++fi) {
auto fv = mesh.face_vertices(static_cast<int>(fi));
if (fv.size() == 3)
tris.push_back({fv[0], fv[1], fv[2]});
}
HalfedgeMesh result;
result.build_from_triangles(new_verts, tris);
return result;
}
} // namespace vde::mesh
-1
View File
@@ -10,4 +10,3 @@ add_vde_test(test_fea_mesh)
add_vde_test(test_hex_mesh)
add_vde_test(test_visualization)
add_vde_test(test_mesh_to_curve)
add_vde_test(test_mesh_blend)
-206
View File
@@ -1,206 +0,0 @@
#include <gtest/gtest.h>
#include "vde/mesh/mesh_blend.h"
#include "vde/mesh/halfedge_mesh.h"
#include <cmath>
using namespace vde::mesh;
// ── Helper: 创建立方体网格(12三角面,8顶点) ──
static HalfedgeMesh make_cube_mesh(double size = 1.0) {
HalfedgeMesh mesh;
double h = size / 2.0;
mesh.build_from_triangles(
{
Point3D(-h, -h, -h), Point3D( h, -h, -h), Point3D( h, h, -h), Point3D(-h, h, -h),
Point3D(-h, -h, h), Point3D( h, -h, h), Point3D( h, h, h), Point3D(-h, h, h),
},
{
{0, 2, 1}, {0, 3, 2}, // 前 (-z)
{4, 5, 6}, {4, 6, 7}, // 后 (+z)
{0, 1, 5}, {0, 5, 4}, // 下 (-y)
{2, 3, 7}, {2, 7, 6}, // 上 (+y)
{0, 4, 7}, {0, 7, 3}, // 左 (-x)
{1, 2, 6}, {1, 6, 5}, // 右 (+x)
}
);
return mesh;
}
// ── Helper: 创建简单四面体(4顶点, 4三角面) ──
static HalfedgeMesh make_tetra_mesh() {
HalfedgeMesh mesh;
mesh.build_from_triangles(
{
Point3D(0, 0, 0), Point3D(1, 0, 0),
Point3D(0.5, 0.866, 0), Point3D(0.5, 0.289, 0.816),
},
{
{0, 1, 2}, // 底面
{0, 3, 1},
{1, 3, 2},
{2, 3, 0},
}
);
return mesh;
}
// ═══════════════════════════════════════════════════════════
// Mesh Fillet Tests (×3)
// ═══════════════════════════════════════════════════════════
TEST(MeshBlendTest, Fillet_ReturnsValidMesh) {
HalfedgeMesh cube = make_cube_mesh(2.0);
ASSERT_GT(cube.num_edges(), 0);
// 对边0(前下面的一条边)做圆角
HalfedgeMesh result = mesh_fillet(cube, 0, 0.05, 4);
// 结果必须是有效网格
EXPECT_GT(result.num_vertices(), 0);
EXPECT_GT(result.num_faces(), 0);
// 圆角会新增顶点和面
EXPECT_GT(result.num_vertices(), cube.num_vertices());
}
TEST(MeshBlendTest, Fillet_InvalidEdgeReturnsOriginal) {
HalfedgeMesh cube = make_cube_mesh(2.0);
size_t invalid_idx = cube.num_edges() + 10;
HalfedgeMesh result = mesh_fillet(cube, invalid_idx, 0.1, 4);
// 无效边应返回原始网格副本
EXPECT_EQ(result.num_vertices(), cube.num_vertices());
EXPECT_EQ(result.num_edges(), cube.num_edges());
}
TEST(MeshBlendTest, Fillet_ZeroRadiusReturnsOriginal) {
HalfedgeMesh cube = make_cube_mesh(2.0);
HalfedgeMesh result = mesh_fillet(cube, 0, 0.0, 4);
// 零半径应返回原始网格
EXPECT_EQ(result.num_vertices(), cube.num_vertices());
}
// ═══════════════════════════════════════════════════════════
// Mesh Chamfer Tests (×3)
// ═══════════════════════════════════════════════════════════
TEST(MeshBlendTest, Chamfer_ReturnsValidMesh) {
HalfedgeMesh cube = make_cube_mesh(2.0);
ASSERT_GT(cube.num_edges(), 0);
HalfedgeMesh result = mesh_chamfer(cube, 0, 0.05, 0.05);
EXPECT_GT(result.num_vertices(), 0);
EXPECT_GT(result.num_faces(), 0);
// 倒角会新增顶点(4个offset顶点)
EXPECT_GT(result.num_vertices(), cube.num_vertices());
}
TEST(MeshBlendTest, Chamfer_InvalidEdgeReturnsOriginal) {
HalfedgeMesh cube = make_cube_mesh(2.0);
size_t invalid_idx = cube.num_edges() + 5;
HalfedgeMesh result = mesh_chamfer(cube, invalid_idx, 0.05);
EXPECT_EQ(result.num_vertices(), cube.num_vertices());
EXPECT_EQ(result.num_edges(), cube.num_edges());
}
TEST(MeshBlendTest, Chamfer_Dist2Default) {
HalfedgeMesh cube = make_cube_mesh(2.0);
// dist2 < 0 应使用 dist1
HalfedgeMesh result = mesh_chamfer(cube, 0, 0.05, -1.0);
EXPECT_GT(result.num_vertices(), cube.num_vertices());
}
// ═══════════════════════════════════════════════════════════
// Mesh Shell Tests (×3)
// ═══════════════════════════════════════════════════════════
TEST(MeshBlendTest, Shell_ClosedShell) {
HalfedgeMesh cube = make_cube_mesh(2.0);
// 完全封闭抽壳(face_index = -1
HalfedgeMesh result = mesh_shell(cube, 0.1, -1);
EXPECT_GT(result.num_vertices(), 0);
EXPECT_GT(result.num_faces(), 0);
// 抽壳:外壳 + 内壳 = 2×顶点
EXPECT_EQ(result.num_vertices(), cube.num_vertices() * 2);
// 面数:外壳12 + 内壳12 = 24
EXPECT_EQ(result.num_faces(), cube.num_faces() * 2);
}
TEST(MeshBlendTest, Shell_WithOpenFace) {
HalfedgeMesh cube = make_cube_mesh(2.0);
// 顶部开口(face_index=0,即前面)
HalfedgeMesh result = mesh_shell(cube, 0.1, 0);
EXPECT_GT(result.num_vertices(), 0);
EXPECT_GT(result.num_faces(), 0);
// 顶点 = 2×原始(外壳+内壳)
EXPECT_EQ(result.num_vertices(), cube.num_vertices() * 2);
}
TEST(MeshBlendTest, Shell_ZeroThickness) {
HalfedgeMesh cube = make_cube_mesh(2.0);
HalfedgeMesh result = mesh_shell(cube, 0.0, -1);
// 零厚度抽壳仍然生成内外壳
EXPECT_EQ(result.num_vertices(), cube.num_vertices() * 2);
}
// ═══════════════════════════════════════════════════════════
// Mesh Draft Tests (×3)
// ═══════════════════════════════════════════════════════════
TEST(MeshBlendTest, Draft_ReturnsValidMesh) {
HalfedgeMesh cube = make_cube_mesh(2.0);
// 对前面(face 0)拔模 5°
HalfedgeMesh result = mesh_draft(cube, {0}, M_PI / 36.0,
Vector3D(0, 0, 1));
EXPECT_EQ(result.num_vertices(), cube.num_vertices());
EXPECT_EQ(result.num_faces(), cube.num_faces());
}
TEST(MeshBlendTest, Draft_EmptyFaceListReturnsOriginal) {
HalfedgeMesh cube = make_cube_mesh(2.0);
HalfedgeMesh result = mesh_draft(cube, {}, M_PI / 18.0,
Vector3D(0, 0, 1));
EXPECT_EQ(result.num_vertices(), cube.num_vertices());
EXPECT_EQ(result.num_faces(), cube.num_faces());
}
TEST(MeshBlendTest, Draft_MultipleFaces) {
HalfedgeMesh cube = make_cube_mesh(2.0);
// 对侧面拔模(面4、5是底面,面6、7是顶面,它们的法向不平行于Z轴)
HalfedgeMesh result = mesh_draft(cube, {4, 5, 6, 7}, M_PI / 18.0,
Vector3D(0, 0, 1));
EXPECT_EQ(result.num_vertices(), cube.num_vertices());
EXPECT_EQ(result.num_faces(), cube.num_faces());
// 拔模后顶点位置应有变化
bool any_changed = false;
for (size_t i = 0; i < result.num_vertices() && i < cube.num_vertices(); ++i) {
double dx = result.vertex(i).x() - cube.vertex(i).x();
double dy = result.vertex(i).y() - cube.vertex(i).y();
double dz = result.vertex(i).z() - cube.vertex(i).z();
if (std::abs(dx) > 1e-9 || std::abs(dy) > 1e-9 || std::abs(dz) > 1e-9) {
any_changed = true;
break;
}
}
EXPECT_TRUE(any_changed);
}